# Runtime instrumentation

> What Elide instruments without code changes: server spans and metrics for served requests, fetch client spans, process metrics, build and test spans in CI with TRACEPARENT, and what is not instrumented yet.

With a destination configured, Elide records these without code changes:

| Area | Spans | Metrics | Setting |
| --- | --- | --- | --- |
| Served HTTP requests | `SERVER` span per request | `http.server.request.duration`, `http.server.active_requests`, `elide.http.server.rejected_requests` | `runtime.traces`, `runtime.metrics` |
| `fetch` | `CLIENT` span per request | `http.client.request.duration` | `runtime.traces`, `runtime.metrics` |
| Process | — | CPU time, memory, threads, file descriptors, uptime | `runtime.metrics` |
| Errors | `exception` events | `elide.errors` | follows logs, traces, metrics |
| `elide build`, `elide test`, `elide install` | Command, task, and test spans | — | `runtime.traces` |
| Elide's own logs | — | — | `runtime.logs` |

Names, units, and attributes are listed in [Emitted telemetry](https://elide.help/docs/opentelemetry/reference).

## Serving

Every request served by `elide serve`, an exported `fetch` handler, or a `node:http` server gets a server span and
the HTTP server metrics. The handler runs inside the span, so spans your code starts there are its children:

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

const tracer = trace.getTracer("orders");

export default {
  async fetch(request: Request): Promise<Response> {
    return tracer.startActiveSpan("load order", async (span) => {
      span.setAttribute("order.id", new URL(request.url).searchParams.get("id") ?? "");
      span.end();
      return new Response("ok\n");
    });
  },
};
```

```bash
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 elide serve server.ts
```

A request carrying a valid `traceparent` continues the caller's trace. A 5xx response, a handler that throws, an
abandoned response, or a reset connection marks the server span `ERROR`.

### Routes

Server spans are named after the method: `GET`. A `node:http` server can name the matched route, which sets
`http.route` and renames the span `GET /items/:id`:

```ts
// routes.ts
import http from "node:http";
import { trace } from "@opentelemetry/api";

http
  .createServer((req, res) => {
    trace.getActiveSpan()?.setAttribute("http.route", "/items/:id");
    res.end("ok\n");
  })
  .listen(8091);
```

A router that sets `req.route` (Express and compatible routers) supplies the route without that line; `req.baseUrl`
is prefixed. Inside a `node:http` handler or an exported `fetch` handler the active span stands for the server
span, and only `http.route` set on it reaches the exported span.

## fetch

Each `fetch` gets a client span, parented to the span active where `fetch` was called, and sends `traceparent` and
`tracestate` so the next service continues the trace. A `traceparent` header you set yourself is kept.

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

await trace.getTracer("sync").startActiveSpan("sync inventory", async (span) => {
  await fetch("https://inventory.example.com/items"); // child CLIENT span
  span.end();
});
```

## Process metrics

`process.cpu.time`, `process.memory.usage`, `process.thread.count`, `process.unix.file_descriptor.count`, and
`process.uptime`, collected at each metric export on Linux and macOS.

## Builds and tests in CI

`elide build`, `elide test`, and `elide install` record a root span for the command (`elide.build`, `elide.test`,
`elide.install`). Build stages and tasks become child spans with outcome and cache-hit attributes, and each test case
becomes a child of `elide.test`. The root span continues a W3C `TRACEPARENT` from the environment, so a CI job's own
trace can include the build:

```bash
TRACEPARENT=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 \
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com \
elide test
```

The command span carries `elide.ci.*` attributes from GitHub Actions, GitLab CI, Buildkite, and CircleCI variables
(see [Emitted telemetry](https://elide.help/docs/opentelemetry/reference#toolchain)) and `process.exit.code`. Elide waits up to 5 seconds
for the export before the command returns.

In GitHub Actions:

```yaml
- name: Test
  run: elide test
  env:
    OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.OTEL_ENDPOINT }}
    OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_HEADERS }}
    OTEL_SERVICE_NAME: my-service-ci
```

## Elide's own logs

Elide's warnings and errors are exported as log records in the `dev.elide.runtime` scope, at or above `runtime.logs`
(`warn` by default), with the active span's ids. Set `runtime { logs = "off" }` to stop them.

## Not instrumented yet

| Area | Workaround |
| --- | --- |
| `node:http` and `node:https` client requests: no client span, no `traceparent` | Inject headers with `propagation.inject`; see [Trace context](https://elide.help/docs/opentelemetry/correlation#trace-context-over-http) |
| Python HTTP clients (`urllib`, `requests`) | Inject headers with `opentelemetry.propagate.inject` |
| Serving from Python | Serve from JavaScript or TypeScript |
| Garbage collection and event-loop delay metrics | — |
| Database, file system, and child process spans | Create spans in your code |
| Java and Kotlin programs | Not supported; see [Languages](https://elide.help/docs/opentelemetry#languages) |
