# OpenTelemetry in JavaScript and TypeScript

> Use @opentelemetry/api in JavaScript and TypeScript on Elide without an SDK: built-in and installed copies, spans with using, metrics, logs, SDK takeover and handback, the elide:telemetry module, and diagnostics_channel events.

Instrument code with the standard [`@opentelemetry/api`](https://www.npmjs.com/package/@opentelemetry/api) and
`@opentelemetry/api-logs`. Elide implements them: there is no SDK, exporter, or collector to add to the
application, and libraries that already use the API work unchanged.

```ts
// app.ts
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("checkout");
await tracer.startActiveSpan("charge", async (span) => {
  span.setAttribute("order.id", 42);
  console.log("charged");
  span.end();
});
```

```bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 elide run app.ts
```

## The API package

| Your project | `import "@opentelemetry/api"` resolves to |
| --- | --- |
| No `@opentelemetry/api` installed | Elide's built-in copy (API 1.9) |
| `@opentelemetry/api` 1.x installed | Your copy, backed by Elide |

The same holds for `@opentelemetry/api-logs` and `@opentelemetry/api/experimental`. Install the package as a dev
dependency when you want its TypeScript types:

```bash
elide add -D @opentelemetry/api
```

Elide registers its tracer, meter, logger, context manager, and propagator in the API's global registry before your
code runs, so any 1.x copy finds them. When telemetry is not configured, the API returns no-op implementations.

## Traces

Tracers, spans, links, events, and status follow the OpenTelemetry API and SDK rules: invalid attribute values are
dropped with a warning, operations on ended spans warn, `UNSET` status is ignored, and `OK` is final.

Spans implement `Symbol.dispose`, so `using` ends them at the end of the block:

```ts
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("jobs");
{
  using span = tracer.startSpan("nightly");
  span.setAttribute("job.items", 120);
} // span ends here
```

Elide's tracers also have `withSpan(name, [options], [context], fn)`: it runs `fn` with a new active span, ends the
span when `fn` returns or its promise settles, and records a throw or rejection as an `ERROR`:

```ts
const tracer = trace.getTracer("jobs") as any;
await tracer.withSpan("import", async (span) => {
  await importRows();
});
```

`wrapTracer` from `@opentelemetry/api/experimental` (or [`elide:telemetry`](#elidetelemetry)) gives any tracer the
same `withSpan` and `withActiveSpan`, typed.

Context follows `await`, promises, timers, event emitters, and callbacks; it is backed by `AsyncLocalStorage`.
Request handlers run inside their server span.

## Metrics

```ts
import { metrics } from "@opentelemetry/api";

const pending: string[] = [];
const meter = metrics.getMeter("checkout");
const orders = meter.createCounter("orders", { unit: "{order}" });
const latency = meter.createHistogram("charge.duration", {
  unit: "s",
  advice: { explicitBucketBoundaries: [0.01, 0.05, 0.1, 0.5, 1] },
});
const queue = meter.createObservableGauge("queue.depth");
queue.addCallback((result) => result.observe(pending.length));

orders.add(1, { region: "eu" });
latency.record(0.042);
```

All synchronous and observable instruments are supported. Observable callbacks run on the program's event loop at
each collection; a callback may return a promise. Measurements are validated as the SDK does (counters and
histograms reject negative values).

## Logs

```ts
import { logs, SeverityNumber } from "@opentelemetry/api-logs";

logs.getLogger("checkout").emit({
  severityNumber: SeverityNumber.WARN,
  severityText: "WARN",
  body: { event: "retry", attempt: 2 },
});
```

Non-string bodies export as structured values. `console.*` output is exported too while `logs.console` is on; see
[Logs](https://elide.help/docs/opentelemetry/signals#logs).

## Propagation

`propagation.inject` and `propagation.extract` use the propagators from `traces.propagators` or `OTEL_PROPAGATORS`
(default `tracecontext,baggage`). Baggage set with `propagation.setBaggage` stays in your code's context; Elide's HTTP
clients send it only when outbound baggage is on. See [Correlation and baggage](https://elide.help/docs/opentelemetry/correlation).

## Using an OpenTelemetry SDK

You can still bring the JavaScript SDK (`@opentelemetry/sdk-node`, `sdk-trace-*`, `sdk-metrics`, `sdk-logs`) and its
exporters. When it registers a provider, it takes that signal over:

| Event | Result |
| --- | --- |
| SDK registers a tracer, meter, or context manager | Elide steps aside for that signal and prints a warning. Tracers, meters, and observable callbacks already handed out forward to the SDK. |
| `@opentelemetry/sdk-logs` is loaded | Elide frees the logs global; its loggers keep recording until the SDK registers a logger provider, then forward to it. |
| SDK calls `disable()` on a global | The signal returns to Elide. |
| SDK is loaded but registers nothing | Elide stays in place. |

The warning names the signal:

```text
[warn] dev.elide.lang.javascript.telemetry.JsTelemetry An OpenTelemetry SDK
registered its own tracer provider; Elide's JavaScript tracer provider steps
aside
```

`status().steppedAside` then lists `trace`.

Call `release()` from `elide:telemetry` before the SDK starts to hand it every signal without warnings. Elide's own
server, client, and error telemetry keeps recording through Elide either way, and request handlers run under
whichever context manager is registered.

## `elide:telemetry`

Controls the API does not cover. Types ship in `@elide-dev/types`; every function is safe to call when telemetry is
not configured.

```ts
import { trace } from "@opentelemetry/api";
import { flush, status } from "elide:telemetry";

{
  using span = trace.getTracer("jobs").startSpan("nightly");
}
const ok = await flush();
console.log(ok, status().spans);
```

| Export | Type | Description |
| --- | --- | --- |
| `active` | `boolean` | Whether telemetry is configured for this process. |
| `flush(timeoutMillis?)` | `(timeoutMillis?: number) => Promise<boolean>` | Exports everything recorded so far, waiting up to `timeoutMillis` (default `5000`). Resolves with whether every destination accepted it; never rejects. |
| `status()` | `() => TelemetryStatus` | Approximate, process-wide pipeline counters; see below. |
| `release()` | `() => boolean` | Stops serving as the global OpenTelemetry implementation for good, so an SDK registered afterwards owns every signal. Elide's runtime instrumentation keeps recording. Returns `false` if already released. |
| `enter(ctx)` | `(context: Context) => ContextScope` | Makes `ctx` active for the rest of the current synchronous scope and the async work it starts. `exit()` or `using` restores the previous context. Warns and does nothing when an SDK registered its own context manager. |
| `wrapTracer(tracer)` | `(tracer: Tracer) => SugaredTracer` | The `SugaredTracer` of `@opentelemetry/api/experimental`: `withActiveSpan` and `withSpan` end the span with the callback and record a throw or rejection. `options.onException` replaces that recording. |
| `currentSpanContext()` | `() => ActiveSpanContext \| undefined` | The active span's `traceId`, `spanId`, `traceFlags`, and `traceparent`, or `undefined` outside a valid span. |
| `api` | `typeof import("@opentelemetry/api")` | The API surface Elide serves. |
| `logs` | `typeof import("@opentelemetry/api-logs")` | The logs API surface Elide serves. |

```ts
interface TelemetryStatus {
  readonly active: boolean;
  readonly released: boolean;
  /** Signals an SDK took over: "trace", "metrics", "context", "propagation", "logs". */
  readonly steppedAside: readonly string[];
  readonly spans: {
    readonly started: number;
    readonly unsampled: number;
    readonly ended: number;
    readonly live: number;
    readonly exported: number;
    readonly dropped: number; // a destination queue was full
    readonly tableFull: number; // the open-span table was full
    readonly reclaimed: number;
    readonly handedOff: number; // sent at exit by the background exporter
    readonly exitDropped: number; // produced at exit and dropped
  };
  readonly logs: {
    readonly emitted: number;
    readonly filtered: number; // below logs.level
    readonly exported: number;
    readonly dropped: number;
    readonly handedOff: number;
    readonly exitDropped: number;
  };
  readonly metrics: { readonly handedOff: number; readonly exitDropped: number };
  readonly exportFailures: number;
  readonly exportsAbandoned: number; // in-process exports still running when exit ended them
}
```

Pass a context to code that runs outside the current one, such as a queue consumer:

```ts
import { context, trace } from "@opentelemetry/api";
import { currentSpanContext, enter } from "elide:telemetry";

const saved = context.active();
setImmediate(() => {
  using _scope = enter(saved);
  console.log(currentSpanContext()?.traceparent);
});
```

## `diagnostics_channel` events

Instrumentation libraries that subscribe to Node's diagnostics channels see the same events as on Node:

| Channel | Published by |
| --- | --- |
| `http.server.request.start` | `node:http` server, before the request handler |
| `http.server.response.created` | `node:http` server |
| `http.server.response.finish` | `node:http` server |
| `http.client.request.created`, `http.client.request.start` | `node:http` client |
| `http.client.request.error` | `node:http` client |
| `http.client.response.finish` | `node:http` client |
| `undici:request:create` | `fetch`, before dispatch (`request.origin`, `method`, `path`, `headers`) |

See [`node:diagnostics_channel`](https://elide.help/docs/api-node-diagnostics-channel).

## Samples

- [Custom span in TypeScript](https://elide.help/docs/code-samples#custom-span-in-typescript)
- [Metrics instruments](https://elide.help/docs/code-samples#metrics-instruments)
- [Baggage across services](https://elide.help/docs/code-samples#baggage-across-services)
