diff --git a/package-lock.json b/package-lock.json index a031d3c9d0..34626a81d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8774,9 +8774,10 @@ } }, "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", - "integrity": "sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz", + "integrity": "sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==", + "license": "Apache-2.0", "engines": { "node": ">=14" } @@ -35245,7 +35246,7 @@ "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/propagation-utils": "^0.31.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/semantic-conventions": "^1.31.0" }, "devDependencies": { "@aws-sdk/client-bedrock-runtime": "^3.587.0", @@ -44123,7 +44124,7 @@ "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/propagation-utils": "^0.31.0", "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/semantic-conventions": "^1.31.0", "@smithy/node-http-handler": "2.4.0", "@types/mocha": "10.0.10", "@types/node": "18.18.14", @@ -47243,9 +47244,9 @@ } }, "@opentelemetry/semantic-conventions": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", - "integrity": "sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==" + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz", + "integrity": "sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==" }, "@opentelemetry/sql-common": { "version": "file:packages/opentelemetry-sql-common", diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json b/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json index e9d3814b47..5a288354a5 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json @@ -47,7 +47,7 @@ "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/propagation-utils": "^0.31.0", - "@opentelemetry/semantic-conventions": "^1.27.0" + "@opentelemetry/semantic-conventions": "^1.31.0" }, "devDependencies": { "@aws-sdk/client-bedrock-runtime": "^3.587.0", diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts index f894e28458..159a27d5ca 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts @@ -21,7 +21,7 @@ import { diag, SpanStatusCode, } from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; +import { hrTime, suppressTracing } from '@opentelemetry/core'; import { AttributeNames } from './enums'; import { ServicesExtensions } from './services'; import { @@ -67,7 +67,8 @@ type V3PluginCommand = AwsV3Command & { export class AwsInstrumentation extends InstrumentationBase { static readonly component = 'aws-sdk'; - private servicesExtensions: ServicesExtensions = new ServicesExtensions(); + // need declare since initialized in callbacks from super constructor + private declare servicesExtensions: ServicesExtensions; constructor(config: AwsSdkInstrumentationConfig = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config); @@ -341,6 +342,7 @@ export class AwsInstrumentation extends InstrumentationBase void; + + updateMetricInstruments?: (meter: Meter) => void; } diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/ServicesExtensions.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/ServicesExtensions.ts index edf18bdaea..939dc881b0 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/ServicesExtensions.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/ServicesExtensions.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Tracer, Span, DiagLogger } from '@opentelemetry/api'; +import { Tracer, Span, DiagLogger, Meter, HrTime } from '@opentelemetry/api'; import { ServiceExtension, RequestMetadata } from './ServiceExtension'; import { SqsServiceExtension } from './sqs'; import { @@ -64,9 +64,16 @@ export class ServicesExtensions implements ServiceExtension { response: NormalizedResponse, span: Span, tracer: Tracer, - config: AwsSdkInstrumentationConfig + config: AwsSdkInstrumentationConfig, + startTime: HrTime ) { const serviceExtension = this.services.get(response.request.serviceName); - serviceExtension?.responseHook?.(response, span, tracer, config); + serviceExtension?.responseHook?.(response, span, tracer, config, startTime); + } + + updateMetricInstruments(meter: Meter) { + for (const serviceExtension of this.services.values()) { + serviceExtension.updateMetricInstruments?.(meter); + } } } diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/bedrock-runtime.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/bedrock-runtime.ts index a073802917..b7c5f83972 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/bedrock-runtime.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/bedrock-runtime.ts @@ -13,7 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Attributes, DiagLogger, Span, Tracer } from '@opentelemetry/api'; +import { + Attributes, + DiagLogger, + Histogram, + HrTime, + Meter, + Span, + Tracer, + ValueType, +} from '@opentelemetry/api'; import { RequestMetadata, ServiceExtension } from './ServiceExtension'; import { ATTR_GEN_AI_SYSTEM, @@ -23,19 +32,60 @@ import { ATTR_GEN_AI_REQUEST_TEMPERATURE, ATTR_GEN_AI_REQUEST_TOP_P, ATTR_GEN_AI_REQUEST_STOP_SEQUENCES, + ATTR_GEN_AI_TOKEN_TYPE, ATTR_GEN_AI_USAGE_INPUT_TOKENS, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, ATTR_GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_OPERATION_NAME_VALUE_CHAT, GEN_AI_SYSTEM_VALUE_AWS_BEDROCK, + GEN_AI_TOKEN_TYPE_VALUE_INPUT, + GEN_AI_TOKEN_TYPE_VALUE_OUTPUT, } from '../semconv'; import { AwsSdkInstrumentationConfig, NormalizedRequest, NormalizedResponse, } from '../types'; +import { + hrTime, + hrTimeDuration, + hrTimeToMilliseconds, +} from '@opentelemetry/core'; export class BedrockRuntimeServiceExtension implements ServiceExtension { + private tokenUsage!: Histogram; + private operationDuration!: Histogram; + + updateMetricInstruments(meter: Meter) { + // https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/#metric-gen_aiclienttokenusage + this.tokenUsage = meter.createHistogram('gen_ai.client.token.usage', { + unit: '{token}', + description: 'Measures number of input and output tokens used', + valueType: ValueType.INT, + advice: { + explicitBucketBoundaries: [ + 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, + 16777216, 67108864, + ], + }, + }); + + // https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/#metric-gen_aiclientoperationduration + this.operationDuration = meter.createHistogram( + 'gen_ai.client.operation.duration', + { + unit: 's', + description: 'GenAI operation duration', + advice: { + explicitBucketBoundaries: [ + 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, + 20.48, 40.96, 81.92, + ], + }, + } + ); + } + requestPreSpanHook( request: NormalizedRequest, config: AwsSdkInstrumentationConfig, @@ -262,7 +312,8 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { response: NormalizedResponse, span: Span, tracer: Tracer, - config: AwsSdkInstrumentationConfig + config: AwsSdkInstrumentationConfig, + startTime: HrTime ) { if (!span.isRecording()) { return; @@ -270,7 +321,13 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { switch (response.request.commandName) { case 'Converse': - return this.responseHookConverse(response, span, tracer, config); + return this.responseHookConverse( + response, + span, + tracer, + config, + startTime + ); case 'InvokeModel': return this.responseHookInvokeModel(response, span, tracer, config); } @@ -280,16 +337,38 @@ export class BedrockRuntimeServiceExtension implements ServiceExtension { response: NormalizedResponse, span: Span, tracer: Tracer, - config: AwsSdkInstrumentationConfig + config: AwsSdkInstrumentationConfig, + startTime: HrTime ) { const { stopReason, usage } = response.data; + + const sharedMetricAttrs: Attributes = { + [ATTR_GEN_AI_SYSTEM]: GEN_AI_SYSTEM_VALUE_AWS_BEDROCK, + [ATTR_GEN_AI_OPERATION_NAME]: GEN_AI_OPERATION_NAME_VALUE_CHAT, + [ATTR_GEN_AI_REQUEST_MODEL]: response.request.commandInput.modelId, + }; + + const durationSecs = + hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())) / 1000; + this.operationDuration.record(durationSecs, sharedMetricAttrs); + if (usage) { const { inputTokens, outputTokens } = usage; if (inputTokens !== undefined) { span.setAttribute(ATTR_GEN_AI_USAGE_INPUT_TOKENS, inputTokens); + + this.tokenUsage.record(inputTokens, { + ...sharedMetricAttrs, + [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_INPUT, + }); } if (outputTokens !== undefined) { span.setAttribute(ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, outputTokens); + + this.tokenUsage.record(outputTokens, { + ...sharedMetricAttrs, + [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_OUTPUT, + }); } } diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-s3.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-s3.test.ts index d12ae4fbdc..51371c46e3 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-s3.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-s3.test.ts @@ -15,17 +15,12 @@ */ import { - AwsInstrumentation, AwsSdkRequestHookInformation, AwsSdkResponseHookInformation, } from '../src'; -import { - getTestSpans, - registerInstrumentationTesting, -} from '@opentelemetry/contrib-test-utils'; -const instrumentation = registerInstrumentationTesting( - new AwsInstrumentation() -); +import { getTestSpans } from '@opentelemetry/contrib-test-utils'; +import { instrumentation } from './load-instrumentation'; + import { PutObjectCommand, PutObjectCommandOutput, diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-sqs.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-sqs.test.ts index 28d4932cee..e4a41c89aa 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-sqs.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3-sqs.test.ts @@ -18,12 +18,8 @@ // covered multiple `client-*` packages. Its tests could be merged into // sqs.test.ts. -import { AwsInstrumentation } from '../src'; -import { - getTestSpans, - registerInstrumentationTesting, -} from '@opentelemetry/contrib-test-utils'; -registerInstrumentationTesting(new AwsInstrumentation()); +import { getTestSpans } from '@opentelemetry/contrib-test-utils'; +import './load-instrumentation'; import { SQS } from '@aws-sdk/client-sqs'; diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/bedrock-runtime.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/bedrock-runtime.test.ts index f66118a918..7d220cc63b 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/bedrock-runtime.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/bedrock-runtime.test.ts @@ -27,12 +27,8 @@ * keeping existing recordings, set NOCK_BACK_MODE to 'record'. */ -import { - getTestSpans, - registerInstrumentationTesting, -} from '@opentelemetry/contrib-test-utils'; -import { AwsInstrumentation } from '../src'; -registerInstrumentationTesting(new AwsInstrumentation()); +import { getTestSpans } from '@opentelemetry/contrib-test-utils'; +import { metricReader } from './load-instrumentation'; import { BedrockRuntimeClient, @@ -157,6 +153,74 @@ describe('Bedrock', () => { [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 10, [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['max_tokens'], }); + + const { resourceMetrics } = await metricReader.collect(); + expect(resourceMetrics.scopeMetrics.length).toBe(1); + const scopeMetrics = resourceMetrics.scopeMetrics[0]; + const tokenUsage = scopeMetrics.metrics.filter( + m => m.descriptor.name === 'gen_ai.client.token.usage' + ); + expect(tokenUsage.length).toBe(1); + expect(tokenUsage[0].descriptor).toMatchObject({ + name: 'gen_ai.client.token.usage', + type: 'HISTOGRAM', + description: 'Measures number of input and output tokens used', + unit: '{token}', + }); + expect(tokenUsage[0].dataPoints.length).toBe(2); + expect(tokenUsage[0].dataPoints).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: expect.objectContaining({ + sum: 8, + }), + attributes: { + 'gen_ai.system': 'aws.bedrock', + 'gen_ai.operation.name': 'chat', + 'gen_ai.request.model': 'amazon.titan-text-lite-v1', + 'gen_ai.token.type': 'input', + }, + }), + expect.objectContaining({ + value: expect.objectContaining({ + sum: 10, + }), + attributes: { + 'gen_ai.system': 'aws.bedrock', + 'gen_ai.operation.name': 'chat', + 'gen_ai.request.model': 'amazon.titan-text-lite-v1', + 'gen_ai.token.type': 'output', + }, + }), + ]) + ); + + const operationDuration = scopeMetrics.metrics.filter( + m => m.descriptor.name === 'gen_ai.client.operation.duration' + ); + expect(operationDuration.length).toBe(1); + expect(operationDuration[0].descriptor).toMatchObject({ + name: 'gen_ai.client.operation.duration', + type: 'HISTOGRAM', + description: 'GenAI operation duration', + unit: 's', + }); + expect(operationDuration[0].dataPoints.length).toBe(1); + expect(operationDuration[0].dataPoints).toEqual([ + expect.objectContaining({ + value: expect.objectContaining({ + sum: expect.any(Number), + }), + attributes: { + 'gen_ai.system': 'aws.bedrock', + 'gen_ai.operation.name': 'chat', + 'gen_ai.request.model': 'amazon.titan-text-lite-v1', + }, + }), + ]); + expect( + (operationDuration[0].dataPoints[0].value as any).sum + ).toBeGreaterThan(0); }); }); diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/kinesis.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/kinesis.test.ts index cdd4d92cfa..9e8fed03e3 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/kinesis.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/kinesis.test.ts @@ -14,14 +14,10 @@ * limitations under the License. */ -import { - getTestSpans, - registerInstrumentationTesting, -} from '@opentelemetry/contrib-test-utils'; -import { AwsInstrumentation } from '../src'; -import { AttributeNames } from '../src/enums'; -registerInstrumentationTesting(new AwsInstrumentation()); +import { getTestSpans } from '@opentelemetry/contrib-test-utils'; +import './load-instrumentation'; +import { AttributeNames } from '../src/enums'; import { DescribeStreamCommand, KinesisClient } from '@aws-sdk/client-kinesis'; import { NodeHttpHandler } from '@smithy/node-http-handler'; import * as fs from 'fs'; diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/lambda.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/lambda.test.ts index 36fc14d990..e7857aeabf 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/lambda.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/lambda.test.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import { AwsInstrumentation } from '../src'; -import { - getTestSpans, - registerInstrumentationTesting, -} from '@opentelemetry/contrib-test-utils'; -registerInstrumentationTesting(new AwsInstrumentation()); +import './load-instrumentation'; +import { getTestSpans } from '@opentelemetry/contrib-test-utils'; import { SEMATTRS_FAAS_EXECUTION, SEMATTRS_FAAS_INVOKED_NAME, diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/load-instrumentation.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/load-instrumentation.ts new file mode 100644 index 0000000000..531ce19b82 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/load-instrumentation.ts @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Because all tests in this folder are run in the same process, if instantiating + * instrumentation within tests with different configurations such as metrics support, + * it can be difficult to ensure the correct instrumentation is applied during the + * specific test. We instead instantiate a single instrumentation instance here to + * use within all tests. + */ +import { + initMeterProvider, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; +import { AwsInstrumentation } from '../src'; + +export const instrumentation = new AwsInstrumentation(); +export const metricReader = initMeterProvider(instrumentation); +registerInstrumentationTesting(instrumentation); diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/s3.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/s3.test.ts index c7f29c8884..0527839ab4 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/s3.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/s3.test.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import { - getTestSpans, - registerInstrumentationTesting, -} from '@opentelemetry/contrib-test-utils'; -import { AwsInstrumentation } from '../src'; +import { getTestSpans } from '@opentelemetry/contrib-test-utils'; import { AttributeNames } from '../src/enums'; -registerInstrumentationTesting(new AwsInstrumentation()); +import './load-instrumentation'; import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import * as fs from 'fs';