Skip to content

Commit 00e7ac6

Browse files
committed
feat(instrumentation-aws-sdk): add gen ai metrics for bedrock
1 parent 9d84216 commit 00e7ac6

File tree

12 files changed

+242
-50
lines changed

12 files changed

+242
-50
lines changed

plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
diag,
2222
SpanStatusCode,
2323
} from '@opentelemetry/api';
24-
import { suppressTracing } from '@opentelemetry/core';
24+
import { hrTime, suppressTracing } from '@opentelemetry/core';
2525
import { AttributeNames } from './enums';
2626
import { ServicesExtensions } from './services';
2727
import {
@@ -67,13 +67,20 @@ type V3PluginCommand = AwsV3Command<any, any, any, any, any> & {
6767

6868
export class AwsInstrumentation extends InstrumentationBase<AwsSdkInstrumentationConfig> {
6969
static readonly component = 'aws-sdk';
70-
private servicesExtensions: ServicesExtensions = new ServicesExtensions();
70+
// initialized in callbacks from super constructor for ordering reasons.
71+
private declare servicesExtensions: ServicesExtensions;
7172

7273
constructor(config: AwsSdkInstrumentationConfig = {}) {
7374
super(PACKAGE_NAME, PACKAGE_VERSION, config);
7475
}
7576

7677
protected init(): InstrumentationModuleDefinition[] {
78+
// Should always have been initialized in _updateMetricInstruments, but check again
79+
// for safety.
80+
if (!this.servicesExtensions) {
81+
this.servicesExtensions = new ServicesExtensions();
82+
}
83+
7784
const v3MiddlewareStackFileOldVersions = new InstrumentationNodeModuleFile(
7885
'@aws-sdk/middleware-stack/dist/cjs/MiddlewareStack.js',
7986
['>=3.1.0 <3.35.0'],
@@ -341,6 +348,7 @@ export class AwsInstrumentation extends InstrumentationBase<AwsSdkInstrumentatio
341348
self.getConfig(),
342349
self._diag
343350
);
351+
const startTime = hrTime();
344352
const span = self._startAwsV3Span(normalizedRequest, requestMetadata);
345353
const activeContextWithSpan = trace.setSpan(context.active(), span);
346354

@@ -404,7 +412,8 @@ export class AwsInstrumentation extends InstrumentationBase<AwsSdkInstrumentatio
404412
normalizedResponse,
405413
span,
406414
self.tracer,
407-
self.getConfig()
415+
self.getConfig(),
416+
startTime
408417
);
409418
self._callUserResponseHook(span, normalizedResponse);
410419
return response;
@@ -464,4 +473,11 @@ export class AwsInstrumentation extends InstrumentationBase<AwsSdkInstrumentatio
464473
return originalFunction();
465474
}
466475
}
476+
477+
override _updateMetricInstruments() {
478+
if (!this.servicesExtensions) {
479+
this.servicesExtensions = new ServicesExtensions();
480+
}
481+
this.servicesExtensions.updateMetricInstruments(this.meter);
482+
}
467483
}

plugins/node/opentelemetry-instrumentation-aws-sdk/src/semconv.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,23 @@ export const GEN_AI_OPERATION_NAME_VALUE_CHAT = 'chat' as const;
138138
* Enum value "aws.bedrock" for attribute {@link ATTR_GEN_AI_SYSTEM}.
139139
*/
140140
export const GEN_AI_SYSTEM_VALUE_AWS_BEDROCK = 'aws.bedrock' as const;
141+
142+
/**
143+
* The type of token being counted.
144+
*
145+
* @example input
146+
* @example output
147+
*
148+
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
149+
*/
150+
export const ATTR_GEN_AI_TOKEN_TYPE = 'gen_ai.token.type' as const;
151+
152+
/**
153+
* Enum value "input" for attribute {@link ATTR_GEN_AI_TOKEN_TYPE}.
154+
*/
155+
export const GEN_AI_TOKEN_TYPE_VALUE_INPUT = 'input' as const;
156+
157+
/**
158+
* Enum value "output" for attribute {@link ATTR_GEN_AI_TOKEN_TYPE}.
159+
*/
160+
export const GEN_AI_TOKEN_TYPE_VALUE_COMPLETION = 'output' as const;

plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/ServiceExtension.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
import {
1717
DiagLogger,
18+
HrTime,
19+
Meter,
1820
Span,
1921
SpanAttributes,
2022
SpanKind,
@@ -49,6 +51,9 @@ export interface ServiceExtension {
4951
response: NormalizedResponse,
5052
span: Span,
5153
tracer: Tracer,
52-
config: AwsSdkInstrumentationConfig
54+
config: AwsSdkInstrumentationConfig,
55+
startTime: HrTime
5356
) => void;
57+
58+
updateMetricInstruments?: (meter: Meter) => void;
5459
}

plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/ServicesExtensions.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { Tracer, Span, DiagLogger } from '@opentelemetry/api';
16+
import { Tracer, Span, DiagLogger, Meter, HrTime } from '@opentelemetry/api';
1717
import { ServiceExtension, RequestMetadata } from './ServiceExtension';
1818
import { SqsServiceExtension } from './sqs';
1919
import {
@@ -64,9 +64,16 @@ export class ServicesExtensions implements ServiceExtension {
6464
response: NormalizedResponse,
6565
span: Span,
6666
tracer: Tracer,
67-
config: AwsSdkInstrumentationConfig
67+
config: AwsSdkInstrumentationConfig,
68+
startTime: HrTime
6869
) {
6970
const serviceExtension = this.services.get(response.request.serviceName);
70-
serviceExtension?.responseHook?.(response, span, tracer, config);
71+
serviceExtension?.responseHook?.(response, span, tracer, config, startTime);
72+
}
73+
74+
updateMetricInstruments(meter: Meter) {
75+
for (const serviceExtension of this.services.values()) {
76+
serviceExtension.updateMetricInstruments?.(meter);
77+
}
7178
}
7279
}

plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/bedrock-runtime.ts

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { Attributes, DiagLogger, Span, Tracer } from '@opentelemetry/api';
16+
import {
17+
Attributes,
18+
DiagLogger,
19+
Histogram,
20+
HrTime,
21+
Meter,
22+
Span,
23+
Tracer,
24+
ValueType,
25+
} from '@opentelemetry/api';
1726
import { RequestMetadata, ServiceExtension } from './ServiceExtension';
1827
import {
1928
ATTR_GEN_AI_SYSTEM,
@@ -23,19 +32,57 @@ import {
2332
ATTR_GEN_AI_REQUEST_TEMPERATURE,
2433
ATTR_GEN_AI_REQUEST_TOP_P,
2534
ATTR_GEN_AI_REQUEST_STOP_SEQUENCES,
35+
ATTR_GEN_AI_TOKEN_TYPE,
2636
ATTR_GEN_AI_USAGE_INPUT_TOKENS,
2737
ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
2838
ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
2939
GEN_AI_OPERATION_NAME_VALUE_CHAT,
3040
GEN_AI_SYSTEM_VALUE_AWS_BEDROCK,
41+
GEN_AI_TOKEN_TYPE_VALUE_INPUT,
42+
GEN_AI_TOKEN_TYPE_VALUE_COMPLETION,
3143
} from '../semconv';
3244
import {
3345
AwsSdkInstrumentationConfig,
3446
NormalizedRequest,
3547
NormalizedResponse,
3648
} from '../types';
49+
import {
50+
hrTime,
51+
hrTimeDuration,
52+
hrTimeToMilliseconds,
53+
} from '@opentelemetry/core';
3754

3855
export class BedrockRuntimeServiceExtension implements ServiceExtension {
56+
private tokenUsage!: Histogram;
57+
private operationDuration!: Histogram;
58+
59+
updateMetricInstruments(meter: Meter) {
60+
this.tokenUsage = meter.createHistogram('gen_ai.client.token.usage', {
61+
unit: '{token}',
62+
description: 'Measures number of input and output tokens used',
63+
valueType: ValueType.INT,
64+
advice: {
65+
explicitBucketBoundaries: [
66+
1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304,
67+
16777216, 67108864,
68+
],
69+
},
70+
});
71+
this.operationDuration = meter.createHistogram(
72+
'gen_ai.client.operation.duration',
73+
{
74+
unit: 's',
75+
description: 'GenAI operation duration',
76+
advice: {
77+
explicitBucketBoundaries: [
78+
0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24,
79+
20.48, 40.96, 81.92,
80+
],
81+
},
82+
}
83+
);
84+
}
85+
3986
requestPreSpanHook(
4087
request: NormalizedRequest,
4188
config: AwsSdkInstrumentationConfig,
@@ -98,32 +145,61 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension {
98145
response: NormalizedResponse,
99146
span: Span,
100147
tracer: Tracer,
101-
config: AwsSdkInstrumentationConfig
148+
config: AwsSdkInstrumentationConfig,
149+
startTime: HrTime
102150
) {
103151
if (!span.isRecording()) {
104152
return;
105153
}
106154

107155
switch (response.request.commandName) {
108156
case 'Converse':
109-
return this.responseHookConverse(response, span, tracer, config);
157+
return this.responseHookConverse(
158+
response,
159+
span,
160+
tracer,
161+
config,
162+
startTime
163+
);
110164
}
111165
}
112166

113167
private responseHookConverse(
114168
response: NormalizedResponse,
115169
span: Span,
116170
tracer: Tracer,
117-
config: AwsSdkInstrumentationConfig
171+
config: AwsSdkInstrumentationConfig,
172+
startTime: HrTime
118173
) {
119174
const { stopReason, usage } = response.data;
175+
176+
const sharedMetricAttrs: Attributes = {
177+
[ATTR_GEN_AI_SYSTEM]: GEN_AI_SYSTEM_VALUE_AWS_BEDROCK,
178+
[ATTR_GEN_AI_OPERATION_NAME]: GEN_AI_OPERATION_NAME_VALUE_CHAT,
179+
[ATTR_GEN_AI_REQUEST_MODEL]: response.request.commandInput.modelId,
180+
};
181+
182+
const durationSecs =
183+
hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())) / 1000;
184+
this.operationDuration.record(durationSecs, sharedMetricAttrs);
185+
120186
if (usage) {
121187
const { inputTokens, outputTokens } = usage;
122188
if (inputTokens !== undefined) {
123189
span.setAttribute(ATTR_GEN_AI_USAGE_INPUT_TOKENS, inputTokens);
190+
191+
this.tokenUsage.record(inputTokens, {
192+
...sharedMetricAttrs,
193+
[ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_INPUT,
194+
});
124195
}
125196
if (outputTokens !== undefined) {
126197
span.setAttribute(ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, outputTokens);
198+
199+
this.tokenUsage.record(outputTokens, {
200+
...sharedMetricAttrs,
201+
[ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_COMPLETION,
202+
});
127203
}
128204
}
129205

plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-s3.test.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,12 @@
1515
*/
1616

1717
import {
18-
AwsInstrumentation,
1918
AwsSdkRequestHookInformation,
2019
AwsSdkResponseHookInformation,
2120
} from '../src';
22-
import {
23-
getTestSpans,
24-
registerInstrumentationTesting,
25-
} from '@opentelemetry/contrib-test-utils';
26-
const instrumentation = registerInstrumentationTesting(
27-
new AwsInstrumentation()
28-
);
21+
import { getTestSpans } from '@opentelemetry/contrib-test-utils';
22+
import { instrumentation } from './load-instrumentation';
23+
2924
import {
3025
PutObjectCommand,
3126
PutObjectCommandOutput,

plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-sqs.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,8 @@
1818
// covered multiple `client-*` packages. Its tests could be merged into
1919
// sqs.test.ts.
2020

21-
import { AwsInstrumentation } from '../src';
22-
import {
23-
getTestSpans,
24-
registerInstrumentationTesting,
25-
} from '@opentelemetry/contrib-test-utils';
26-
registerInstrumentationTesting(new AwsInstrumentation());
21+
import { getTestSpans } from '@opentelemetry/contrib-test-utils';
22+
import './load-instrumentation';
2723

2824
import { SQS } from '@aws-sdk/client-sqs';
2925

plugins/node/opentelemetry-instrumentation-aws-sdk/test/bedrock-runtime.test.ts

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,8 @@
2727
* keeping existing recordings, set NOCK_BACK_MODE to 'record'.
2828
*/
2929

30-
import {
31-
getTestSpans,
32-
registerInstrumentationTesting,
33-
} from '@opentelemetry/contrib-test-utils';
34-
import { AwsInstrumentation } from '../src';
35-
registerInstrumentationTesting(new AwsInstrumentation());
30+
import { getTestSpans } from '@opentelemetry/contrib-test-utils';
31+
import { metricReader } from './load-instrumentation';
3632

3733
import {
3834
BedrockRuntimeClient,
@@ -151,6 +147,74 @@ describe('Bedrock', () => {
151147
[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 10,
152148
[ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['max_tokens'],
153149
});
150+
151+
const { resourceMetrics } = await metricReader.collect();
152+
expect(resourceMetrics.scopeMetrics.length).toBe(1);
153+
const scopeMetrics = resourceMetrics.scopeMetrics[0];
154+
const tokenUsage = scopeMetrics.metrics.filter(
155+
m => m.descriptor.name === 'gen_ai.client.token.usage'
156+
);
157+
expect(tokenUsage.length).toBe(1);
158+
expect(tokenUsage[0].descriptor).toMatchObject({
159+
name: 'gen_ai.client.token.usage',
160+
type: 'HISTOGRAM',
161+
description: 'Measures number of input and output tokens used',
162+
unit: '{token}',
163+
});
164+
expect(tokenUsage[0].dataPoints.length).toBe(2);
165+
expect(tokenUsage[0].dataPoints).toEqual(
166+
expect.arrayContaining([
167+
expect.objectContaining({
168+
value: expect.objectContaining({
169+
sum: 8,
170+
}),
171+
attributes: {
172+
'gen_ai.system': 'aws.bedrock',
173+
'gen_ai.operation.name': 'chat',
174+
'gen_ai.request.model': 'amazon.titan-text-lite-v1',
175+
'gen_ai.token.type': 'input',
176+
},
177+
}),
178+
expect.objectContaining({
179+
value: expect.objectContaining({
180+
sum: 10,
181+
}),
182+
attributes: {
183+
'gen_ai.system': 'aws.bedrock',
184+
'gen_ai.operation.name': 'chat',
185+
'gen_ai.request.model': 'amazon.titan-text-lite-v1',
186+
'gen_ai.token.type': 'output',
187+
},
188+
}),
189+
])
190+
);
191+
192+
const operationDuration = scopeMetrics.metrics.filter(
193+
m => m.descriptor.name === 'gen_ai.client.operation.duration'
194+
);
195+
expect(operationDuration.length).toBe(1);
196+
expect(operationDuration[0].descriptor).toMatchObject({
197+
name: 'gen_ai.client.operation.duration',
198+
type: 'HISTOGRAM',
199+
description: 'GenAI operation duration',
200+
unit: 's',
201+
});
202+
expect(operationDuration[0].dataPoints.length).toBe(1);
203+
expect(operationDuration[0].dataPoints).toEqual([
204+
expect.objectContaining({
205+
value: expect.objectContaining({
206+
sum: expect.any(Number),
207+
}),
208+
attributes: {
209+
'gen_ai.system': 'aws.bedrock',
210+
'gen_ai.operation.name': 'chat',
211+
'gen_ai.request.model': 'amazon.titan-text-lite-v1',
212+
},
213+
}),
214+
]);
215+
expect(
216+
(operationDuration[0].dataPoints[0].value as any).sum
217+
).toBeGreaterThan(0);
154218
});
155219
});
156220

0 commit comments

Comments
 (0)