> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sedata-ai.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom instrumentation

> Patterns for adding your own spans, histograms, and counters.

The auto-instrumentation gives you per-tool spans for free. For everything
else — cache lookups, upstream HTTP calls, business outcomes — use the
helpers on the `ObservabilityInstance` returned by `instrumentServer`.

## Setup once, reuse everywhere

Create your instruments once at module scope so you're not reallocating
histograms or counters on the hot path.

```ts instruments.ts theme={null}
import { instrumentServer } from '@sedata-ai/mcp'

export const telemetry = instrumentServer(server, telemetryConfig)

export const recordCacheLookup = telemetry.getHistogram('cache.lookup.duration', {
  description: 'Cache lookup duration',
  unit: 'ms',
})

export const incCacheOutcome = telemetry.getIncrementCounter('cache.outcome.count', {
  description: 'Cache hit/miss outcome',
})
```

Then import where you need them:

```ts theme={null}
import { telemetry, recordCacheLookup, incCacheOutcome } from './instruments'
```

## Pattern: time + count an operation

```ts theme={null}
async function fetchUser(id: string) {
  return telemetry.startActiveSpan(
    'user.fetch',
    { 'user.id': id },
    async (span) => {
      const t0 = Date.now()
      try {
        const user = await db.user.findUnique({ where: { id } })
        span.setAttribute('user.found', !!user)
        return user
      } catch (err) {
        span.recordException(err as Error)
        span.setStatus({ code: 2, message: (err as Error).message })
        throw err
      } finally {
        span.setAttribute('user.fetch.duration_ms', Date.now() - t0)
        span.end()
      }
    },
  )
}
```

The new span becomes a child of the auto-generated `tools/call <toolName>`
span if you call this from inside a tool handler.

## Pattern: record a histogram with tags

```ts theme={null}
const t0 = Date.now()
const value = cache.get(key)
recordCacheLookup(Date.now() - t0, {
  'cache.hit': !!value,
  'cache.key.kind': 'user',
})
incCacheOutcome(1, { outcome: value ? 'hit' : 'miss' })
```

## Pattern: business outcomes

Counters are great for outcome tracking even when nothing is timed:

```ts theme={null}
const incPaymentOutcome = telemetry.getIncrementCounter('payment.outcome.count', {
  description: 'Payment outcome',
})

incPaymentOutcome(1, { result: 'authorized', method: 'card' })
incPaymentOutcome(1, { result: 'declined',  method: 'card', code: 'insufficient_funds' })
```

## Pattern: distributed trace context

If a tool calls an upstream HTTP service, propagate the active context so the
upstream's spans hang under your trace:

```ts theme={null}
import { context, propagation } from '@opentelemetry/api'

await telemetry.startActiveSpan(
  'github.repos.get',
  { 'http.method': 'GET', 'http.target': '/repos/x/y' },
  async (span) => {
    const headers: Record<string, string> = {}
    propagation.inject(context.active(), headers)

    const res = await fetch('https://api.github.com/repos/x/y', { headers })
    span.setAttribute('http.status_code', res.status)
    span.end()
  },
)
```

The upstream service (if it's also instrumented with OTel) will continue your
trace.

## Pattern: feature-flag the SDK

If you want to gate instrumentation:

```ts theme={null}
const enabled = process.env.ENABLE_SEDATA === 'true'
const telemetry = enabled
  ? instrumentServer(server, config)
  : noopTelemetry()

function noopTelemetry() {
  const noop = () => {}
  return {
    startActiveSpan: (_n: string, _a: any, fn: any) => fn({ end: noop, setAttribute: noop, setStatus: noop, recordException: noop }),
    getHistogram: () => noop,
    getIncrementCounter: () => noop,
    processTelemetryAttributes: (d: any) => d,
    shutdown: async () => {},
  }
}
```

## Pattern: async iterators

If your tool streams events, record one span per batch + a counter per event:

```ts theme={null}
const incEvent = telemetry.getIncrementCounter('stream.event.count', {
  description: 'Stream event count',
})

for await (const evt of stream) {
  incEvent(1, { type: evt.type })
}
```

For a span-per-event you'd lose more in overhead than you'd gain in
visibility — counters are usually the right shape for high-frequency events.

## Pattern: shut down on signal

```ts theme={null}
const stop = async (code = 0) => {
  await telemetry.shutdown()
  process.exit(code)
}

process.on('SIGINT', () => stop(0))
process.on('SIGTERM', () => stop(0))
process.on('uncaughtException', (err) => {
  console.error(err)
  stop(1)
})
```

## See also

<CardGroup cols={2}>
  <Card title="Automatic metrics" icon="gauge" href="/metrics-and-traces/automatic-metrics">
    What the package records without any code.
  </Card>

  <Card title="Data processors" icon="filter" href="/guides/data-processors">
    Mutate every attribute set in one place.
  </Card>
</CardGroup>
