Repository navigation
Instrumentation: enterWith leak, operation span reachable from the public API, context-store eviction, body-drain diagnostics, DEXPACE_LOG_LEVEL wiring #80
Copy link
Copy link
Closed
Labels
area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*area:platformConfig, observability, non-functional, hashing: CFG-* OBS-* NFR-* SHA-*Config, observability, non-functional, hashing: CFG-* OBS-* NFR-* SHA-*type:bugDefect in shipped behaviorDefect in shipped behavior
Milestone
Description
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.enterWithleaks outward.pushDiagnosticFields
(observability/diagnostic-context.ts:91) andcreateAsyncScopedStore.enter(:118,125)
callenterWith. The restore closure runs afterawaits insidesend(), in a different
async resource, and never reaches the caller. Afterruntime.send()returns,
getActiveSpan()is the ended span and the process-wide diagnostic context still holds that
request'strace.id/span.id. An unrelated application log through any coreLoggerthen
carries them. The existing tests assert inside the same continuation that calledclose().- The operation span is suppressed after the first send.
startOperationSpan
(pipeline/runtime.ts:50) bails whengetActiveSpan().isRecording. The leaked span still
reportsisRecording === true, so only the first operation per process gets the
http.client.operationspan. - The operation span cannot be enabled at all from the public API.
PipelineBuilder.build()
(pipeline/builder.ts:241) callscreateRuntime(flattened, transport)with no
contextInit;createRuntimeis@internal;standardResilience()has no instrumentation
option. Every public runtime getsnoopInstrumentationBundle.createInstrumentationBundle
is public with nowhere public to pass its result. The changeset
2026-09-04-per-operation-span.mdshowscreateRuntime(...)in its example. - A throwing
tracerFactoryleaks the context-store entry.runtime.ts:170installs the
request context before thetryat:176;startOperationSpanandactivateSpan(:174-175)
sit in the gap.store.sizegrows by one per failed send. span.end()can run twice. Ifend()on the success path throws, thecatchcalls
recordExceptionandend()again (runtime.ts:193,198-199).- Body-drain failures are silent. The catches at
observability/logging-step.ts:309-312
and:333-335return an empty capture and emit nothing. AfileBody()over a deleted file
logs"http.request.body.preview": ""with nohttp.instrumentation.*event. DEXPACE_LOG_LEVELis dead by default.resolveGranularity(logging-step.ts:84)
readsgetGlobalConfiguration(), whose default is an empty configuration with an env seam of
() => undefined. Nothing inpackages/ordocs/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.activeSpanisundefinedinstead ofNOOP_SPAN
(context/instrumentation.ts:65).operationNameis never passed topromoteToRequest
(runtime.ts:169), soctx.context.operationNameis alwaysundefined(CTX-16).
redactUrl(string)normalizes port, path and host case (redaction.ts:83; OBS-14).
logging-step.ts:427's span is ended insideexecutePipeline, not in thefinally.
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
enterWithplus restore closure withstorage.run(value, fn)around the awaited
body ofsend().runrestores on exit and does not leak outward. Where a handle-based API
must stay, document thatclose()cannot restore the caller's context. Test: log after
await send()and assert notrace.id; assert the second send gets its own operation span. - Public path: add an
instrumentation(orcontextInit) option toPipelineBuilder's
constructor and tostandardResilience()'s options, threaded tocreateRuntime. Pass
operationNamethrough the same option (CTX-16). Fix the changeset example. - Move
installinside thetry, or open thetrybefore the span start. Single exit for
span.end()guarded by anendedflag. - Body-drain:
safeEmitanhttp.instrumentation.bodyCaptureFailedevent withcause(). - Log level: either default the global configuration to
defaultConfiguration()(behavior
change, changeset) or documentsetGlobalConfiguration(defaultConfiguration())in
pipelines.md. AddconfigKeytoLoggingStepSettings. - Land subtask 1 first:
tracerFactory's TSDoc and thedeviations.mdOBS-29 row change again
here. - Minor changeset for
@dexpace/core(new builder option).api:local.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:coreCore HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*Core HTTP, IO, body, context, encoding: HTTP-* IO-* BODY-* CTX-* UTF-*area:platformConfig, observability, non-functional, hashing: CFG-* OBS-* NFR-* SHA-*Config, observability, non-functional, hashing: CFG-* OBS-* NFR-* SHA-*type:bugDefect in shipped behaviorDefect in shipped behavior
Activity
Wahbeh-Mohammad commentedon Sep 5, 2026
Addressed in PR #95, merged into
audit/remediation-67; reachesmvpthrough PR #98. Decision record:docs/audit-67-decisions.md.