Skip to content

Instrumentation: enterWith leak, operation span reachable from the public API, context-store eviction, body-drain diagnostics, DEXPACE_LOG_LEVEL wiring #80

Description

@Wahbeh-Mohammad

Part of the audit remediation umbrella #67. Milestone 4. Severity: HIGH (first two items), MEDIUM (rest). Requirements: OBS-20, OBS-22, OBS-23, OBS-29, OBS-35, CTX-11, CTX-14, CTX-15, CTX-16.

Current SDK behavior

  • AsyncLocalStorage.enterWith leaks outward. pushDiagnosticFields
    (observability/diagnostic-context.ts:91) and createAsyncScopedStore.enter (:118,125)
    call enterWith. The restore closure runs after awaits inside send(), in a different
    async resource, and never reaches the caller. After runtime.send() returns,
    getActiveSpan() is the ended span and the process-wide diagnostic context still holds that
    request's trace.id / span.id. An unrelated application log through any core Logger then
    carries them. The existing tests assert inside the same continuation that called close().
  • The operation span is suppressed after the first send. startOperationSpan
    (pipeline/runtime.ts:50) bails when getActiveSpan().isRecording. The leaked span still
    reports isRecording === true, so only the first operation per process gets the
    http.client.operation span.
  • The operation span cannot be enabled at all from the public API. PipelineBuilder.build()
    (pipeline/builder.ts:241) calls createRuntime(flattened, transport) with no
    contextInit; createRuntime is @internal; standardResilience() has no instrumentation
    option. Every public runtime gets noopInstrumentationBundle. createInstrumentationBundle
    is public with nowhere public to pass its result. The changeset
    2026-09-04-per-operation-span.md shows createRuntime(...) in its example.
  • A throwing tracerFactory leaks the context-store entry. runtime.ts:170 installs the
    request context before the try at :176; startOperationSpan and activateSpan (:174-175)
    sit in the gap. store.size grows by one per failed send.
  • span.end() can run twice. If end() on the success path throws, the catch calls
    recordException and end() again (runtime.ts:193,198-199).
  • Body-drain failures are silent. The catches at observability/logging-step.ts:309-312
    and :333-335 return an empty capture and emit nothing. A fileBody() over a deleted file
    logs "http.request.body.preview": "" with no http.instrumentation.* event.
  • DEXPACE_LOG_LEVEL is dead by default. resolveGranularity (logging-step.ts:84)
    reads getGlobalConfiguration(), whose default is an empty configuration with an env seam of
    () => undefined. Nothing in packages/ or docs/sdk-documentation/ installs
    defaultConfiguration(). The key name is hardcoded (:85); OBS-35 says the SDK MUST NOT bake
    in a default key name.
  • Smaller: noopInstrumentationBundle.activeSpan is undefined instead of NOOP_SPAN
    (context/instrumentation.ts:65). operationName is never passed to promoteToRequest
    (runtime.ts:169), so ctx.context.operationName is always undefined (CTX-16).
    redactUrl(string) normalizes port, path and host case (redaction.ts:83; OBS-14).
    logging-step.ts:427's span is ended inside executePipeline, not in the finally.

Expected behavior

When send() resolves or rejects, the active span and the diagnostic fields are what they
were before the call. Every operation gets its own http.client.operation span. A consumer
can pass an InstrumentationBundle through PipelineBuilder and standardResilience().
The context store has the same size after a failed send as before it. A body-drain failure
emits a best-effort http.instrumentation.* diagnostic. DEXPACE_LOG_LEVEL (or a
caller-chosen key) is read from the environment without extra wiring, or the required wiring
is documented.

Notes and leads

  • Replace enterWith plus restore closure with storage.run(value, fn) around the awaited
    body of send(). run restores on exit and does not leak outward. Where a handle-based API
    must stay, document that close() cannot restore the caller's context. Test: log after
    await send() and assert no trace.id; assert the second send gets its own operation span.
  • Public path: add an instrumentation (or contextInit) option to PipelineBuilder's
    constructor and to standardResilience()'s options, threaded to createRuntime. Pass
    operationName through the same option (CTX-16). Fix the changeset example.
  • Move install inside the try, or open the try before the span start. Single exit for
    span.end() guarded by an ended flag.
  • Body-drain: safeEmit an http.instrumentation.bodyCaptureFailed event with cause().
  • Log level: either default the global configuration to defaultConfiguration() (behavior
    change, changeset) or document setGlobalConfiguration(defaultConfiguration()) in
    pipelines.md. Add configKey to LoggingStepSettings.
  • Land subtask 1 first: tracerFactory's TSDoc and the deviations.md OBS-29 row change again
    here.
  • Minor changeset for @dexpace/core (new builder option). api:local.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions