Skip to content

Commit 012ebea

Browse files
committed
Add setTag to emitter interface to set arbitrary key/value
Here, bring in a `setTag` function that lets X-ray users set an arbitrary key/value on a request span. These pairs are JSON encoded at the end of a request and emitted into the span. This is one that we've already supported for a while in Go and on the server, so we're just bringing equivalent functionality to this emitter so we can get access to it from the Stainless stack.
1 parent a4fef64 commit 012ebea

6 files changed

Lines changed: 125 additions & 1 deletion

File tree

src/core/attributes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
ATTR_URL_PATH,
1111
ATTR_USER_ID,
1212
} from '@opentelemetry/semantic-conventions/incubating';
13-
import type { CapturedBody } from './types';
13+
import type { AttributeValue, CapturedBody } from './types';
1414
import {
1515
AttributeKeyRequestBody,
1616
AttributeKeyRequestBodyEncoding,
@@ -19,6 +19,7 @@ import {
1919
AttributeKeyResponseBody,
2020
AttributeKeyResponseBodyEncoding,
2121
AttributeKeyResponseBodyTruncated,
22+
AttributeKeyTags,
2223
AttributeKeyTenantID,
2324
} from './attrkey';
2425

@@ -291,3 +292,7 @@ export function setTenantIdAttribute(span: Span, tenantId: string): void {
291292
export function setRequestIdAttribute(span: Span, requestId: string): void {
292293
span.setAttribute(AttributeKeyRequestID, requestId);
293294
}
295+
296+
export function setTagsAttribute(span: Span, tags: Record<string, AttributeValue>): void {
297+
span.setAttribute(AttributeKeyTags, JSON.stringify(tags));
298+
}

src/core/attrkey.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const AttributeKeyResponseBody = 'http.response.body';
88
export const AttributeKeyResponseBodyEncoding = 'http.response.body.encoding';
99
export const AttributeKeyResponseBodyTruncated = 'http.response.body.truncated';
1010
export const AttributeKeySpanDrop = 'stainlessxray.internal.drop';
11+
export const AttributeKeyTags = 'stainlessxray.internal.tags';
1112
export const AttributeKeyTenantID = 'stainlessxray.tenant.id';
1213
export const AttributeKeyXrayCaptureErrorCode = 'xray.capture_error_code';
1314
export const AttributeKeyXrayErrorThrown = 'xray.error_thrown';

src/core/emitter.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,3 +331,99 @@ test('setUserId logs when span attributes fail', () => {
331331
assert.equal(errors[0]?.msg, 'xray: setUserId failed');
332332
assert.equal(errors[0]?.fields?.['error'], 'setAttribute failed');
333333
});
334+
335+
test('setTag records tags in log and span', async () => {
336+
const spans: ReadableSpan[] = [];
337+
const xray = createEmitter(
338+
{
339+
serviceName: 'test',
340+
endpointUrl: 'https://collector',
341+
exporter: { spanProcessor: 'simple' },
342+
},
343+
createRecordingExporter(spans),
344+
);
345+
346+
const ctx = xray.startRequest({
347+
method: 'GET',
348+
url: 'https://example.test/tags',
349+
headers: {},
350+
startTimeMs: 0,
351+
});
352+
ctx.setTag('environment', 'staging');
353+
ctx.setTag('region', 'us-east-1');
354+
ctx.setTag('retries', 3);
355+
ctx.setTag('verbose', true);
356+
357+
const log = xray.endRequest(ctx, {
358+
statusCode: 200,
359+
headers: {},
360+
endTimeMs: 5,
361+
});
362+
363+
await xray.flush();
364+
assert.deepEqual(log.tags, { environment: 'staging', region: 'us-east-1', retries: 3, verbose: true });
365+
const tagsAttr = spans[0]?.attributes['stainlessxray.internal.tags'];
366+
assert.equal(typeof tagsAttr, 'string');
367+
assert.deepEqual(JSON.parse(tagsAttr as string), {
368+
environment: 'staging',
369+
region: 'us-east-1',
370+
retries: 3,
371+
verbose: true,
372+
});
373+
});
374+
375+
test('setTag overwrites previous value for same key', () => {
376+
const xray = createEmitter(
377+
{
378+
serviceName: 'test',
379+
endpointUrl: 'https://collector',
380+
},
381+
createNoopExporter(),
382+
);
383+
384+
const ctx = xray.startRequest({
385+
method: 'GET',
386+
url: 'https://example.test/tags-overwrite',
387+
headers: {},
388+
startTimeMs: 0,
389+
});
390+
ctx.setTag('env', 'dev');
391+
ctx.setTag('env', 'prod');
392+
393+
const log = xray.endRequest(ctx, {
394+
statusCode: 200,
395+
headers: {},
396+
endTimeMs: 5,
397+
});
398+
399+
assert.deepEqual(log.tags, { env: 'prod' });
400+
});
401+
402+
test('tags omitted from log and span when empty', async () => {
403+
const spans: ReadableSpan[] = [];
404+
const xray = createEmitter(
405+
{
406+
serviceName: 'test',
407+
endpointUrl: 'https://collector',
408+
exporter: { spanProcessor: 'simple' },
409+
},
410+
createRecordingExporter(spans),
411+
);
412+
413+
const ctx = xray.startRequest({
414+
method: 'GET',
415+
url: 'https://example.test/no-tags',
416+
headers: {},
417+
startTimeMs: 0,
418+
});
419+
420+
const log = xray.endRequest(ctx, {
421+
statusCode: 200,
422+
headers: {},
423+
endTimeMs: 5,
424+
});
425+
426+
await xray.flush();
427+
assert.equal(log.tags, undefined);
428+
assert.equal('stainlessxray.internal.tags' in (spans[0]?.attributes ?? {}), false);
429+
});

src/core/emitter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
setResponseBodySizeAttribute,
1515
setResponseStatusAttribute,
1616
setRouteAttribute,
17+
setTagsAttribute,
1718
setTenantIdAttribute,
1819
setUserIdAttribute,
1920
} from './attributes';
@@ -136,6 +137,13 @@ function startRequest(
136137
}
137138
state.sessionId = id;
138139
},
140+
setTag: (key, value) => {
141+
const state = getContextState(context);
142+
if (!state) {
143+
return;
144+
}
145+
state.tags[key] = value;
146+
},
139147
setAttribute: (key, value) => {
140148
const state = getContextState(context);
141149
if (!state) {
@@ -187,6 +195,7 @@ function startRequest(
187195
context,
188196
attributes: {},
189197
events: [],
198+
tags: {},
190199
};
191200

192201
bindContext(context, state);
@@ -250,6 +259,7 @@ function endRequest(
250259
sessionId: state.sessionId ?? undefined,
251260
error: buildError(err ?? state.error),
252261
attributes: Object.keys(state.attributes).length > 0 ? { ...state.attributes } : undefined,
262+
tags: Object.keys(state.tags).length > 0 ? { ...state.tags } : undefined,
253263
timestamp: new Date(endTimeMs).toISOString(),
254264
};
255265

@@ -304,6 +314,9 @@ function endRequest(
304314
if (state.userId) {
305315
setUserIdAttribute(span, state.userId);
306316
}
317+
if (Object.keys(state.tags).length > 0) {
318+
setTagsAttribute(span, state.tags);
319+
}
307320
if (err ?? state.error) {
308321
spanStatusFromError(span, err ?? state.error);
309322
}

src/core/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type RequestState = {
99
context: XrayContext;
1010
attributes: Record<string, AttributeValue>;
1111
events: Array<{ name: string; attributes?: Record<string, AttributeValue> }>;
12+
tags: Record<string, AttributeValue>;
1213
tenantId?: string;
1314
userId?: string;
1415
sessionId?: string;

src/core/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ export interface RequestLog {
119119
* Custom attributes added via `setAttribute`.
120120
*/
121121
attributes?: Record<string, AttributeValue>;
122+
/**
123+
* Custom tags added via `setTag`.
124+
*/
125+
tags?: Record<string, AttributeValue>;
122126
/**
123127
* ISO timestamp for when request processing completed.
124128
*/
@@ -156,6 +160,10 @@ export interface XrayContext {
156160
* Set a session identifier on the request log.
157161
*/
158162
setSessionId(id: string): void;
163+
/**
164+
* Set an arbitrary user-defined key/value tag on the request.
165+
*/
166+
setTag(key: string, value: AttributeValue): void;
159167
/**
160168
* Add or overwrite a custom attribute on the request span/log.
161169
*/

0 commit comments

Comments
 (0)