Skip to content

Commit d42f30b

Browse files
committed
feat: stamp W3C trace context on MongoDB metadata writes
Add a traceContext field to ObjectMD carrying the currently-active OTEL trace context (W3C traceparent/tracestate). Inject it automatically at the three metadata write chokepoints where originOp is set today: internalPutObject, repair, and internalDeleteObject. The value ends up in the MongoDB oplog; downstream consumers (backbeat, sorbet) can extract it to continue the trace across the async boundary, closing the loop on end-to-end tracing for flows that cross the oplog. When OTEL is not active on the caller (no SDK, or request outside a traced context), captureCurrentTraceContext returns undefined, setTraceContext no-ops, and the field stays absent — zero cost. Only adds @opentelemetry/api as a runtime dependency (the API-only surface package, becomes a no-op when no SDK is registered). Issue: ARSN-572
1 parent 600fb06 commit d42f30b

6 files changed

Lines changed: 94 additions & 0 deletions

File tree

lib/models/ObjectMD.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export type ObjectMDData = {
8989
replicationInfo: ReplicationInfo;
9090
dataStoreName: string;
9191
originOp: string;
92+
traceContext?: {
93+
traceparent?: string;
94+
tracestate?: string;
95+
};
9296
microVersionId?: string;
9397
// Deletion flag
9498
// Used for keeping object metadata in the oplog event
@@ -1442,6 +1446,28 @@ export default class ObjectMD {
14421446
return this._data.originOp;
14431447
}
14441448

1449+
/**
1450+
* Attach a W3C trace context to the metadata so it ends up in
1451+
* the MongoDB oplog and downstream consumers can continue the trace.
1452+
* Pass undefined (or a value with no traceparent) to leave the field absent.
1453+
* @param tc - W3C trace context carrier
1454+
* @return itself
1455+
*/
1456+
setTraceContext(tc?: { traceparent?: string; tracestate?: string }) {
1457+
if (tc && tc.traceparent) {
1458+
this._data.traceContext = tc;
1459+
}
1460+
return this;
1461+
}
1462+
1463+
/**
1464+
* Returns the trace context attached to the metadata, if any.
1465+
* @return W3C trace context carrier or undefined
1466+
*/
1467+
getTraceContext() {
1468+
return this._data.traceContext;
1469+
}
1470+
14451471
/**
14461472
* Returns metadata object
14471473
*
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { context, propagation, trace } from '@opentelemetry/api';
2+
3+
/**
4+
* Capture the currently-active OTEL trace context as a plain object
5+
* suitable for storing alongside metadata (ends up in the MongoDB oplog).
6+
* Returns undefined when no trace is active (e.g., OTEL not enabled,
7+
* or called outside a traced request).
8+
*/
9+
export function captureCurrentTraceContext():
10+
{ traceparent: string; tracestate?: string } | undefined {
11+
const ctx = context.active();
12+
const span = trace.getSpan(ctx);
13+
if (!span) return undefined;
14+
15+
const carrier: Record<string, string> = {};
16+
propagation.inject(ctx, carrier, {
17+
set: (c, k, v) => { c[k] = v; },
18+
});
19+
if (!carrier.traceparent) return undefined;
20+
21+
const out: { traceparent: string; tracestate?: string } = {
22+
traceparent: carrier.traceparent,
23+
};
24+
if (carrier.tracestate) out.tracestate = carrier.tracestate;
25+
return out;
26+
}

lib/storage/metadata/mongoclient/MongoClientInterface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ErrorLike, reshapeExceptionError } from '../../../errorUtils';
1818
import errors, { ArsenalError, errorInstances } from '../../../errors';
1919
import BucketInfo, { BucketMetadata, Capabilities } from '../../../models/BucketInfo';
2020
import ObjectMD, { ObjectMDData } from '../../../models/ObjectMD';
21+
import { captureCurrentTraceContext } from '../captureTraceContext';
2122
import * as jsutil from '../../../jsutil';
2223
import { ArsenalCallback, NestedOmit } from '../../../types';
2324

@@ -1284,6 +1285,7 @@ class MongoClientInterface {
12841285
const obj = doc.value;
12851286
const objMetadata = new ObjectMD(obj);
12861287
objMetadata.setOriginOp(params.originOp);
1288+
objMetadata.setTraceContext(captureCurrentTraceContext());
12871289
objMetadata.setDeleted(true);
12881290
return next(null, objMetadata.getValue());
12891291
}).catch(err => {
@@ -1635,6 +1637,10 @@ class MongoClientInterface {
16351637
const masterKey = formatMasterKey(objName, vFormat);
16361638
MongoUtils.serialize(objVal);
16371639
objVal.originOp = 's3:ObjectRemoved:Delete';
1640+
const tc = captureCurrentTraceContext();
1641+
if (tc) {
1642+
objVal.traceContext = tc;
1643+
}
16381644
c.findOneAndReplace({
16391645
'_id': masterKey,
16401646
'value.isPHD': true,
@@ -2073,6 +2079,7 @@ class MongoClientInterface {
20732079
const obj = doc.value;
20742080
const objMetadata = new ObjectMD(obj.value);
20752081
objMetadata.setOriginOp(originOp);
2082+
objMetadata.setTraceContext(captureCurrentTraceContext());
20762083
objMetadata.setDeleted(true);
20772084
return next(null, objMetadata.getValue());
20782085
}).catch(err => {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@azure/identity": "^4.13.0",
2525
"@azure/storage-blob": "^12.31.0",
2626
"@js-sdsl/ordered-set": "^4.4.2",
27+
"@opentelemetry/api": "^1.9.0",
2728
"@scality/hdclient": "^1.3.1",
2829
"@smithy/node-http-handler": "^4.3.0",
2930
"@smithy/protocol-http": "^5.3.5",

tests/unit/models/ObjectMD.spec.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,35 @@ describe('ObjectMD class setters/getters', () => {
328328
assert.deepStrictEqual(md.getOriginOp(), 'Copy');
329329
});
330330

331+
it('ObjectMD::set/getTraceContext with valid traceparent', () => {
332+
const tc = {
333+
traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
334+
tracestate: 'rojo=00f067aa0ba902b7',
335+
};
336+
md.setTraceContext(tc);
337+
assert.deepStrictEqual(md.getTraceContext(), tc);
338+
});
339+
340+
it('ObjectMD::setTraceContext with undefined is a no-op', () => {
341+
md.setTraceContext(undefined);
342+
assert.strictEqual(md.getTraceContext(), undefined);
343+
});
344+
345+
it('ObjectMD::setTraceContext without traceparent is a no-op', () => {
346+
md.setTraceContext({ tracestate: 'rojo=00f067aa0ba902b7' });
347+
assert.strictEqual(md.getTraceContext(), undefined);
348+
});
349+
350+
it('ObjectMD::getValue serializes traceContext only when set', () => {
351+
assert.strictEqual(md.getValue().traceContext, undefined);
352+
md.setTraceContext({
353+
traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
354+
});
355+
assert.deepStrictEqual(md.getValue().traceContext, {
356+
traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01',
357+
});
358+
});
359+
331360
it('ObjectMD::set/getAmzRestore', () => {
332361
md.setAmzRestore({
333362
'ongoing-request': false,

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2466,6 +2466,11 @@
24662466
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
24672467
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
24682468

2469+
"@opentelemetry/api@^1.9.0":
2470+
version "1.9.1"
2471+
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.1.tgz#c1b0346de336ba55af2d5a7970882037baedec05"
2472+
integrity sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==
2473+
24692474
"@pkgjs/parseargs@^0.11.0":
24702475
version "0.11.0"
24712476
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"

0 commit comments

Comments
 (0)