Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,19 @@ process.on('SIGTERM', async () => {

A flush that times out rejects, and the `finally` exits anyway.

### Context

`context` reads and sets the [Context](https://opentelemetry.io/docs/specs/otel/context/)
microtel propagates. See [docs/context.md](./docs/context.md).

## Instrumented operations

When tracing is active, spans are created automatically for the following
operations. Per the OpenTelemetry [semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/http-spans/), span
names must be low cardinality. Node.js core has no route concept, so spans are
named `{method}` and the request details live in the attributes. Requests the
named `{method}` and the request details live in the attributes. A server span
is named `{method} {target}` when a framework sets the target on the request's
[`Operation`](./docs/context.md#type-operation). Requests the
exporter itself sends to the collector's export path are never traced, on
either side; other requests, including other requests to the collector's
host, are.
Expand Down Expand Up @@ -135,6 +142,7 @@ Attributes set on server spans:
| `network.protocol.version` | HTTP version (e.g. `1.1`) | Always |
| `user_agent.original` | Client `User-Agent` header | When the header is present |
| `http.response.status_code` | Response status code | When response finishes |
| `http.route` | The target a framework set | When a target is set |
| `error.type` | HTTP status code as string | On 5xx responses |

The address attributes follow the proxy headers the way OpenTelemetry's
Expand Down Expand Up @@ -180,6 +188,7 @@ Attributes set on HTTP/2 server spans:
| `network.protocol.version` | `2.0` | Always |
| `user_agent.original` | Client `User-Agent` header | When the header is present |
| `http.response.status_code` | Response status code | When the response is sent |
| `http.route` | The target a framework set | When a target is set |
| `error.type` | HTTP status code as string | On 5xx responses |

The address attributes follow the same rules as for HTTP/1 server
Expand Down
182 changes: 182 additions & 0 deletions docs/context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Context

microtel propagates a [Context][] across async execution. microtel sets its
values while tracing is active, or an adapter can supply them. This
document has two sections, for different readers of the API:

- [API consumers](#api-consumers), such as web frameworks and logging
libraries, read values from the current Context.
- [Context adapters](#context-adapters) get and set microtel's values in
another implementation's Context objects, so both share one current
Context.

## API consumers

`microtel.context` holds a closed set of values, each with its own accessor.
There is no string-keyed access. Libraries that need other values keep them
Comment on lines +15 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite following this. Is this saying the context object is not mutable?

@legendecas legendecas Nov 11, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a Context is a snapshot, handed from one piece of code to the next. The OpenTelemetry Context spec also requires a context to be immutable. A receiver can't change which values a Context holds. It can only make a new Context with different ones, and pass the new Context to child receiver.

The objects that a Context refers to can still be mutable. An Operation or a Span in a Context stands for the operation itself, so the receiver can update it like setting the target info or adding attributes to the spans. Maybe the context.setTarget in this API makes this confusing, I can update it.

The intention is that a context receiver must not mutate the context directly, like a receiver must not modify the TraceContext or the Baggage, because those describe this Context's position in the trace and are meant to be scoped to it.

in their own `AsyncLocalStorage`. These functions work the same whichever
adapter is set.

A Context is immutable. No accessor changes which values a Context holds.
A value in a Context can still be a mutable object, such as the
[`Operation`](#type-operation).

Each accessor takes the Context to read. Get the current Context
once with `context.active()` and pass it to every accessor you call:

```js
const ctx = context.active();
const traceContext = context.getTraceContext(ctx);
const operation = context.getOperation(ctx);
```

### `context.active()`

- Returns: {any} The current Context, or `undefined` when none is current.

The Context is an opaque value. Pass it to the accessors without reading it
directly.

### Type: `TraceContext`

A read-only object. Modifying any of its fields is undefined behavior.

- `traceId` {string} The [trace-id][].
- `spanId` {string} The span's ID, propagated as the [parent-id][].
- `traceFlags` {number} The [trace-flags][].
- `traceState` {Object|undefined} The [tracestate header][], as an object with:
- `serialize()` Returns: {string} The header.
- `isRemote` {boolean|undefined} [IsRemote][], `true` when the trace context
came from another process.

### `context.getTraceContext(ctx)`

- `ctx` {any} A Context from `context.active()`.
- Returns: {TraceContext|undefined}

Returns the trace context of the span in `ctx`, or `undefined` when it
has none.

### Type: `Operation`

The metadata of the operation being traced. microtel puts an Operation in the
Context of each incoming request it traces. Every Context derived while the
operation is traced shares it, so a value set on it stays visible until the
operation ends.

- `target` {string|undefined} A low-cardinality name for what the operation
is performed against, such as the route template `/users/:id`. microtel
reads it when the operation's span ends, and records it on the span when it
is a non-empty string. Only the code that resolved the target, such as a
framework's router, should set it.

The target is the low-cardinality part of a span name that says what the
operation acts on, like `{target}` in an [HTTP span name][], the method in an
[RPC span name][], or `{destination}` in a [messaging span name][].

### `context.getOperation(ctx)`

- `ctx` {any} A Context from `context.active()`.
- Returns: {Operation|undefined}

Returns the operation being traced in `ctx`, or `undefined` when it has none.

```js
const operation = context.getOperation(context.active());
if (operation) operation.target = '/users/:id';
```

## Context adapters

### Adapter and store

microtel treats each Context as an opaque value. It never reads or builds
one directly, only through the adapter, so an adapter can represent a
Context however it likes, such as its own Context objects with values of its
own next to microtel's.

Two objects with separate roles handle Contexts:

- The **adapter** defines what a Context is. It gets microtel's values from
a Context and makes new Contexts with values set. It never decides which
Context is current.
- The **store** tracks which Context is current in each async execution. It
holds Contexts without looking inside them.

microtel starts with a default adapter, whose Contexts hold only microtel's
values, and a store of its own.

### `context.setAdapter(adapter)`

- `adapter` {Object} An [adapter](#adapter-interface).

Replaces the default adapter and returns a new [store](#store), which the
adapter uses to make its Contexts current.

Call it as soon as the process starts, before microtel traces any request. A
request already in flight keeps its Context in the old store, so the requests
it makes after the call start new traces.

It can be called once per Node.js environment, such as the main thread or a
worker. A second call throws `ERR_INVALID_STATE`.
Comment on lines +120 to +121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I think setAdapter is more appropriate. register implies that it can be done more than once.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setAdapter also sounds good to me. Open to API name suggestions.


### Adapter interface

An adapter implements the Context API's [Get value][] and [Set value][] for
microtel's [keys](#keys).

#### `adapter.getValue(context, key)`

- `context` A Context this adapter made.
- `key` {string} One of the [keys](#keys).

Returns microtel's value for `key` in `context`.

#### `adapter.setValues(context, values)`

- `context` A Context this adapter made, or `undefined` for the root
Context.
- `values` {Object} Values by [key](#keys).

Returns a new Context with each key of `values` set, or removed when its
value is `undefined`.

#### Keys

- `traceContext` The trace context `context.getTraceContext()` returns.
- `operation` {Operation} The [`Operation`](#type-operation)
`context.getOperation()` returns.

microtel may set other keys. An adapter maps the keys it understands onto its
own Context and stores the rest as they are, so a key microtel adds needs no
change to the adapter.

### Store

The store's methods are the Context API's [optional global operations][].

#### `store.active()`

Returns the current Context, or `undefined` for the root Context.

#### `store.with(context, fn[, ...args])`

Calls `fn` with `context` current, and returns what `fn` returns.

#### `store.attach(context)`

Makes `context` current, and returns a token whose `dispose()` restores the
previous Context.

[Context]: https://opentelemetry.io/docs/specs/otel/context/
[Get value]: https://opentelemetry.io/docs/specs/otel/context/#get-value
[HTTP span name]: https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name
[IsRemote]: https://opentelemetry.io/docs/specs/otel/trace/api/#isremote
[messaging span name]: https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#span-name
[optional global operations]: https://opentelemetry.io/docs/specs/otel/context/#optional-global-operations
[parent-id]: https://www.w3.org/TR/trace-context/#parent-id
[RPC span name]: https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#name
[Set value]: https://opentelemetry.io/docs/specs/otel/context/#set-value
[trace-flags]: https://www.w3.org/TR/trace-context/#trace-flags
[trace-id]: https://www.w3.org/TR/trace-context/#trace-id
[tracestate header]: https://www.w3.org/TR/trace-context/#tracestate-header
15 changes: 14 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,26 @@
//
// init() activates tracing (at most once per process, and it cannot be
// stopped), isActive() reports state, and flush() forces an export whose
// returned promise resolves once it has finished.
// returned promise resolves once it has finished. context reads and sets
// the propagated context.

const { start, isActive } = require('./lib/core');
const { flush } = require('./lib/flush');
const {
getCurrentContext,
getOperation,
getTraceContext,
setAdapter,
} = require('./lib/context');

module.exports = {
init: start,
isActive,
flush,
context: {
active: getCurrentContext,
getOperation,
getTraceContext,
setAdapter,
},
};
Loading
Loading