Repository navigation
feat: add context api #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then I think
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| ### 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 | ||
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, a
Contextis 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 aContextholds. It can only make a newContextwith different ones, and pass the newContextto child receiver.The objects that a
Contextrefers to can still be mutable. AnOperationor aSpanin aContextstands for the operation itself, so the receiver can update it like setting the target info or adding attributes to the spans. Maybe thecontext.setTargetin 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
TraceContextor theBaggage, because those describe this Context's position in the trace and are meant to be scoped to it.