Skip to content

Commit 0348202

Browse files
feat(LLMO-1023): add trace ID to API response headers
Enhance logWrapper to stamp x-trace-id header on outgoing responses, enabling end-to-end distributed tracing from request through to the API response. Uses context.traceId (propagated) with X-Ray fallback. Made-with: Cursor
1 parent 29c7744 commit 0348202

File tree

2 files changed

+89
-3
lines changed

2 files changed

+89
-3
lines changed

packages/spacecat-shared-utils/src/log-wrapper.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ export function logWrapper(fn) {
7777
context.contextualLog = context.log;
7878
}
7979

80-
return fn(message, context);
80+
const response = await fn(message, context);
81+
82+
// Add traceId to response headers for end-to-end distributed tracing
83+
const traceId = context.traceId || getTraceId();
84+
if (traceId && response?.headers?.set) {
85+
response.headers.set('x-trace-id', traceId);
86+
}
87+
88+
return response;
8189
};
8290
}

packages/spacecat-shared-utils/test/log-wrapper.test.js

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const logLevels = [
4040
'fatal',
4141
];
4242

43-
const mockFnFromSqs = sinon.spy();
43+
let mockFnFromSqs;
4444
let mockContext;
4545

4646
describe('logWrapper tests', () => {
@@ -56,7 +56,8 @@ describe('logWrapper tests', () => {
5656

5757
beforeEach(() => {
5858
sinon.resetHistory();
59-
getTraceIdStub.returns(null); // Default to no trace ID
59+
mockFnFromSqs = sinon.stub().resolves(undefined);
60+
getTraceIdStub.returns(null);
6061
mockContext = {
6162
// Simulate an SQS event
6263
invocation: {
@@ -300,4 +301,81 @@ describe('logWrapper tests', () => {
300301
expect(callArgs[0]).to.equal(errorObject);
301302
});
302303
});
304+
305+
// Tests for x-trace-id response header
306+
describe('response header x-trace-id', () => {
307+
it('should add x-trace-id header to the response when traceId is available from X-Ray', async () => {
308+
getTraceIdStub.returns('1-5e8e8e8e-5e8e8e8e5e8e8e8e5e8e8e8e');
309+
const mockHeaders = { set: sinon.spy() };
310+
const mockResponse = { headers: mockHeaders };
311+
mockFnFromSqs.resolves(mockResponse);
312+
313+
const wrappedFn = logWrapper(mockFnFromSqs);
314+
const result = await wrappedFn(message, mockContext);
315+
316+
expect(result).to.equal(mockResponse);
317+
expect(mockHeaders.set.calledOnce).to.be.true;
318+
expect(mockHeaders.set.calledWith('x-trace-id', '1-5e8e8e8e-5e8e8e8e5e8e8e8e5e8e8e8e')).to.be.true;
319+
});
320+
321+
it('should add x-trace-id header from context.traceId when available', async () => {
322+
mockContext.traceId = '1-context-trace-id';
323+
getTraceIdStub.returns('1-xray-trace-id');
324+
const mockHeaders = { set: sinon.spy() };
325+
const mockResponse = { headers: mockHeaders };
326+
mockFnFromSqs.resolves(mockResponse);
327+
328+
const wrappedFn = logWrapper(mockFnFromSqs);
329+
const result = await wrappedFn(message, mockContext);
330+
331+
expect(result).to.equal(mockResponse);
332+
expect(mockHeaders.set.calledOnce).to.be.true;
333+
expect(mockHeaders.set.calledWith('x-trace-id', '1-context-trace-id')).to.be.true;
334+
});
335+
336+
it('should not add x-trace-id header when no traceId is available', async () => {
337+
getTraceIdStub.returns(null);
338+
const mockHeaders = { set: sinon.spy() };
339+
const mockResponse = { headers: mockHeaders };
340+
mockFnFromSqs.resolves(mockResponse);
341+
342+
const wrappedFn = logWrapper(mockFnFromSqs);
343+
const result = await wrappedFn({}, mockContext);
344+
345+
expect(result).to.equal(mockResponse);
346+
expect(mockHeaders.set.called).to.be.false;
347+
});
348+
349+
it('should not fail when response has no headers object', async () => {
350+
getTraceIdStub.returns('1-abc-def');
351+
const mockResponse = {};
352+
mockFnFromSqs.resolves(mockResponse);
353+
354+
const wrappedFn = logWrapper(mockFnFromSqs);
355+
const result = await wrappedFn(message, mockContext);
356+
357+
expect(result).to.equal(mockResponse);
358+
});
359+
360+
it('should not fail when response is null or undefined', async () => {
361+
getTraceIdStub.returns('1-abc-def');
362+
mockFnFromSqs.resolves(null);
363+
364+
const wrappedFn = logWrapper(mockFnFromSqs);
365+
const result = await wrappedFn(message, mockContext);
366+
367+
expect(result).to.be.null;
368+
});
369+
370+
it('should not fail when response headers has no set method', async () => {
371+
getTraceIdStub.returns('1-abc-def');
372+
const mockResponse = { headers: {} };
373+
mockFnFromSqs.resolves(mockResponse);
374+
375+
const wrappedFn = logWrapper(mockFnFromSqs);
376+
const result = await wrappedFn(message, mockContext);
377+
378+
expect(result).to.equal(mockResponse);
379+
});
380+
});
303381
});

0 commit comments

Comments
 (0)