Skip to content

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.

On this page

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

AreaSpansMetricsSetting
Served HTTP requestsSERVER span per requesthttp.server.request.duration, http.server.active_requests, elide.http.server.rejected_requestsruntime.traces, runtime.metrics
fetchCLIENT span per requesthttp.client.request.durationruntime.traces, runtime.metrics
Process—CPU time, memory, threads, file descriptors, uptimeruntime.metrics
Errorsexception eventselide.errorsfollows logs, traces, metrics
elide build, elide test, elide installCommand, task, and test spans—runtime.traces
Elide’s own logs——runtime.logs

Names, units, and attributes are listed in Emitted telemetry.

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");
    });
  },
};
utf-8 · ts · Lines: 14
Terminal
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.

Request rate comes from the count of http.server.request.duration: divide the increase in count by the elapsed seconds. These counts include unsampled requests, so changing trace sampling does not change the reported throughput. http.server.active_requests tracks requests still in flight.

http.server.request.body.size and http.server.response.body.size record body sizes in bytes. Known empty bodies contribute zero; unknown response sizes are omitted. These histograms share the duration metric’s method, scheme, protocol, status, error, and known route dimensions. Raw paths, query strings, user agents, and baggage are not automatic metric dimensions. Body-size metric names follow OpenTelemetry’s development semantic conventions.

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);
utf-8 · ts · Lines: 10

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();
});
utf-8 · ts · Lines: 6

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

Enable build reports

Build-report export is opt-in: configure an OTLP destination with CLI flags, elide.pkl, or environment variables. With no destination configured, Elide exports no build or test telemetry. Once a destination accepts traces, runtime.traces enables the command, task, execution-unit, and individual-test spans together; it defaults to true. There is no separate build-report flag.

For one invocation, point Elide at a running OTLP/HTTP collector. Place these global flags before the command:

Terminal
bash
elide --otel-endpoint=http://localhost:4318 --otel-protocol=http/protobuf build
elide --otel-endpoint=http://localhost:4318 --otel-protocol=http/protobuf test

For a project, add this configuration to elide.pkl (merge the block into an existing manifest):

pkl
amends "elide:project.pkl"

name = "my-project"

opentelemetry {
  service { name = "my-project-builds" }
  destinations {
    ["collector"] {
      endpoint = "http://localhost:4318"
      protocol = "http/protobuf"
      signals { "traces" }
    }
  }
  runtime { traces = true }
}
utf-8 · pkl · Lines: 15

Then run elide build or elide test normally. Replace the endpoint with your collector’s base URL; Elide appends /v1/traces for HTTP export. For gRPC, use port 4317 and protocol grpc. The destination above exports only traces. Configure authentication through environment-backed headers.

To suppress telemetry for one invocation, even when the project or environment enables it:

Terminal
bash
elide --no-otel build
elide --no-otel test

In the manifest, opentelemetry { enabled = false } disables all telemetry. To keep application telemetry but suppress automatic build/test spans, set opentelemetry { runtime { traces = false } }; this also disables Elide’s automatic HTTP spans. Setting only traces { enabled = false } controls application spans and does not disable runtime spans. OTEL_TRACES_EXPORTER=none disables both kinds of trace export.

To verify export, find elide.build or elide.test in your collector under the configured service name, then group its descendants by elide.invocation.id. Task, execution, and test spans appear when that work actually runs. Sampling still applies: an unsampled upstream TRACEPARENT suppresses spans with the default parent-based sampler. For a local diagnostic run, OTEL_TRACES_SAMPLER=always_on records every span; it does not guarantee delivery if an exporter queue fills. See Troubleshooting if no spans arrive.

ELIDE_BUILD_TELEMETRY is a separate local timing-profile switch. It is not required for OTLP build reports and does not enable OTLP export.

Correlate builds and tests

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 nests under its execution unit (a JS file or JVM launcher fork), which nests under the owning task. The root span continues a W3C TRACEPARENT from the environment, so a CI job’s own trace can include the build:

Terminal
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) and process.exit.code. Elide waits up to 5 seconds for the export before the command returns.

Build-report spans carry schema version 1, an elide.span.type, and an elide.invocation.id on every span. The invocation ID combines the command’s trace ID and span ID: two commands can share a CI trace without merging into one build. Use this ID for a single report, and workspace/project name plus task path and test identity to compare reports across runs. Scope comparisons to the same repository or service; project names alone are not globally unique. CI revision and pipeline attributes remain on the command span.

Task spans include their implementation type, workspace-qualified path, prerequisite paths, cacheability, and outcome. Driver operations remain nested under tasks. JS files have zero-based shard indexes and the discovered file count; JVM source-set forks have separate execution IDs, including discovery fallback forks. This describes local execution units; it does not add distributed sharding. Each execution reports result counts and becomes an error on a failed test or unsuccessful execution, even when the runner returned normally.

Individual tests carry a stable, length-prefixed display-path identity and an occurrence number within their execution. Identical display paths intentionally share a comparison key; occurrences distinguish emitted results, not retries. JUnit’s text feed cannot yet provide native unique IDs. Test span positions are reconstructed from result arrival and reported duration, clamped to their execution’s lifetime; elide.test.duration_ns preserves the reported duration. This is sufficient for result comparison but not an exact concurrent-test timeline. Execution spans measure their full lifecycle. Filtered tests contribute to execution counts but emit no test span.

Telemetry remains subject to tracing configuration, sampling, and exporter queue limits. It is not a lossless build event archive. Structured JUnit identities/start events, per-task scheduler wait, artifact provenance, and a durable report store are follow-up work; this schema does not claim complete Bazel or Gradle report parity.

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
utf-8 · yaml · Lines: 6

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.

Rust instrumentation and library diagnostics also use this runtime scope. Structured log fields and span parentage are retained. Console output follows ELIDE_LOG and the active console or progress logger; export independently follows runtime.logs. Application log and trace settings do not enable runtime signals.

Compio’s internal task and poll spans are off by default. Set ELIDE_OTEL_RUNTIME_INTERNAL_TRACES=true to include them when runtime tracing is enabled and a telemetry destination is configured. This can produce many spans; library log events still follow runtime.logs. Tokio’s unstable internal tracing is not enabled in shipped builds. Library events do not automatically become metrics, and exporter diagnostics are excluded from export to prevent feedback.

Not instrumented yet

AreaWorkaround
node:http and node:https client requests: no client span, no traceparentInject headers with propagation.inject; see Trace context
Python HTTP clients (urllib, requests)Inject headers with opentelemetry.propagate.inject
Serving from PythonServe from JavaScript or TypeScript
Garbage collection and event-loop delay metrics—
Database, file system, and child process spansCreate spans in your code
Java and Kotlin programsNot supported; see Languages

Was this page helpful?