Skip to content

feat(CLDSRV-884): Add OpenTelemetry tracing instrumentation#6140

Open
delthas wants to merge 9 commits into
development/9.3from
improvement/CLDSRV-884/otel-instrumentation
Open

feat(CLDSRV-884): Add OpenTelemetry tracing instrumentation#6140
delthas wants to merge 9 commits into
development/9.3from
improvement/CLDSRV-884/otel-instrumentation

Conversation

@delthas
Copy link
Copy Markdown
Contributor

@delthas delthas commented Apr 2, 2026

Summary

Add OpenTelemetry tracing instrumentation to cloudserver, gated behind ENABLE_OTEL=true. When the flag is unset, no @opentelemetry/* package is loaded — zero overhead off the OTEL path.

The four atomic commits:

  1. feat: add OpenTelemetry tracing with trust boundaries and probe filtering

    • NodeSDK with OTLP/HTTP trace exporter, default 1% sampling
    • ParentBasedSampler({ root: TraceIdRatioBasedSampler(ratio) }) so inbound traceparent sampled=1 from NGINX/Beyla is honored end-to-end (was being re-sampled by a plain ratio sampler before)
    • Three explicit instrumentation packages, declared as direct deps — no auto-instrumentations-node bundle (which pulled ~36 unused instrumentations: pg, mysql, kafkajs, cassandra, oracle, etc.):
      • instrumentation-http with ignoreIncomingRequestHook (drops k8s probes + OPTIONS) and requestHook (strips traceparent/tracestate on outbound requests to hosts not in the trusted allowlist; client span is preserved and tagged scality.trace.suppressed=true)
      • instrumentation-mongodb with low-cardinality settings (no collection names in span names, no captured query payloads)
      • instrumentation-ioredis with requireParentSpan: true (no orphan spans from background stats/rate-limit jobs)
    • buildTrustedHosts(config) derives the allowlist from cloudserver's own Config (vaultd, dataClient, metadataClient, pfsClient, cdmi, bucketd, KMIP, KMS, scuba, utapi, mongodb replica set, backbeat, managementAgent + hdclient/sproxyd bootstrap entries from locationConstraints for direct Scality storage connectors), plus PUSH_ENDPOINT/MANAGEMENT_ENDPOINT env vars and loopback aliases. Unit test asserts every config host shape is included so the derivation stays honest.
    • Probe/scrape filter (lib/tracing/healthPaths.js): /live, /ready, /_/healthcheck, /_/healthcheck/deep, /metrics produce no spans
    • diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.WARN) so SDK errors (export failures, malformed traceparent, etc.) surface instead of disappearing
    • Span limits: attributeValueLengthLimit: 4096, attributeCountLimit: 128, eventCountLimit: 128, linkCountLimit: 128 to bound memory under pathological cases
    • logRecordProcessors: [], metricReaders: [] — explicit traces-only; without these, NodeSDK silently spins up OTLP log + metric exporters
  2. feat: flush OTEL on shutdown

    • shutdownOtel() wired into S3Server.cleanUp()'s shutdown chain (between server.close() and process.exit(0)), capped at 5 s via Promise.race so an unreachable collector can't block past Kubernetes' default 30 s terminationGracePeriodSeconds
    • caughtExceptionShutdown() also attempts a best-effort flush before process.exit(1) on the non-cluster path
    • .finally() guard so any unexpected promise rejection in the shutdown chain still exits the process
    • Inbound trace-context extraction is intentionally NOT done manually here: @opentelemetry/instrumentation-http already extracts traceparent and creates the server span as a child of the remote parent. A manual extract on top would replace the active context with the (non-recording) remote parent and demote downstream api.<handler> spans to siblings instead of children of the HTTP server span. Removed from the original draft.
  3. feat: instrument all S3 API handlers with OTEL spans

    • lib/instrumentation/simple.js exports a single helper instrumentApiMethod(handler, methodName) that wraps an S3 handler in an api.<methodName> span owning the entire handler execution (auth, body parsing, metadata I/O, data path, finalizers). Auto-instrumentation spans (HTTP, MongoDB, ioredis) nest beneath. ~70 distinct span names total — well within trace-backend limits.
    • lib/api/api.js applies the wrapper via Object.entries(api) loop with a NON_HANDLER_KEYS skip set (callApiMethod, checkAuthResults, handleAuthorizationResults). New S3 handlers added to the literal are auto-instrumented; no per-handler boilerplate.
    • The wrapper handles callback / Promise / sync return shapes, has a single-end-once guard, sets cloudserver.error_code on the error path
  4. chore: bump arsenal to ARSN-572 PoC branch for e2e trace context testing

    • Temporary: pins arsenal at the ARSN-572 PoC branch (scality/Arsenal#2611), which adds traceContext plumbing to MongoDB metadata writes (object oplog entries carry the trace context that caused them, so async consumers — Backbeat, Sorbet, lifecycle — can continue the trace across the oplog/queue boundary)
    • To be reverted to a real 8.3.x release tag once ARSN-572 ships

Origin

Extracted and cleaned up from William Lardier's user/test/wlardier/servicemesh-2 branch (based on development/9.0, July–Sep 2025), which mixed OTEL with performance optimizations and debug artifacts. This PR contains only the OTEL instrumentation, rebased onto development/9.3. Dropped:

  • lib/otelContextPropagation.js (dead code; never imported)
  • MOCK_DOAUTH / lib/api/apiUtils/mock/backendMocks.js (production-dangerous auth-result caching, unrelated to tracing)
  • ~15 console.log() debug artifacts, commented-out span code
  • All performance optimizations (manual GC, monitorLatency, releaseRequestContexts, arsenal perf pin) — out of scope; will land separately if needed

Tests

29 unit tests (22 in tests/unit/lib/otel.spec.js, 7 in tests/unit/lib/instrumentationSimple.spec.js) covering extractHost, buildTrustedHosts (full config snapshot + locationConstraints connector enumeration), makeRequestHook (outbound trust check, inbound IncomingMessage skip), isHealthPath, and the full lifecycle of instrumentApiMethod (callback success/error, double-end guard, async resolve/reject, sync throw, OTEL-disabled bypass).

Context

  • Parent investigation: OS-1072
  • Companion arsenal change: scality/Arsenal#2611 (ARSN-572) — to be merged before this PR can lose its temporary branch pin
  • Scality ADR mandates OpenTelemetry across all products. The storage layer (hdcontroller 1.12+ / hyperiod) is already OTEL-instrumented; this PR makes cloudserver complete the chain via @opentelemetry/instrumentation-http's automatic outbound traceparent propagation

Issue: CLDSRV-884

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented Apr 2, 2026

Hello delthas,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented Apr 2, 2026

Request integration branches

Waiting for integration branch creation to be requested by the user.

To request integration branches, please comment on this pull request with the following command:

/create_integration_branches

Alternatively, the /approve and /create_pull_requests commands will automatically
create the integration branches.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 80.79470% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.51%. Comparing base (ab23592) to head (fb2ed26).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
lib/tracing/index.js 29.72% 26 Missing ⚠️
lib/server.js 50.00% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

Files with missing lines Coverage Δ
lib/api/api.js 91.48% <100.00%> (+0.15%) ⬆️
lib/instrumentation/simple.js 100.00% <100.00%> (ø)
lib/tracing/healthPaths.js 100.00% <100.00%> (ø)
lib/tracing/trustedHosts.js 100.00% <100.00%> (ø)
lib/server.js 79.42% <50.00%> (-0.19%) ⬇️
lib/tracing/index.js 29.72% <29.72%> (ø)

... and 2 files with indirect coverage changes

@@                 Coverage Diff                 @@
##           development/9.3    #6140      +/-   ##
===================================================
- Coverage            84.60%   84.51%   -0.10%     
===================================================
  Files                  206      210       +4     
  Lines                13322    13470     +148     
===================================================
+ Hits                 11271    11384     +113     
- Misses                2051     2086      +35     
Flag Coverage Δ
file-ft-tests 67.92% <17.88%> (-0.56%) ⬇️
kmip-ft-tests 28.26% <17.88%> (-0.12%) ⬇️
mongo-v0-ft-tests 69.10% <17.88%> (-0.65%) ⬇️
mongo-v1-ft-tests 69.11% <17.88%> (-0.68%) ⬇️
multiple-backend 36.52% <17.88%> (-0.22%) ⬇️
sur-tests 35.73% <17.88%> (-0.24%) ⬇️
sur-tests-inflights 37.57% <17.88%> (-0.25%) ⬇️
unit 70.56% <76.15%> (+0.07%) ⬆️
utapi-v2-tests 34.46% <17.88%> (-0.19%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread lib/otel.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
@delthas delthas force-pushed the improvement/CLDSRV-884/otel-instrumentation branch from 9c93a1d to 97e63fc Compare April 2, 2026 16:32
Comment thread lib/instrumentation/simple.js Outdated
Comment thread package.json Outdated
Comment thread lib/otel.js Outdated
Comment thread lib/otel.js Outdated
@delthas delthas force-pushed the improvement/CLDSRV-884/otel-instrumentation branch from 97e63fc to f978280 Compare April 2, 2026 16:35
Comment thread lib/instrumentation/simple.js Outdated
Comment thread lib/otel.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
@delthas delthas force-pushed the improvement/CLDSRV-884/otel-instrumentation branch from f978280 to 06eea4e Compare April 2, 2026 16:49
Comment thread lib/instrumentation/simple.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
Comment thread package.json Outdated
@delthas delthas force-pushed the improvement/CLDSRV-884/otel-instrumentation branch from 06eea4e to 75c3afb Compare April 2, 2026 17:02
Comment thread lib/instrumentation/simple.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
Comment thread package.json Outdated
Comment thread lib/otel.js Outdated
Comment thread lib/instrumentation/simple.js Outdated
@delthas delthas force-pushed the improvement/CLDSRV-884/otel-instrumentation branch from 75c3afb to 207b785 Compare April 13, 2026 13:05
Comment thread lib/instrumentation/simple.js Outdated
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
@scality scality deleted a comment from claude Bot May 13, 2026
delthas added 9 commits May 13, 2026 16:14
…ring

Behind ENABLE_OTEL=true:
- NodeSDK with OTLP/HTTP exporter and ParentBased + ratio sampler
  so we honor upstream NGINX/Beyla sampling decisions instead of
  re-sampling them away.
- HTTP instrumentation with two hooks:
  - ignoreIncomingRequestHook drops spans on probe / scrape paths
    (/live, /ready, /_/healthcheck, /_/healthcheck/deep, /metrics)
    and OPTIONS preflight.
  - requestHook strips traceparent/tracestate on outbound requests
    to hosts not in TRUSTED_HOSTS, so trace IDs do not leak to
    external destinations. The client span is preserved (we still
    observe the call) and tagged scality.trace.suppressed=true.
- buildTrustedHosts derives the allowlist from cloudserver Config
  (vaultd, dataClient, metadataClient, bucketd, KMIP, KMS, scuba,
  utapi, mongodb, hdclient/sproxyd connectors from locationConfig,
  PUSH/MANAGEMENT_ENDPOINT env vars, plus loopback). A unit test
  asserts every Config host shape is covered so the derivation
  stays honest as new backends land.
- shutdownOtel() helper for the server's cleanUp() to await the
  exporter flush before process.exit so in-flight traces are not
  lost on SIGTERM.
- mongodb auto-instrumentation tuned for low cardinality; ioredis
  enabled with requireParentSpan; fs / redis (v2/v3/v4) / aws-sdk
  disabled.

When ENABLE_OTEL is unset the SDK and @opentelemetry/* packages are
not loaded at all - zero overhead off the OTEL path.

Issue: CLDSRV-884
Wire shutdownOtel() into the server's cleanUp() chain between closing
HTTP servers and process.exit(0). Without this, async sdk.shutdown()
fired by signal handlers can race the exit and lose buffered spans for
whatever was still in flight at SIGTERM time.

Inbound traceparent extraction is intentionally NOT done here:
@opentelemetry/instrumentation-http already calls propagation.extract
on every incoming request, creates a server span as a child of the
remote parent, and sets that server span as the active context. A
manual extract on top of that would replace the active context with
the (non-recording) remote parent and demote downstream api spans
to siblings - rather than children - of the HTTP server span,
breaking the trace hierarchy in exactly the distributed-tracing
scenarios the manual block was meant to support.

Issue: CLDSRV-884
Add lib/instrumentation/simple.js exporting instrumentApiMethod, a
wrapper that surrounds an S3 handler invocation with an OTEL span
named api.<methodName>. The span owns the entire handler execution
(auth, body parsing, metadata I/O, data path, finalizers) and
becomes the parent for any auto-instrumentation spans (HTTP,
MongoDB, ioredis) that fire underneath.

Span name is the handler name verbatim - objectGetACL stays
distinct from objectGet, objectPutTagging stays distinct from
objectPut. ~70 handlers means ~70 distinct span names total, well
within trace backend limits, and operators can tell variants apart
without reading attributes.

api.js applies the wrapper to every function-valued key in the api
object except for the dispatcher (callApiMethod) and pure helpers
(checkAuthResults, handleAuthorizationResults). New handlers added
to the literal are automatically instrumented - no per-handler
boilerplate to remember.

The wrapper handles callback / promise / sync return paths, sets
SpanStatusCode.OK or ERROR + recordException as appropriate, and
sets cloudserver.error_code on the error path. When ENABLE_OTEL is
unset @opentelemetry/api is not loaded and the wrapper returns the
original function unchanged.

Issue: CLDSRV-884
Temporarily point the arsenal dep at scality/Arsenal#improvement/ARSN-572/trace-context
so we can validate end-to-end trace context propagation from cloudserver
HTTP spans through to the MongoDB oplog on a test cluster. ARSN-572 adds
traceContext plumbing on metadata writes; cloudserver needs no code change
thanks to OTEL async context hooks.

Yarn resolves the branch ref and pins the resolved commit hash in
yarn.lock so installs are reproducible.

Revert to a clean #8.x release tag once ARSN-572 ships.

Issue: CLDSRV-884
@delthas delthas force-pushed the improvement/CLDSRV-884/otel-instrumentation branch from 97b88f5 to fb2ed26 Compare May 13, 2026 14:16
Comment thread package.json
"@opentelemetry/sdk-trace-base": "^2.7.0",
"@smithy/node-http-handler": "^3.0.0",
"arsenal": "git+https://github.com/scality/Arsenal#8.4.1",
"arsenal": "git+https://github.com/scality/Arsenal#improvement/ARSN-572/trace-context",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Arsenal is pinned to a feature branch (improvement/ARSN-572/trace-context) instead of a release tag. The PR description notes this is temporary, but merging this means main carries an unpinned git dep — if the branch is force-pushed or rebased, installs become non-reproducible. Consider gating this PR behind the ARSN-572 arsenal release so it can land with a tag pin (e.g. #8.4.2), or at minimum add a tracking issue to ensure the branch pin is reverted before the next cloudserver release.

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 13, 2026

  • Arsenal is pinned to a feature branch instead of a release tag, making builds non-reproducible if the branch is rebased or force-pushed. Pin to a tag before merging, or add a tracking issue to revert the branch pin before the next release.

    Otherwise the implementation is solid: lazy loading keeps OTEL zero-cost when disabled, the trusted-hosts derivation from Config is thorough and well-tested, the instrumentApiMethod wrapper handles callback/async/hybrid shapes correctly with an end-once guard, and the shutdown path has proper deadline capping and idempotency.

    Review by Claude Code

@scality scality deleted a comment from claude Bot May 13, 2026
Comment on lines +65 to +68
return result.then(
value => { endSpan(); return value; },
err => { endSpan(err); throw err; },
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants