From 2d48adb4b2d646a113ebb29cf25c6417870dbf7a Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 11:16:20 +0100 Subject: [PATCH 01/28] feat(instrumentation-undici): add new instrumentation package for undici and fetch --- .../.eslintignore | 1 + .../.eslintrc.js | 7 + .../.npmignore | 4 + .../LICENSE | 201 +++++++++++++++++ .../README.md | 71 ++++++ .../package.json | 75 +++++++ .../src/index.ts | 17 ++ .../src/internal-types.ts | 42 ++++ .../src/types.ts | 24 +++ .../src/undici.ts | 203 ++++++++++++++++++ .../tsconfig.json | 34 +++ package-lock.json | 98 +++++++++ tsconfig.json | 4 + 13 files changed, 781 insertions(+) create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/.eslintignore create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/.eslintrc.js create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/.npmignore create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/LICENSE create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/README.md create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/package.json create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/src/index.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/src/types.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/tsconfig.json diff --git a/experimental/packages/opentelemetry-instrumentation-undici/.eslintignore b/experimental/packages/opentelemetry-instrumentation-undici/.eslintignore new file mode 100644 index 00000000000..378eac25d31 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/.eslintignore @@ -0,0 +1 @@ +build diff --git a/experimental/packages/opentelemetry-instrumentation-undici/.eslintrc.js b/experimental/packages/opentelemetry-instrumentation-undici/.eslintrc.js new file mode 100644 index 00000000000..9baf1b49565 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.base.js') +} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/.npmignore b/experimental/packages/opentelemetry-instrumentation-undici/.npmignore new file mode 100644 index 00000000000..9505ba9450f --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/experimental/packages/opentelemetry-instrumentation-undici/LICENSE b/experimental/packages/opentelemetry-instrumentation-undici/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/experimental/packages/opentelemetry-instrumentation-undici/README.md b/experimental/packages/opentelemetry-instrumentation-undici/README.md new file mode 100644 index 00000000000..4f778441cc8 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/README.md @@ -0,0 +1,71 @@ +# OpenTelemetry Undici/fetch Instrumentation for Node.js + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +**Note: This is an experimental package under active development. New releases may include breaking changes.** + +This module provides automatic instrumentation for [`undici`](https://undici.nodejs.org/) and [`fetch`](https://nodejs.org/docs/latest/api/globals.html#fetch). + + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-undici +``` + +## Usage + +OpenTelemetry HTTP Instrumentation allows the user to automatically collect trace data and export them to their backend of choice, to give observability to distributed systems. + +To load a specific instrumentation (Undici in this case), specify it in the Node Tracer's configuration. + +```js +const { UndiciInstrumentation } = require('@opentelemetry/instrumentation-undici'); +const { + ConsoleSpanExporter, + NodeTracerProvider, + SimpleSpanProcessor, +} = require('@opentelemetry/sdk-trace-node'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); + +provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); +provider.register(); + +registerInstrumentations({ + instrumentations: [new UndiciInstrumentation()], +}); + +``` + +TODO: +See [examples/http](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/fetch) for a short example. + +### Fetch instrumentation Options + +TODO: + +Undici instrumentation has few options available to choose from. You can set the following: + +| Options | Type | Description | +| ------- | ---- | ----------- | +| [`onRequest`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts#93) | `HttpRequestCustomAttributeFunction` | Function for adding custom attributes before request is handled | + + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-http +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-http.svg diff --git a/experimental/packages/opentelemetry-instrumentation-undici/package.json b/experimental/packages/opentelemetry-instrumentation-undici/package.json new file mode 100644 index 00000000000..ea7a1c3d1b9 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/package.json @@ -0,0 +1,75 @@ +{ + "name": "@opentelemetry/instrumentation-undici", + "version": "0.1.0", + "description": "OpenTelemetry undici/fetch automatic instrumentation package.", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "prepublishOnly": "npm run compile", + "compile": "tsc --build", + "clean": "tsc --build --clean", + "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", + "tdd": "npm run test -- --watch-extensions ts --watch", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../", + "version": "node ../../../scripts/version-update.js", + "watch": "tsc --build --watch", + "precompile": "cross-var lerna run version --scope $npm_package_name --include-dependencies", + "prewatch": "node ../../../scripts/version-update.js", + "peer-api-check": "node ../../../scripts/peer-api-check.js" + }, + "keywords": [ + "opentelemetry", + "fetch", + "undici", + "nodejs", + "tracing", + "profiling", + "instrumentation" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@opentelemetry/api": "1.7.0", + "@opentelemetry/sdk-metrics": "1.19.0", + "@opentelemetry/sdk-trace-base": "1.19.0", + "@opentelemetry/sdk-trace-node": "1.19.0", + "@types/node": "18.6.5", + "codecov": "3.8.3", + "cross-var": "1.1.0", + "lerna": "6.6.2", + "mocha": "10.2.0", + "nock": "13.3.8", + "nyc": "15.1.0", + "sinon": "15.1.2", + "superagent": "8.0.9", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "dependencies": { + "@opentelemetry/core": "1.19.0", + "@opentelemetry/instrumentation": "0.46.0", + "@opentelemetry/semantic-conventions": "1.19.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-undici", + "sideEffects": false +} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/index.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/index.ts new file mode 100644 index 00000000000..fda578e7897 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './undici'; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts new file mode 100644 index 00000000000..a491fbe68e9 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +import type { Request, RequestInfo, Response } from 'undici'; + +// type Writeable = { -readonly [P in keyof T]: T[P] }; +// type WriteableRequest = Writeable; + +// Types declared in the lib +// - have some properties declared as `readonly` but we are changing them +// - imts some properties we need to inspect for the instrumentation +type UndiciRequest = Request & { + origin: RequestInfo; + path: string; +}; + +export interface RequestMessage { + request: UndiciRequest; +} + +export interface RequestResponseMessage { + request: UndiciRequest; + response: Response; +} + +export interface RequestErrorMessage { + request: UndiciRequest; + error: Error; +} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts new file mode 100644 index 00000000000..e0cf3f638bf --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import type { Span } from '@opentelemetry/api'; + +export type UndiciRequestHook = + (args: { request: RequestType; span: Span; additionalHeaders: Record; }) => void; + +export interface UndiciInstrumentationConfig extends InstrumentationConfig { + onRequest?: UndiciRequestHook; +} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts new file mode 100644 index 00000000000..3ef86b844e8 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -0,0 +1,203 @@ +/* + * 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. + */ +import * as diagch from 'diagnostics_channel'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { Instrumentation, InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { + Attributes, + context, + Meter, + MeterProvider, + metrics, + propagation, + Span, + SpanKind, + SpanStatusCode, + trace, + Tracer, + TracerProvider, +} from '@opentelemetry/api'; + +import { UndiciInstrumentationConfig } from './types'; + +interface ListenerRecord { + name: string; + channel: diagch.Channel; + onMessage: diagch.ChannelListener; +} + +// Get the content-length from undici response headers. +// `headers` is an Array of buffers: [k, v, k, v, ...]. +// If the header is not present, or has an invalid value, this returns null. +function contentLengthFromResponseHeaders(headers: Buffer[]) { + const name = 'content-length'; + for (let i = 0; i < headers.length; i += 2) { + const k = headers[i]; + if (k.length === name.length && k.toString().toLowerCase() === name) { + const v = Number(headers[i + 1]); + if (!Number.isNaN(Number(v))) { + return v; + } + return undefined; + } + } + return undefined; +} + +// A combination of https://github.com/elastic/apm-agent-nodejs and +// https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-undici/src/index.ts +export class UndiciInstrumentation implements Instrumentation { + // Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for + // unsubscribing. + private channelSubs: Array | undefined; + + private spanFromReq = new WeakMap(); + + private tracer: Tracer; + + private config: UndiciInstrumentationConfig; + + private meter: Meter; + + public readonly instrumentationName = 'opentelemetry-instrumentation-node-18-fetch'; + + public readonly instrumentationVersion = '1.0.0'; + + public readonly instrumentationDescription = 'Instrumentation for Node 18 fetch via diagnostics_channel'; + + private subscribeToChannel(diagnosticChannel: string, onMessage: diagch.ChannelListener) { + const channel = diagch.channel(diagnosticChannel); + channel.subscribe(onMessage); + this.channelSubs!.push({ + name: diagnosticChannel, + channel, + onMessage, + }); + } + + constructor(config: UndiciInstrumentationConfig) { + // Force load fetch API (since it's lazy loaded in Node 18) + fetch('').catch(() => {}); + this.channelSubs = []; + this.meter = metrics.getMeter(this.instrumentationName, this.instrumentationVersion); + this.tracer = trace.getTracer(this.instrumentationName, this.instrumentationVersion); + this.config = { ...config }; + } + + disable(): void { + this.channelSubs?.forEach((sub) => sub.channel.unsubscribe(sub.onMessage)); + } + + enable(): void { + this.subscribeToChannel('undici:request:create', (args) => this.onRequest(args)); + this.subscribeToChannel('undici:request:headers', (args) => this.onHeaders(args)); + this.subscribeToChannel('undici:request:trailers', (args) => this.onDone(args)); + this.subscribeToChannel('undici:request:error', (args) => this.onError(args)); + } + + setTracerProvider(tracerProvider: TracerProvider): void { + this.tracer = tracerProvider.getTracer( + this.instrumentationName, + this.instrumentationVersion, + ); + } + + public setMeterProvider(meterProvider: MeterProvider): void { + this.meter = meterProvider.getMeter( + this.instrumentationName, + this.instrumentationVersion, + ); + } + + setConfig(config: InstrumentationConfig): void { + this.config = { ...config }; + } + + getConfig(): InstrumentationConfig { + return this.config; + } + + onRequest({ request }: any): void { + // We do not handle instrumenting HTTP CONNECT. See limitation notes above. + if (request.method === 'CONNECT') { + return; + } + const span = this.tracer.startSpan(`HTTP ${request.method}`, { + kind: SpanKind.CLIENT, + attributes: { + [SemanticAttributes.HTTP_URL]: String(request.origin), + [SemanticAttributes.HTTP_METHOD]: request.method, + [SemanticAttributes.HTTP_TARGET]: request.path, + 'http.client': 'fetch', + }, + }); + const requestContext = trace.setSpan(context.active(), span); + const addedHeaders: Record = {}; + propagation.inject(requestContext, addedHeaders); + + if (this.config.onRequest) { + this.config.onRequest({ request, span, additionalHeaders: addedHeaders }); + } + + request.headers += Object.entries(addedHeaders) + .map(([k, v]) => `${k}: ${v}\r\n`) + .join(''); + this.spanFromReq.set(request, span); + } + + onHeaders({ request, response }: any): void { + const span = this.spanFromReq.get(request); + + if (span !== undefined) { + // We are currently *not* capturing response headers, even though the + // intake API does allow it, because none of the other `setHttpContext` + // uses currently do. + + const cLen = contentLengthFromResponseHeaders(response.headers); + const attrs: Attributes = { + [SemanticAttributes.HTTP_STATUS_CODE]: response.statusCode, + }; + if (cLen) { + attrs[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = cLen; + } + span.setAttributes(attrs); + span.setStatus({ + code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.OK, + message: String(response.statusCode), + }); + } + } + + onDone({ request }: any): void { + const span = this.spanFromReq.get(request); + if (span !== undefined) { + span.end(); + this.spanFromReq.delete(request); + } + } + + onError({ request, error }: any): void { + const span = this.spanFromReq.get(request); + if (span !== undefined) { + span.recordException(error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); + span.end(); + } + } +} \ No newline at end of file diff --git a/experimental/packages/opentelemetry-instrumentation-undici/tsconfig.json b/experimental/packages/opentelemetry-instrumentation-undici/tsconfig.json new file mode 100644 index 00000000000..60bb10f9028 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "." + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-node" + }, + { + "path": "../../../packages/opentelemetry-semantic-conventions" + }, + { + "path": "../../../packages/sdk-metrics" + }, + { + "path": "../opentelemetry-instrumentation" + } + ] +} diff --git a/package-lock.json b/package-lock.json index 3e074c7978c..528031c77aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3555,6 +3555,40 @@ "proxy-from-env": "^1.1.0" } }, + "experimental/packages/opentelemetry-instrumentation-undici": { + "name": "@opentelemetry/instrumentation-undici", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.19.0", + "@opentelemetry/instrumentation": "0.46.0", + "@opentelemetry/semantic-conventions": "1.19.0" + }, + "devDependencies": { + "@opentelemetry/api": "1.7.0", + "@opentelemetry/sdk-metrics": "1.19.0", + "@opentelemetry/sdk-trace-base": "1.19.0", + "@opentelemetry/sdk-trace-node": "1.19.0", + "@types/node": "18.6.5", + "codecov": "3.8.3", + "cross-var": "1.1.0", + "lerna": "6.6.2", + "mocha": "10.2.0", + "nock": "13.3.8", + "nyc": "15.1.0", + "sinon": "15.1.2", + "superagent": "8.0.9", + "ts-mocha": "10.0.0", + "typescript": "4.4.4", + "undici": "6.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "experimental/packages/opentelemetry-instrumentation-xml-http-request": { "name": "@opentelemetry/instrumentation-xml-http-request", "version": "0.46.0", @@ -7118,6 +7152,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "dev": true, @@ -9653,6 +9696,10 @@ "resolved": "experimental/packages/opentelemetry-instrumentation-http", "link": true }, + "node_modules/@opentelemetry/instrumentation-undici": { + "resolved": "experimental/packages/opentelemetry-instrumentation-undici", + "link": true + }, "node_modules/@opentelemetry/instrumentation-xml-http-request": { "resolved": "experimental/packages/opentelemetry-instrumentation-xml-http-request", "link": true @@ -31127,6 +31174,18 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.2.1.tgz", + "integrity": "sha512-7Wa9thEM6/LMnnKtxJHlc8SrTlDmxqJecgz1iy8KlsN0/iskQXOQCuPkrZLXbElPaSw5slFFyKIKXyJ3UtbApw==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=18.0" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "dev": true, @@ -37268,6 +37327,12 @@ "version": "8.44.0", "dev": true }, + "@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "dev": true + }, "@gar/promisify": { "version": "1.1.3", "dev": true @@ -41173,6 +41238,30 @@ } } }, + "@opentelemetry/instrumentation-undici": { + "version": "file:experimental/packages/opentelemetry-instrumentation-undici", + "requires": { + "@opentelemetry/api": "1.7.0", + "@opentelemetry/core": "1.19.0", + "@opentelemetry/instrumentation": "0.46.0", + "@opentelemetry/sdk-metrics": "1.19.0", + "@opentelemetry/sdk-trace-base": "1.19.0", + "@opentelemetry/sdk-trace-node": "1.19.0", + "@opentelemetry/semantic-conventions": "1.19.0", + "@types/node": "18.6.5", + "codecov": "3.8.3", + "cross-var": "1.1.0", + "lerna": "6.6.2", + "mocha": "10.2.0", + "nock": "13.3.8", + "nyc": "15.1.0", + "sinon": "15.1.2", + "superagent": "8.0.9", + "ts-mocha": "10.0.0", + "typescript": "4.4.4", + "undici": "6.2.1" + } + }, "@opentelemetry/instrumentation-xml-http-request": { "version": "file:experimental/packages/opentelemetry-instrumentation-xml-http-request", "requires": { @@ -57874,6 +57963,15 @@ "version": "1.13.6", "dev": true }, + "undici": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.2.1.tgz", + "integrity": "sha512-7Wa9thEM6/LMnnKtxJHlc8SrTlDmxqJecgz1iy8KlsN0/iskQXOQCuPkrZLXbElPaSw5slFFyKIKXyJ3UtbApw==", + "dev": true, + "requires": { + "@fastify/busboy": "^2.0.0" + } + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "dev": true diff --git a/tsconfig.json b/tsconfig.json index acf73290872..0784fb5ee87 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "experimental/packages/opentelemetry-instrumentation-fetch", "experimental/packages/opentelemetry-instrumentation-grpc", "experimental/packages/opentelemetry-instrumentation-http", + "experimental/packages/opentelemetry-instrumentation-undici", "experimental/packages/opentelemetry-instrumentation-xml-http-request", "experimental/packages/opentelemetry-sdk-node", "experimental/packages/otlp-exporter-base", @@ -111,6 +112,9 @@ { "path": "experimental/packages/opentelemetry-instrumentation-http" }, + { + "path": "experimental/packages/opentelemetry-instrumentation-undici" + }, { "path": "experimental/packages/opentelemetry-instrumentation-xml-http-request" }, From 23c7890750520ee34fd1961df429a85cb2825f7d Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 15:35:49 +0100 Subject: [PATCH 02/28] tests(instrumentation-undici): add test file for fetch --- .../package.json | 3 +- .../src/internal-types.ts | 12 +- .../src/types.ts | 2 + .../src/undici.ts | 130 ++++++++---------- .../test/fetch.test.ts | 108 +++++++++++++++ .../test/utils/mock-server.ts | 71 ++++++++++ 6 files changed, 251 insertions(+), 75 deletions(-) create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts diff --git a/experimental/packages/opentelemetry-instrumentation-undici/package.json b/experimental/packages/opentelemetry-instrumentation-undici/package.json index ea7a1c3d1b9..b26848169cd 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/package.json +++ b/experimental/packages/opentelemetry-instrumentation-undici/package.json @@ -60,7 +60,8 @@ "sinon": "15.1.2", "superagent": "8.0.9", "ts-mocha": "10.0.0", - "typescript": "4.4.4" + "typescript": "4.4.4", + "undici": "6.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts index a491fbe68e9..b53373eba5c 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts @@ -13,15 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { Channel } from 'diagnostics_channel'; import type { Request, RequestInfo, Response } from 'undici'; +export interface ListenerRecord { + name: string; + channel: Channel; + onMessage: (message: any, name: string) => void; +} + // type Writeable = { -readonly [P in keyof T]: T[P] }; // type WriteableRequest = Writeable; +// TODO: the actual `request` object at runtime have subtle differences +// from the `Request` type declared in `undici`. Type properly +// // Types declared in the lib // - have some properties declared as `readonly` but we are changing them -// - imts some properties we need to inspect for the instrumentation +// - omits some properties we need to inspect for the instrumentation type UndiciRequest = Request & { origin: RequestInfo; path: string; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts index e0cf3f638bf..772cc6e6f83 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts @@ -19,6 +19,8 @@ import type { Span } from '@opentelemetry/api'; export type UndiciRequestHook = (args: { request: RequestType; span: Span; additionalHeaders: Record; }) => void; +// TODO: This package will instrument HTTP requests made through Undici +// so it seems logical to have similar options than the HTTP instrumentation export interface UndiciInstrumentationConfig extends InstrumentationConfig { onRequest?: UndiciRequestHook; } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 3ef86b844e8..cda11e41cbd 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -15,29 +15,22 @@ */ import * as diagch from 'diagnostics_channel'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { Instrumentation, InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { InstrumentationBase } from '@opentelemetry/instrumentation'; import { Attributes, context, Meter, - MeterProvider, - metrics, propagation, Span, SpanKind, SpanStatusCode, trace, - Tracer, - TracerProvider, } from '@opentelemetry/api'; -import { UndiciInstrumentationConfig } from './types'; +import { VERSION } from './version'; -interface ListenerRecord { - name: string; - channel: diagch.Channel; - onMessage: diagch.ChannelListener; -} +import { ListenerRecord } from './internal-types'; +import { UndiciInstrumentationConfig } from './types'; // Get the content-length from undici response headers. // `headers` is an Array of buffers: [k, v, k, v, ...]. @@ -59,82 +52,71 @@ function contentLengthFromResponseHeaders(headers: Buffer[]) { // A combination of https://github.com/elastic/apm-agent-nodejs and // https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-undici/src/index.ts -export class UndiciInstrumentation implements Instrumentation { +export class UndiciInstrumentation extends InstrumentationBase { // Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for // unsubscribing. - private channelSubs: Array | undefined; - - private spanFromReq = new WeakMap(); - - private tracer: Tracer; - - private config: UndiciInstrumentationConfig; - - private meter: Meter; + private _channelSubs!: Array; - public readonly instrumentationName = 'opentelemetry-instrumentation-node-18-fetch'; + private _spanFromReq = new WeakMap(); - public readonly instrumentationVersion = '1.0.0'; + private _requestHook: UndiciInstrumentationConfig['onRequest']; - public readonly instrumentationDescription = 'Instrumentation for Node 18 fetch via diagnostics_channel'; + // @ts-expect-error -- we are no reading its value in this + override meter: Meter; - private subscribeToChannel(diagnosticChannel: string, onMessage: diagch.ChannelListener) { - const channel = diagch.channel(diagnosticChannel); - channel.subscribe(onMessage); - this.channelSubs!.push({ - name: diagnosticChannel, - channel, - onMessage, - }); - } - - constructor(config: UndiciInstrumentationConfig) { + constructor(config?: UndiciInstrumentationConfig) { + super('@opentelemetry/instrumentation-undici', VERSION, config); // Force load fetch API (since it's lazy loaded in Node 18) fetch('').catch(() => {}); - this.channelSubs = []; - this.meter = metrics.getMeter(this.instrumentationName, this.instrumentationVersion); - this.tracer = trace.getTracer(this.instrumentationName, this.instrumentationVersion); - this.config = { ...config }; - } - - disable(): void { - this.channelSubs?.forEach((sub) => sub.channel.unsubscribe(sub.onMessage)); + this.setConfig(config); } - enable(): void { - this.subscribeToChannel('undici:request:create', (args) => this.onRequest(args)); - this.subscribeToChannel('undici:request:headers', (args) => this.onHeaders(args)); - this.subscribeToChannel('undici:request:trailers', (args) => this.onDone(args)); - this.subscribeToChannel('undici:request:error', (args) => this.onError(args)); + // No need to instrument files/modules + protected override init() { + return undefined; } - setTracerProvider(tracerProvider: TracerProvider): void { - this.tracer = tracerProvider.getTracer( - this.instrumentationName, - this.instrumentationVersion, - ); + override disable(): void { + this._channelSubs.forEach((sub) => sub.channel.unsubscribe(sub.onMessage)); + this._channelSubs.length = 0; } - public setMeterProvider(meterProvider: MeterProvider): void { - this.meter = meterProvider.getMeter( - this.instrumentationName, - this.instrumentationVersion, - ); + override enable(): void { + if (this._config.enabled) { + return; + } + // This methos is called by the `InstrumentationAbstract` constructor before + // ours is called. So we need to ensure the property is initalized + this._channelSubs = this._channelSubs || []; + this.subscribeToChannel('undici:request:create', this.onRequest.bind(this)); + this.subscribeToChannel('undici:request:headers', this.onHeaders.bind(this)); + this.subscribeToChannel('undici:request:trailers', this.onDone.bind(this)); + this.subscribeToChannel('undici:request:error', this.onError.bind(this)); } - setConfig(config: InstrumentationConfig): void { - this.config = { ...config }; + override setConfig(config?: UndiciInstrumentationConfig): void { + super.setConfig(config); + if (typeof config?.onRequest === 'function') { + this._requestHook = config.onRequest; + } } - getConfig(): InstrumentationConfig { - return this.config; + private subscribeToChannel(diagnosticChannel: string, onMessage: ListenerRecord['onMessage']) { + const channel = diagch.channel(diagnosticChannel); + channel.subscribe(onMessage); + this._channelSubs.push({ + name: diagnosticChannel, + channel, + onMessage, + }); } - onRequest({ request }: any): void { + private onRequest({ request }: any): void { // We do not handle instrumenting HTTP CONNECT. See limitation notes above. if (request.method === 'CONNECT') { return; } + const span = this.tracer.startSpan(`HTTP ${request.method}`, { kind: SpanKind.CLIENT, attributes: { @@ -148,18 +130,20 @@ export class UndiciInstrumentation implements Instrumentation { const addedHeaders: Record = {}; propagation.inject(requestContext, addedHeaders); - if (this.config.onRequest) { - this.config.onRequest({ request, span, additionalHeaders: addedHeaders }); + if (this._requestHook) { + this._requestHook({ request, span, additionalHeaders: addedHeaders }); } + console.log('request', request) request.headers += Object.entries(addedHeaders) .map(([k, v]) => `${k}: ${v}\r\n`) .join(''); - this.spanFromReq.set(request, span); + console.log('headers', request.headers) + this._spanFromReq.set(request, span); } - onHeaders({ request, response }: any): void { - const span = this.spanFromReq.get(request); + private onHeaders({ request, response }: any): void { + const span = this._spanFromReq.get(request); if (span !== undefined) { // We are currently *not* capturing response headers, even though the @@ -181,16 +165,16 @@ export class UndiciInstrumentation implements Instrumentation { } } - onDone({ request }: any): void { - const span = this.spanFromReq.get(request); + private onDone({ request }: any): void { + const span = this._spanFromReq.get(request); if (span !== undefined) { span.end(); - this.spanFromReq.delete(request); + this._spanFromReq.delete(request); } } - onError({ request, error }: any): void { - const span = this.spanFromReq.get(request); + private onError({ request, error }: any): void { + const span = this._spanFromReq.get(request); if (span !== undefined) { span.recordException(error); span.setStatus({ diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts new file mode 100644 index 00000000000..5eb3ec56d4c --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -0,0 +1,108 @@ +/* + * 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. + */ + +import * as assert from 'assert'; +import * as http from 'http'; +import * as url from 'url'; + +import { SpanKind, Span, context, propagation } from '@opentelemetry/api'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; + +import { UndiciInstrumentation } from '../src/undici'; + +import { MockServer } from './utils/mock-server' + + +const instrumentation = new UndiciInstrumentation(); +instrumentation.enable(); +instrumentation.disable(); + + +const protocol = 'http'; +const serverPort = 32345; +const hostname = 'localhost'; +const mockServer = new MockServer(); +const memoryExporter = new InMemorySpanExporter(); +const provider = new NodeTracerProvider(); +provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); +instrumentation.setTracerProvider(provider); + +describe('UndiciInstrumentation `fetch` tests', () => { + before(done => { + mockServer.start(done); + }); + + after(done => { + mockServer.stop(done); + }); + + beforeEach(() => { + memoryExporter.reset(); + }); + + before(() => { + // propagation.setGlobalPropagator(new DummyPropagation()); + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + }); + + after(() => { + context.disable(); + propagation.disable(); + }); + + describe('enable()', () => { + before(() => { + instrumentation.enable(); + }); + after(() => { + instrumentation.disable(); + }); + + it('should create a rootSpan for GET requests and add propagation headers', async () => { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const response = await fetch( + `${protocol}://localhost:${mockServer.port}/?query=test` + ); + + spans = memoryExporter.getFinishedSpans(); + // const span = spans.find(s => s.kind === SpanKind.CLIENT); + const span = spans[0]; + assert.ok(span); + const validations = { + hostname: 'localhost', + httpStatusCode: response.status, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: response.headers, + reqHeaders: {}, + component: 'http', + }; + + assert.strictEqual(spans.length, 1); + assert.strictEqual(span.name, 'HTTP GET'); + // console.log(span) + // assertSpan(span, SpanKind.CLIENT, validations); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts new file mode 100644 index 00000000000..fc9e7d60e9e --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts @@ -0,0 +1,71 @@ +/* + * 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. + */ + +import * as assert from 'assert'; +import * as http from 'http'; +import * as url from 'url'; + + +export class MockServer { + private _port: number; + private _httpServer: http.Server; + + get port(): number { + return this._port; + } + + start(cb: (err?: Error) => void) { + this._httpServer = http.createServer((req, res) => { + if (req.url === '/timeout') { + setTimeout(() => { + res.end(); + }, 1000); + } + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.write( + JSON.stringify({ + success: true, + }) + ); + res.end(); + }); + + this._httpServer.listen(0, () => { + const addr = this._httpServer.address(); + if (addr == null) { + cb(new Error('unexpected addr null')); + return; + } + + if (typeof addr === 'string') { + cb(new Error(`unexpected addr ${addr}`)); + return; + } + + if (addr.port <= 0) { + cb(new Error('Could not get port')); + return; + } + this._port = addr.port; + cb(); + }); + } + + stop(cb: (err?: Error) => void) { + this._httpServer.close(cb); + } +} From be8c2a55c563b0ab355b6a170a79dc5809cb63aa Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 16:59:40 +0100 Subject: [PATCH 03/28] tests(instrumentation-undici): add attribute assertions --- .../src/undici.ts | 4 +- .../test/fetch.test.ts | 57 +++++++++++-------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index cda11e41cbd..ac4589198b7 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import * as diagch from 'diagnostics_channel'; + import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { InstrumentationBase } from '@opentelemetry/instrumentation'; import { @@ -123,7 +124,6 @@ export class UndiciInstrumentation extends InstrumentationBase { [SemanticAttributes.HTTP_URL]: String(request.origin), [SemanticAttributes.HTTP_METHOD]: request.method, [SemanticAttributes.HTTP_TARGET]: request.path, - 'http.client': 'fetch', }, }); const requestContext = trace.setSpan(context.active(), span); @@ -134,11 +134,9 @@ export class UndiciInstrumentation extends InstrumentationBase { this._requestHook({ request, span, additionalHeaders: addedHeaders }); } - console.log('request', request) request.headers += Object.entries(addedHeaders) .map(([k, v]) => `${k}: ${v}\r\n`) .join(''); - console.log('headers', request.headers) this._spanFromReq.set(request, span); } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 5eb3ec56d4c..2c98760ae2c 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import * as assert from 'assert'; -import * as http from 'http'; -import * as url from 'url'; -import { SpanKind, Span, context, propagation } from '@opentelemetry/api'; +import { SpanKind, context, propagation } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { InMemorySpanExporter, + ReadableSpan, SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; @@ -30,14 +29,11 @@ import { UndiciInstrumentation } from '../src/undici'; import { MockServer } from './utils/mock-server' - const instrumentation = new UndiciInstrumentation(); instrumentation.enable(); instrumentation.disable(); - const protocol = 'http'; -const serverPort = 32345; const hostname = 'localhost'; const mockServer = new MockServer(); const memoryExporter = new InMemorySpanExporter(); @@ -59,6 +55,7 @@ describe('UndiciInstrumentation `fetch` tests', () => { }); before(() => { + // TODO: mock propagation and test it // propagation.setGlobalPropagator(new DummyPropagation()); context.setGlobalContextManager(new AsyncHooksContextManager().enable()); }); @@ -80,29 +77,39 @@ describe('UndiciInstrumentation `fetch` tests', () => { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - const response = await fetch( - `${protocol}://localhost:${mockServer.port}/?query=test` - ); + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const response = await fetch(fetchUrl); spans = memoryExporter.getFinishedSpans(); - // const span = spans.find(s => s.kind === SpanKind.CLIENT); const span = spans[0]; + assert.ok(span); - const validations = { - hostname: 'localhost', - httpStatusCode: response.status, - httpMethod: 'GET', - pathname: '/', - path: '/?query=test', - resHeaders: response.headers, - reqHeaders: {}, - component: 'http', - }; - assert.strictEqual(spans.length, 1); - assert.strictEqual(span.name, 'HTTP GET'); - // console.log(span) - // assertSpan(span, SpanKind.CLIENT, validations); + assertSpanAttribs(span, 'HTTP GET', { + // TODO: I guess we want to have parity with HTTP insturmentation + // - there are missing attributes + // - also check if these current values make sense + [SemanticAttributes.HTTP_URL]: `${protocol}://${hostname}:${mockServer.port}`, + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_STATUS_CODE]: response.status, + [SemanticAttributes.HTTP_TARGET]: '/?query=test', + }); }); }); }); + + +function assertSpanAttribs(span: ReadableSpan, name: string, attribs: Record) { + assert.strictEqual(span.spanContext().traceId.length, 32); + assert.strictEqual(span.spanContext().spanId.length, 16); + assert.strictEqual(span.kind, SpanKind.CLIENT); + assert.strictEqual(span.name, name); + + for (const [key, value] of Object.entries(attribs)) { + assert.strictEqual( + span.attributes[key], + value, + `expected value "${value}" but got "${span.attributes[key]}" for attribute "${key}" `, + ); + } +} From e091a8f67a8da998872d8c243ce1ff4ed1d46c6d Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 17:27:14 +0100 Subject: [PATCH 04/28] chore(instrumentation-undici): update readme --- .../packages/opentelemetry-instrumentation-undici/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/README.md b/experimental/packages/opentelemetry-instrumentation-undici/README.md index 4f778441cc8..ace387920b2 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/README.md +++ b/experimental/packages/opentelemetry-instrumentation-undici/README.md @@ -40,18 +40,18 @@ registerInstrumentations({ ``` -TODO: + See [examples/http](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/fetch) for a short example. ### Fetch instrumentation Options -TODO: + Undici instrumentation has few options available to choose from. You can set the following: | Options | Type | Description | | ------- | ---- | ----------- | -| [`onRequest`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts#93) | `HttpRequestCustomAttributeFunction` | Function for adding custom attributes before request is handled | +| [`onRequest`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts#19) | `UndiciRequestHook` | Function for adding custom attributes before request is handled | ## Useful links From 9870554bee7c2409aa64574ed13882c5f7eb4616 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 17:34:12 +0100 Subject: [PATCH 05/28] chore(instrumentation-undici): fix lint issues --- .../src/internal-types.ts | 2 +- .../src/types.ts | 7 +++++-- .../src/undici.ts | 17 ++++++++++++----- .../test/fetch.test.ts | 17 ++++++++++------- .../test/utils/mock-server.ts | 7 +++---- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts index b53373eba5c..89077065c40 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts @@ -28,7 +28,7 @@ export interface ListenerRecord { // TODO: the actual `request` object at runtime have subtle differences // from the `Request` type declared in `undici`. Type properly -// +// // Types declared in the lib // - have some properties declared as `readonly` but we are changing them // - omits some properties we need to inspect for the instrumentation diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts index 772cc6e6f83..576e03e6e11 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts @@ -16,8 +16,11 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import type { Span } from '@opentelemetry/api'; -export type UndiciRequestHook = - (args: { request: RequestType; span: Span; additionalHeaders: Record; }) => void; +export type UndiciRequestHook = (args: { + request: RequestType; + span: Span; + additionalHeaders: Record; +}) => void; // TODO: This package will instrument HTTP requests made through Undici // so it seems logical to have similar options than the HTTP instrumentation diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index ac4589198b7..615eedcf224 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -78,7 +78,7 @@ export class UndiciInstrumentation extends InstrumentationBase { } override disable(): void { - this._channelSubs.forEach((sub) => sub.channel.unsubscribe(sub.onMessage)); + this._channelSubs.forEach(sub => sub.channel.unsubscribe(sub.onMessage)); this._channelSubs.length = 0; } @@ -90,7 +90,10 @@ export class UndiciInstrumentation extends InstrumentationBase { // ours is called. So we need to ensure the property is initalized this._channelSubs = this._channelSubs || []; this.subscribeToChannel('undici:request:create', this.onRequest.bind(this)); - this.subscribeToChannel('undici:request:headers', this.onHeaders.bind(this)); + this.subscribeToChannel( + 'undici:request:headers', + this.onHeaders.bind(this) + ); this.subscribeToChannel('undici:request:trailers', this.onDone.bind(this)); this.subscribeToChannel('undici:request:error', this.onError.bind(this)); } @@ -102,7 +105,10 @@ export class UndiciInstrumentation extends InstrumentationBase { } } - private subscribeToChannel(diagnosticChannel: string, onMessage: ListenerRecord['onMessage']) { + private subscribeToChannel( + diagnosticChannel: string, + onMessage: ListenerRecord['onMessage'] + ) { const channel = diagch.channel(diagnosticChannel); channel.subscribe(onMessage); this._channelSubs.push({ @@ -157,7 +163,8 @@ export class UndiciInstrumentation extends InstrumentationBase { } span.setAttributes(attrs); span.setStatus({ - code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.OK, + code: + response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.OK, message: String(response.statusCode), }); } @@ -182,4 +189,4 @@ export class UndiciInstrumentation extends InstrumentationBase { span.end(); } } -} \ No newline at end of file +} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 2c98760ae2c..1e7f13c0d42 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -27,7 +27,7 @@ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { UndiciInstrumentation } from '../src/undici'; -import { MockServer } from './utils/mock-server' +import { MockServer } from './utils/mock-server'; const instrumentation = new UndiciInstrumentation(); instrumentation.enable(); @@ -76,10 +76,10 @@ describe('UndiciInstrumentation `fetch` tests', () => { it('should create a rootSpan for GET requests and add propagation headers', async () => { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const response = await fetch(fetchUrl); - + spans = memoryExporter.getFinishedSpans(); const span = spans[0]; @@ -98,18 +98,21 @@ describe('UndiciInstrumentation `fetch` tests', () => { }); }); - -function assertSpanAttribs(span: ReadableSpan, name: string, attribs: Record) { +function assertSpanAttribs( + span: ReadableSpan, + name: string, + attribs: Record +) { assert.strictEqual(span.spanContext().traceId.length, 32); assert.strictEqual(span.spanContext().spanId.length, 16); assert.strictEqual(span.kind, SpanKind.CLIENT); assert.strictEqual(span.name, name); - + for (const [key, value] of Object.entries(attribs)) { assert.strictEqual( span.attributes[key], value, - `expected value "${value}" but got "${span.attributes[key]}" for attribute "${key}" `, + `expected value "${value}" but got "${span.attributes[key]}" for attribute "${key}" ` ); } } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts index fc9e7d60e9e..d0a2f5f0f39 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts @@ -18,7 +18,6 @@ import * as assert from 'assert'; import * as http from 'http'; import * as url from 'url'; - export class MockServer { private _port: number; private _httpServer: http.Server; @@ -43,19 +42,19 @@ export class MockServer { ); res.end(); }); - + this._httpServer.listen(0, () => { const addr = this._httpServer.address(); if (addr == null) { cb(new Error('unexpected addr null')); return; } - + if (typeof addr === 'string') { cb(new Error(`unexpected addr ${addr}`)); return; } - + if (addr.port <= 0) { cb(new Error('Could not get port')); return; From c6b0258b72185f8f5b379ee6b8cad929e8bda51b Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 17:42:05 +0100 Subject: [PATCH 06/28] chore(instrumentation-undici): fix types in tests --- .../test/utils/mock-server.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts index d0a2f5f0f39..30b8e641674 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts @@ -13,17 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import * as assert from 'assert'; import * as http from 'http'; -import * as url from 'url'; export class MockServer { - private _port: number; - private _httpServer: http.Server; + private _port: number | undefined; + private _httpServer: http.Server | undefined; get port(): number { - return this._port; + return this._port || 0; } start(cb: (err?: Error) => void) { @@ -44,7 +41,7 @@ export class MockServer { }); this._httpServer.listen(0, () => { - const addr = this._httpServer.address(); + const addr = this._httpServer!.address(); if (addr == null) { cb(new Error('unexpected addr null')); return; From 459eca88f9ae50936cd49a5f384c6b449d6af21d Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 18:24:22 +0100 Subject: [PATCH 07/28] chore(instrumentation-undici): fix types and skip tests --- .../src/undici.ts | 15 ++++++++++----- .../test/fetch.test.ts | 7 ++++++- .../test/utils/mock-server.ts | 4 +++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 615eedcf224..adbef55ee27 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -20,7 +20,7 @@ import { InstrumentationBase } from '@opentelemetry/instrumentation'; import { Attributes, context, - Meter, + diag, propagation, Span, SpanKind, @@ -62,13 +62,18 @@ export class UndiciInstrumentation extends InstrumentationBase { private _requestHook: UndiciInstrumentationConfig['onRequest']; - // @ts-expect-error -- we are no reading its value in this - override meter: Meter; - constructor(config?: UndiciInstrumentationConfig) { super('@opentelemetry/instrumentation-undici', VERSION, config); // Force load fetch API (since it's lazy loaded in Node 18) - fetch('').catch(() => {}); + // `fetch` Added in: v17.5.0, v16.15.0 (with flag) and we suport lower verisons + // https://nodejs.org/api/globals.html#fetch + try { + fetch('').catch(() => {}); + } catch (err) { + // TODO: nicer message + diag.info(`fetch API not available`); + } + this.setConfig(config); } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 1e7f13c0d42..1ff3423a405 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -41,7 +41,12 @@ const provider = new NodeTracerProvider(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); instrumentation.setTracerProvider(provider); -describe('UndiciInstrumentation `fetch` tests', () => { +// Simpler way to skip the while suite +// also `this` is not providing the skpi method inside tests +const shouldTest = typeof globalThis.fetch === 'function' +const describeFn = shouldTest ? describe : describe.skip; + +describeFn('UndiciInstrumentation `fetch` tests', () => { before(done => { mockServer.start(done); }); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts index 30b8e641674..04744cbce32 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts @@ -62,6 +62,8 @@ export class MockServer { } stop(cb: (err?: Error) => void) { - this._httpServer.close(cb); + if (this._httpServer) { + this._httpServer.close(cb); + } } } From 4a769eb7e4f6fa442057c2faa190243abaf0085d Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 28 Dec 2023 18:27:25 +0100 Subject: [PATCH 08/28] chore(instrumentation-undici): fix readme linting --- .../packages/opentelemetry-instrumentation-undici/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/README.md b/experimental/packages/opentelemetry-instrumentation-undici/README.md index ace387920b2..f3fb8a8d203 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/README.md +++ b/experimental/packages/opentelemetry-instrumentation-undici/README.md @@ -7,7 +7,6 @@ This module provides automatic instrumentation for [`undici`](https://undici.nodejs.org/) and [`fetch`](https://nodejs.org/docs/latest/api/globals.html#fetch). - ## Installation ```bash @@ -53,7 +52,6 @@ Undici instrumentation has few options available to choose from. You can set the | ------- | ---- | ----------- | | [`onRequest`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts#19) | `UndiciRequestHook` | Function for adding custom attributes before request is handled | - ## Useful links - For more information on OpenTelemetry, visit: From 092b9c1a40cd2032755c236dd1c33fd9ba12f4da Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 4 Jan 2024 19:12:58 +0100 Subject: [PATCH 09/28] chore(instrumentation-undici): add new span assertions --- .../src/enums/AttributeNames.ts | 24 +++ .../src/undici.ts | 7 +- .../test/fetch.test.ts | 54 +++--- .../test/utils/assertSpan.ts | 155 ++++++++++++++++++ .../test/utils/mock-server.ts | 3 +- 5 files changed, 213 insertions(+), 30 deletions(-) create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts new file mode 100644 index 00000000000..f9b8be3c8ea --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md + */ +export enum AttributeNames { + HTTP_ERROR_NAME = 'http.error_name', + HTTP_ERROR_MESSAGE = 'http.error_message', + HTTP_STATUS_TEXT = 'http.status_text', +} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index adbef55ee27..9f739343dae 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -91,14 +91,11 @@ export class UndiciInstrumentation extends InstrumentationBase { if (this._config.enabled) { return; } - // This methos is called by the `InstrumentationAbstract` constructor before + // This method is called by the `InstrumentationAbstract` constructor before // ours is called. So we need to ensure the property is initalized this._channelSubs = this._channelSubs || []; this.subscribeToChannel('undici:request:create', this.onRequest.bind(this)); - this.subscribeToChannel( - 'undici:request:headers', - this.onHeaders.bind(this) - ); + this.subscribeToChannel('undici:request:headers',this.onHeaders.bind(this)); this.subscribeToChannel('undici:request:trailers', this.onDone.bind(this)); this.subscribeToChannel('undici:request:error', this.onError.bind(this)); } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 1ff3423a405..398dc74cfab 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -28,6 +28,7 @@ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { UndiciInstrumentation } from '../src/undici'; import { MockServer } from './utils/mock-server'; +import { assertSpan } from './utils/assertSpan'; const instrumentation = new UndiciInstrumentation(); instrumentation.enable(); @@ -41,44 +42,41 @@ const provider = new NodeTracerProvider(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); instrumentation.setTracerProvider(provider); -// Simpler way to skip the while suite -// also `this` is not providing the skpi method inside tests -const shouldTest = typeof globalThis.fetch === 'function' -const describeFn = shouldTest ? describe : describe.skip; - -describeFn('UndiciInstrumentation `fetch` tests', () => { - before(done => { - mockServer.start(done); - }); - - after(done => { - mockServer.stop(done); - }); - - beforeEach(() => { - memoryExporter.reset(); - }); - - before(() => { +describe('UndiciInstrumentation `fetch` tests', function () { + before(function (done) { + // Do not test if the `fetch` global API is not available + // This applies to nodejs < v18 or nodejs < v16.15 wihtout the flag + // `--experimental-global-fetch` set + // https://nodejs.org/api/globals.html#fetch + if (typeof globalThis.fetch !== 'function') { + this.skip(); + } + // TODO: mock propagation and test it // propagation.setGlobalPropagator(new DummyPropagation()); context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + mockServer.start(done); }); - after(() => { + after(function(done) { context.disable(); propagation.disable(); + mockServer.stop(done); }); - describe('enable()', () => { - before(() => { + beforeEach(function () { + memoryExporter.reset(); + }); + + describe('enable()', function () { + before(function () { instrumentation.enable(); }); - after(() => { + after(function () { instrumentation.disable(); }); - it('should create a rootSpan for GET requests and add propagation headers', async () => { + it('should create a rootSpan for GET requests and add propagation headers', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -99,6 +97,14 @@ describeFn('UndiciInstrumentation `fetch` tests', () => { [SemanticAttributes.HTTP_STATUS_CODE]: response.status, [SemanticAttributes.HTTP_TARGET]: '/?query=test', }); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: response.status, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: response.headers, + }); }); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts new file mode 100644 index 00000000000..b139ad4a7ee --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -0,0 +1,155 @@ +/* + * 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. + */ +import { + SpanKind, + SpanStatus, + Exception, + SpanStatusCode, +} from '@opentelemetry/api'; +import { hrTimeToNanoseconds } from '@opentelemetry/core'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +// import { DummyPropagation } from './DummyPropagation'; +import { AttributeNames } from '../../src/enums/AttributeNames'; + +export const assertSpan = ( + span: ReadableSpan, + validations: { + httpStatusCode?: number; + httpMethod: string; + resHeaders: Headers; + hostname: string; + pathname: string; + reqHeaders?: Headers; + path?: string | null; + forceStatus?: SpanStatus; + serverName?: string; + noNetPeer?: boolean; // we don't expect net peer info when request throw before being sent + error?: Exception; + } +) => { + assert.strictEqual(span.spanContext().traceId.length, 32); + assert.strictEqual(span.spanContext().spanId.length, 16); + assert.strictEqual(span.kind, SpanKind.CLIENT, 'span.kind is correct'); + assert.strictEqual(span.name, `HTTP ${validations.httpMethod}`, 'span.name is correct'); + assert.strictEqual( + span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], + span.status.message, + `attributes['${AttributeNames.HTTP_ERROR_MESSAGE}'] is correct`, + ); + assert.strictEqual( + span.attributes[SemanticAttributes.HTTP_METHOD], + validations.httpMethod, + `attributes['${SemanticAttributes.HTTP_METHOD}'] is correct`, + ); + assert.strictEqual( + span.attributes[SemanticAttributes.HTTP_TARGET], + validations.path || validations.pathname, + `attributes['${SemanticAttributes.HTTP_TARGET}'] is correct`, + ); + assert.strictEqual( + span.attributes[SemanticAttributes.HTTP_STATUS_CODE], + validations.httpStatusCode + ); + + assert.strictEqual(span.links.length, 0); + + if (validations.error) { + assert.strictEqual(span.events.length, 1); + assert.strictEqual(span.events[0].name, 'exception'); + + const eventAttributes = span.events[0].attributes; + assert.ok(eventAttributes != null); + assert.deepStrictEqual(Object.keys(eventAttributes), [ + 'exception.type', + 'exception.message', + 'exception.stacktrace', + ]); + } else { + assert.strictEqual(span.events.length, 0); + } + + const { httpStatusCode } = validations; + const isStatusUnset = httpStatusCode && httpStatusCode >= 100 && httpStatusCode < 400; + + assert.deepStrictEqual( + span.status, + validations.forceStatus || { + code: isStatusUnset ? SpanStatusCode.UNSET : SpanStatusCode.ERROR + }, + ); + + assert.ok(span.endTime, 'must be finished'); + assert.ok(hrTimeToNanoseconds(span.duration) > 0, 'must have positive duration'); + + const contentLengthHeader = validations.resHeaders.get('content-length'); + if (contentLengthHeader) { + const contentLength = Number(contentLengthHeader); + + const contentEncodingHeader = validations.resHeaders.get('content-encoding'); + if ( + contentEncodingHeader && + contentEncodingHeader !== 'identity' + ) { + assert.strictEqual( + span.attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH], + contentLength + ); + } else { + assert.strictEqual( + span.attributes[ + SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED + ], + contentLength + ); + } + } + assert.strictEqual( + span.attributes[SemanticAttributes.NET_PEER_NAME], + validations.hostname, + 'must be consistent (PEER_NAME and hostname)' + ); + if (!validations.noNetPeer) { + assert.ok( + span.attributes[SemanticAttributes.NET_PEER_IP], + 'must have PEER_IP' + ); + assert.ok( + span.attributes[SemanticAttributes.NET_PEER_PORT], + 'must have PEER_PORT' + ); + } + assert.ok( + (span.attributes[SemanticAttributes.HTTP_URL] as string).indexOf( + span.attributes[SemanticAttributes.NET_PEER_NAME] as string + ) > -1, + 'must be consistent' + ); + + + if (validations.reqHeaders) { + const userAgent = validations.reqHeaders.get('user-agent'); + if (userAgent) { + assert.strictEqual( + span.attributes[SemanticAttributes.HTTP_USER_AGENT], + userAgent + ); + } + // assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); + // assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); + } +}; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts index 04744cbce32..52a645ed85f 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts @@ -63,7 +63,8 @@ export class MockServer { stop(cb: (err?: Error) => void) { if (this._httpServer) { - this._httpServer.close(cb); + this._httpServer.close(); + cb(); } } } From af72d4d98bff0b63604190c0c9d30a1b1fc760cb Mon Sep 17 00:00:00 2001 From: David Luna Date: Wed, 17 Jan 2024 16:47:00 +0100 Subject: [PATCH 10/28] chore(instrumentation-undici): add instrumentation config --- .../src/internal-types.ts | 20 +- .../src/types.ts | 56 +++++- .../src/undici.ts | 186 +++++++++++++----- .../test/fetch.test.ts | 154 +++++++++++---- .../test/utils/assertSpan.ts | 17 +- .../test/utils/mock-server.ts | 22 ++- 6 files changed, 335 insertions(+), 120 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts index 89077065c40..4206664c565 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts @@ -15,7 +15,7 @@ */ import type { Channel } from 'diagnostics_channel'; -import type { Request, RequestInfo, Response } from 'undici'; +import { UndiciRequest, UnidiciResponse }from './types'; export interface ListenerRecord { name: string; @@ -23,27 +23,13 @@ export interface ListenerRecord { onMessage: (message: any, name: string) => void; } -// type Writeable = { -readonly [P in keyof T]: T[P] }; -// type WriteableRequest = Writeable; - -// TODO: the actual `request` object at runtime have subtle differences -// from the `Request` type declared in `undici`. Type properly -// -// Types declared in the lib -// - have some properties declared as `readonly` but we are changing them -// - omits some properties we need to inspect for the instrumentation -type UndiciRequest = Request & { - origin: RequestInfo; - path: string; -}; - export interface RequestMessage { request: UndiciRequest; } -export interface RequestResponseMessage { +export interface HeadersMessage { request: UndiciRequest; - response: Response; + response: UnidiciResponse; } export interface RequestErrorMessage { diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts index 576e03e6e11..81f899453ec 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; -import type { Span } from '@opentelemetry/api'; +import type { Attributes, Span } from '@opentelemetry/api'; export type UndiciRequestHook = (args: { request: RequestType; @@ -22,8 +22,56 @@ export type UndiciRequestHook = (args: { additionalHeaders: Record; }) => void; -// TODO: This package will instrument HTTP requests made through Undici +// TODO: notes about support +// - `fetch` API is added in node v16.15.0 +// - `undici` supports node >=18 + + +// TODO: `Request` class was added in node v16.15.0, make it work with v14 +// also we do not get that object from the diagnostics channel message but the +// core request from https://github.com/nodejs/undici/blob/main/lib/core/request.js +// which is not typed + + +export interface UndiciRequest { + origin: string; + method: string; + path: string; + /** + * Serialized string of headers in the form `name: value\r\n` + */ + headers: string; + throwOnError: boolean; + completed: boolean; + aborted: boolean; + idempotent: boolean; + contentLength: number | null; + contentType: string | null; + body: any; +} + +export interface UnidiciResponse { + headers: Buffer[]; + statusCode: number; +} + + +// This package will instrument HTTP requests made through `undici` or `fetch` global API // so it seems logical to have similar options than the HTTP instrumentation -export interface UndiciInstrumentationConfig extends InstrumentationConfig { - onRequest?: UndiciRequestHook; +export interface UndiciInstrumentationConfig extends InstrumentationConfig { + /** Not trace all outgoing requests that matched with custom function */ + ignoreRequestHook?: (request: RequestType) => boolean; + /** Function for adding custom attributes after response is handled */ + applyCustomAttributesOnSpan?: (span: Span, request: RequestType, response: Response) => void; + /** Function for adding custom attributes before request is handled */ + requestHook?: (span: Span, request: RequestType) => void; + /** Function for adding custom attributes before a span is started in outgoingRequest */ + startSpanHook?: (request: RequestType) => Attributes; + /** Require parent to create span for outgoing requests */ + requireParentforSpans?: boolean; + /** Map the following HTTP headers to span attributes. */ + headersToSpanAttributes?: { + requestHeaders?: string[]; + responseHeaders?: string[]; + }; } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 9f739343dae..efad7038061 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import * as diagch from 'diagnostics_channel'; +import { URL } from 'url'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { InstrumentationBase } from '@opentelemetry/instrumentation'; +import { InstrumentationBase, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; import { Attributes, context, @@ -30,26 +31,9 @@ import { import { VERSION } from './version'; -import { ListenerRecord } from './internal-types'; -import { UndiciInstrumentationConfig } from './types'; - -// Get the content-length from undici response headers. -// `headers` is an Array of buffers: [k, v, k, v, ...]. -// If the header is not present, or has an invalid value, this returns null. -function contentLengthFromResponseHeaders(headers: Buffer[]) { - const name = 'content-length'; - for (let i = 0; i < headers.length; i += 2) { - const k = headers[i]; - if (k.length === name.length && k.toString().toLowerCase() === name) { - const v = Number(headers[i + 1]); - if (!Number.isNaN(Number(v))) { - return v; - } - return undefined; - } - } - return undefined; -} +import { HeadersMessage, ListenerRecord, RequestMessage } from './internal-types'; +import { UndiciInstrumentationConfig, UndiciRequest } from './types'; + // A combination of https://github.com/elastic/apm-agent-nodejs and // https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-undici/src/index.ts @@ -58,9 +42,7 @@ export class UndiciInstrumentation extends InstrumentationBase { // unsubscribing. private _channelSubs!: Array; - private _spanFromReq = new WeakMap(); - - private _requestHook: UndiciInstrumentationConfig['onRequest']; + private _spanFromReq = new WeakMap(); constructor(config?: UndiciInstrumentationConfig) { super('@opentelemetry/instrumentation-undici', VERSION, config); @@ -83,29 +65,44 @@ export class UndiciInstrumentation extends InstrumentationBase { } override disable(): void { + if (!this._config.enabled) { + return; + } + this._channelSubs.forEach(sub => sub.channel.unsubscribe(sub.onMessage)); this._channelSubs.length = 0; + this._config.enabled = false; } override enable(): void { if (this._config.enabled) { return; } + this._config.enabled = true; + // This method is called by the `InstrumentationAbstract` constructor before // ours is called. So we need to ensure the property is initalized this._channelSubs = this._channelSubs || []; - this.subscribeToChannel('undici:request:create', this.onRequest.bind(this)); - this.subscribeToChannel('undici:request:headers',this.onHeaders.bind(this)); + this.subscribeToChannel('undici:request:create', this.onRequestCreated.bind(this)); + this.subscribeToChannel('undici:client:sendHeaders',this.onRequestHeaders.bind(this)); + this.subscribeToChannel('undici:request:headers',this.onResponseHeaders.bind(this)); this.subscribeToChannel('undici:request:trailers', this.onDone.bind(this)); this.subscribeToChannel('undici:request:error', this.onError.bind(this)); } override setConfig(config?: UndiciInstrumentationConfig): void { super.setConfig(config); - if (typeof config?.onRequest === 'function') { - this._requestHook = config.onRequest; + + if (config?.enabled) { + this.enable(); + } else { + this.disable(); } } + + private _getConfig(): UndiciInstrumentationConfig { + return this._config as UndiciInstrumentationConfig; + } private subscribeToChannel( diagnosticChannel: string, @@ -120,27 +117,79 @@ export class UndiciInstrumentation extends InstrumentationBase { }); } - private onRequest({ request }: any): void { - // We do not handle instrumenting HTTP CONNECT. See limitation notes above. - if (request.method === 'CONNECT') { + // This is the 1st message we receive for each request (fired after request creation). Here we will + // create the span and populate some atttributes, then link the span to the request for further + // span processing + private onRequestCreated({ request }: RequestMessage): void { + console.log('onRequestCreated') + // Ignore if: + // - instrumentation is disabled + // - ignored by config + // - method is 'CONNECT' (TODO: check for limitations) + const config = this._getConfig(); + const shouldIgnoreReq = safeExecuteInTheMiddle( + () => !config.enabled || request.method === 'CONNECT' || config.ignoreRequestHook?.(request), + (e) => e && this._diag.error('caught ignoreRequestHook error: ', e), + true, + ); + + if (shouldIgnoreReq) { return; } + const requestUrl = new URL(request.origin); + const spanAttributes = { + [SemanticAttributes.HTTP_URL]: request.origin, + [SemanticAttributes.HTTP_METHOD]: request.method, + [SemanticAttributes.HTTP_TARGET]: request.path || '/', + [SemanticAttributes.NET_PEER_NAME]: requestUrl.hostname, + }; + + const rawHeaders = request.headers.split('\r\n'); + const reqHeaders = new Map(rawHeaders.map(h => { + const sepIndex = h.indexOf(':'); + const name = h.substring(0, sepIndex).toLowerCase(); + const val = h.substring(sepIndex + 1).trim(); + return [name, val]; + })); + + let hostAttribute = reqHeaders.get('host'); + + if (!hostAttribute) { + const protocolPorts: Record = { https: '443', http: '80' }; + const defaultPort = protocolPorts[requestUrl.protocol] || ''; + const port = requestUrl.port || defaultPort; + + hostAttribute = requestUrl.hostname; + if (port) { + hostAttribute += `:${port}`; + } + } + spanAttributes[SemanticAttributes.HTTP_HOST] = hostAttribute; + + const userAgent = reqHeaders.get('user-agent'); + if (userAgent) { + spanAttributes[SemanticAttributes.HTTP_USER_AGENT] = userAgent; + } + const span = this.tracer.startSpan(`HTTP ${request.method}`, { kind: SpanKind.CLIENT, - attributes: { - [SemanticAttributes.HTTP_URL]: String(request.origin), - [SemanticAttributes.HTTP_METHOD]: request.method, - [SemanticAttributes.HTTP_TARGET]: request.path, - }, + attributes: spanAttributes, }); + + // TODO: add headers based on config + + // Context propagation const requestContext = trace.setSpan(context.active(), span); const addedHeaders: Record = {}; propagation.inject(requestContext, addedHeaders); - if (this._requestHook) { - this._requestHook({ request, span, additionalHeaders: addedHeaders }); - } + // Execute the request hook if defined + safeExecuteInTheMiddle( + () => this._getConfig().requestHook?.(span, request), + (e) => e && this._diag.error('caught requestHook error: ', e), + true, + ); request.headers += Object.entries(addedHeaders) .map(([k, v]) => `${k}: ${v}\r\n`) @@ -148,31 +197,65 @@ export class UndiciInstrumentation extends InstrumentationBase { this._spanFromReq.set(request, span); } - private onHeaders({ request, response }: any): void { + // This is the 2nd message we recevie for each request. It is fired when connection with + // the remote is stablished and abut to send the first byte. Here do have info about the + // remote addres an port sowe can poupulate some `net.*` attributes into the span + private onRequestHeaders({ request, socket }: any): void { + console.log('onRequestHeaders') + const span = this._spanFromReq.get(request as UndiciRequest); + + if (span) { + const { remoteAddress, remotePort } = socket; + + span.setAttributes({ + [SemanticAttributes.NET_PEER_IP]: remoteAddress, + [SemanticAttributes.NET_PEER_PORT]: remotePort, + }); + } + } + + // This is the 3rd message we get for each request and it's fired when the server + // headers are received, body may not be accessible yet (TODO: check this). + // From the response headers we can set the status and content length + private onResponseHeaders({ request, response }: HeadersMessage): void { + console.log('onResponseHeaders') const span = this._spanFromReq.get(request); if (span !== undefined) { // We are currently *not* capturing response headers, even though the // intake API does allow it, because none of the other `setHttpContext` - // uses currently do. - - const cLen = contentLengthFromResponseHeaders(response.headers); + // uses currently do const attrs: Attributes = { [SemanticAttributes.HTTP_STATUS_CODE]: response.statusCode, }; - if (cLen) { - attrs[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = cLen; + + // Get headers with names lowercased but values intact + const resHeaders = response.headers.map((h, idx) => { + const isName = idx % 2 === 0; + const result = h.toString(); + + return isName ? result.toLowerCase() : result; + }); + + // TODO: capture headers based on config + + const contentLengthIndex = resHeaders.findIndex(h => h === 'content-length'); + const contentLength = Number(contentLengthIndex === -1 ? undefined : resHeaders[contentLengthIndex + 1]); + if (!isNaN(contentLength)) { + attrs[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = contentLength; } + span.setAttributes(attrs); span.setStatus({ - code: - response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.OK, - message: String(response.statusCode), + code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET, }); } } + + // This is the last event we receive if the request went without any errors (TODO: check this) private onDone({ request }: any): void { + console.log('onDone') const span = this._spanFromReq.get(request); if (span !== undefined) { span.end(); @@ -180,6 +263,13 @@ export class UndiciInstrumentation extends InstrumentationBase { } } + // TODO: check this + // This messge si triggered if there is any error in the request + // TODO: in `undici@6.3.0` when request aborted the error type changes from + // a custom error (`RequestAbortedError`) to a built-in `DOMException` so + // - `code` is from DOMEXception (ABORT_ERR: 20) + // - `message` changes + // - stacktrace is smaller and contains node internal frames private onError({ request, error }: any): void { const span = this._spanFromReq.get(request); if (span !== undefined) { diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 398dc74cfab..35524c794d2 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -15,12 +15,10 @@ */ import * as assert from 'assert'; -import { SpanKind, context, propagation } from '@opentelemetry/api'; +import { context, propagation } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; -import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { InMemorySpanExporter, - ReadableSpan, SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; @@ -61,6 +59,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { after(function(done) { context.disable(); propagation.disable(); + mockServer.mockListener(undefined); mockServer.stop(done); }); @@ -69,34 +68,69 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); describe('enable()', function () { - before(function () { + beforeEach(function () { instrumentation.enable(); }); - after(function () { + afterEach(function () { instrumentation.disable(); }); - it('should create a rootSpan for GET requests and add propagation headers', async function () { + it.skip('should create valid spans even if the configuration hooks fail', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); + // Set the bad configuration + instrumentation.setConfig({ + enabled: true, + ignoreRequestHook: () => { + throw new Error('ignoreRequestHook error'); + }, + applyCustomAttributesOnSpan: () => { + throw new Error('ignoreRequestHook error'); + }, + requestHook: () => { + throw new Error('requestHook error'); + }, + startSpanHook: () => { + throw new Error('startSpanHook error'); + }, + }) + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const response = await fetch(fetchUrl); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; - assert.ok(span); + assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); - assertSpanAttribs(span, 'HTTP GET', { - // TODO: I guess we want to have parity with HTTP insturmentation - // - there are missing attributes - // - also check if these current values make sense - [SemanticAttributes.HTTP_URL]: `${protocol}://${hostname}:${mockServer.port}`, - [SemanticAttributes.HTTP_METHOD]: 'GET', - [SemanticAttributes.HTTP_STATUS_CODE]: response.status, - [SemanticAttributes.HTTP_TARGET]: '/?query=test', + // console.dir(span, { depth: 9 }); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: response.status, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + resHeaders: response.headers, }); + }); + + it.skip('should create valid spans with empty configuration', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Empty configuration + instrumentation.setConfig({ enabled: true }); + + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const response = await fetch(fetchUrl); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + // console.dir(span, { depth: 9 }); assertSpan(span, { hostname: 'localhost', httpStatusCode: response.status, @@ -106,24 +140,76 @@ describe('UndiciInstrumentation `fetch` tests', function () { resHeaders: response.headers, }); }); + + it('should create valid spans with the given configuration configuration', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Empty configuration + instrumentation.setConfig({ + enabled: true, + ignoreRequestHook: (req) => { + return req.path.indexOf('/ignore/path') !== -1; + }, + requestHook: (span, req) => { + // TODO: maybe an intermediate request with better API + req.headers += `x-requested-with: undici instrumentation\r\n`; + }, + startSpanHook: (request) => { + return { + 'test.request.origin': request.origin, + 'test.request.headers.lengh': request.headers.split('\r\n').length, + }; + }, + headersToSpanAttributes: { + requestHeaders: ['foo-client', 'test.foo.client'], + responseHeaders: ['foo-server', 'test.foo.server'] + } + }); + + // Add some extra headers in the response + mockServer.mockListener((req, res) => { + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.setHeader('foo-server', 'bar'); + res.write(JSON.stringify({ success: true })); + res.end(); + }); + + // Do some requests + await fetch(`${protocol}://${hostname}:${mockServer.port}/ignore/path`); + const reqInit = { + headers: new Headers({ + 'user-agent': 'custom', + 'foo-client': 'bar' + }), + }; + const response = await fetch(`${protocol}://${hostname}:${mockServer.port}/?query=test`, reqInit); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + console.dir(span, { depth: 9 }); + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: response.status, + httpMethod: 'GET', + pathname: '/', + path: '/?query=test', + reqHeaders: reqInit.headers, + resHeaders: response.headers, + }); + assert.strictEqual( + span.attributes['test.foo.client'], + 'bar', + `request headers are captured`, + ); + assert.strictEqual( + span.attributes['test.foo.server'], + 'bar', + `response headers are captured`, + ); + }); }); }); - -function assertSpanAttribs( - span: ReadableSpan, - name: string, - attribs: Record -) { - assert.strictEqual(span.spanContext().traceId.length, 32); - assert.strictEqual(span.spanContext().spanId.length, 16); - assert.strictEqual(span.kind, SpanKind.CLIENT); - assert.strictEqual(span.name, name); - - for (const [key, value] of Object.entries(attribs)) { - assert.strictEqual( - span.attributes[key], - value, - `expected value "${value}" but got "${span.attributes[key]}" for attribute "${key}" ` - ); - } -} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index b139ad4a7ee..7dc5eac866f 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -37,7 +37,6 @@ export const assertSpan = ( reqHeaders?: Headers; path?: string | null; forceStatus?: SpanStatus; - serverName?: string; noNetPeer?: boolean; // we don't expect net peer info when request throw before being sent error?: Exception; } @@ -63,24 +62,25 @@ export const assertSpan = ( ); assert.strictEqual( span.attributes[SemanticAttributes.HTTP_STATUS_CODE], - validations.httpStatusCode + validations.httpStatusCode, + `attributes['${SemanticAttributes.HTTP_STATUS_CODE}'] is correct`, ); - assert.strictEqual(span.links.length, 0); + assert.strictEqual(span.links.length, 0, 'there are no links'); if (validations.error) { - assert.strictEqual(span.events.length, 1); - assert.strictEqual(span.events[0].name, 'exception'); + assert.strictEqual(span.events.length, 1, 'span contains one error event'); + assert.strictEqual(span.events[0].name, 'exception', 'error event name is correct'); const eventAttributes = span.events[0].attributes; - assert.ok(eventAttributes != null); + assert.ok(eventAttributes != null, 'event has attributes'); assert.deepStrictEqual(Object.keys(eventAttributes), [ 'exception.type', 'exception.message', 'exception.stacktrace', - ]); + ], 'the event attribute names are correct'); } else { - assert.strictEqual(span.events.length, 0); + assert.strictEqual(span.events.length, 0, 'span contains no events'); } const { httpStatusCode } = validations; @@ -91,6 +91,7 @@ export const assertSpan = ( validations.forceStatus || { code: isStatusUnset ? SpanStatusCode.UNSET : SpanStatusCode.ERROR }, + 'span status is correct' ); assert.ok(span.endTime, 'must be finished'); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts index 52a645ed85f..db47cea2904 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts @@ -15,28 +15,31 @@ */ import * as http from 'http'; + export class MockServer { private _port: number | undefined; private _httpServer: http.Server | undefined; + private _reqListener: http.RequestListener | undefined; get port(): number { return this._port || 0; } + mockListener(handler: http.RequestListener | undefined): void { + this._reqListener = handler; + } + start(cb: (err?: Error) => void) { this._httpServer = http.createServer((req, res) => { - if (req.url === '/timeout') { - setTimeout(() => { - res.end(); - }, 1000); + // Use the mock listener if defined + if (typeof this._reqListener === 'function') { + return this._reqListener(req, res); } + + // If no mock function is provided fallback to a basic response res.statusCode = 200; res.setHeader('content-type', 'application/json'); - res.write( - JSON.stringify({ - success: true, - }) - ); + res.write(JSON.stringify({ success: true })); res.end(); }); @@ -63,6 +66,7 @@ export class MockServer { stop(cb: (err?: Error) => void) { if (this._httpServer) { + this._reqListener = undefined; this._httpServer.close(); cb(); } From 057206f1d946c87271e8c8df317cb2aaf3e6fd25 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 22 Jan 2024 19:44:39 +0100 Subject: [PATCH 11/28] chore(instrumentation-undici): add tests for request/response headers config --- .../src/undici.ts | 55 +++++++++++++------ .../test/fetch.test.ts | 13 +++-- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index efad7038061..7b875e394b6 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -152,9 +152,8 @@ export class UndiciInstrumentation extends InstrumentationBase { const val = h.substring(sepIndex + 1).trim(); return [name, val]; })); - - let hostAttribute = reqHeaders.get('host'); + let hostAttribute = reqHeaders.get('host'); if (!hostAttribute) { const protocolPorts: Record = { https: '443', http: '80' }; const defaultPort = protocolPorts[requestUrl.protocol] || ''; @@ -172,13 +171,24 @@ export class UndiciInstrumentation extends InstrumentationBase { spanAttributes[SemanticAttributes.HTTP_USER_AGENT] = userAgent; } + // Put headers as attributes based on config + if (config.headersToSpanAttributes?.requestHeaders) { + config.headersToSpanAttributes.requestHeaders.forEach((name) => { + const headerName = name.toLowerCase(); + const headerValue = reqHeaders.get(headerName); + + if (headerValue) { + const normalizedName = headerName.replace(/-/g, '_'); + spanAttributes[`http.request.header.${normalizedName}`] = headerValue; + } + }); + } + const span = this.tracer.startSpan(`HTTP ${request.method}`, { kind: SpanKind.CLIENT, attributes: spanAttributes, }); - // TODO: add headers based on config - // Context propagation const requestContext = trace.setSpan(context.active(), span); const addedHeaders: Record = {}; @@ -186,7 +196,7 @@ export class UndiciInstrumentation extends InstrumentationBase { // Execute the request hook if defined safeExecuteInTheMiddle( - () => this._getConfig().requestHook?.(span, request), + () => config.requestHook?.(span, request), (e) => e && this._diag.error('caught requestHook error: ', e), true, ); @@ -198,8 +208,8 @@ export class UndiciInstrumentation extends InstrumentationBase { } // This is the 2nd message we recevie for each request. It is fired when connection with - // the remote is stablished and abut to send the first byte. Here do have info about the - // remote addres an port sowe can poupulate some `net.*` attributes into the span + // the remote is stablished and about to send the first byte. Here do have info about the + // remote addres an port so we can poupulate some `net.*` attributes into the span private onRequestHeaders({ request, socket }: any): void { console.log('onRequestHeaders') const span = this._spanFromReq.get(request as UndiciRequest); @@ -207,6 +217,7 @@ export class UndiciInstrumentation extends InstrumentationBase { if (span) { const { remoteAddress, remotePort } = socket; + // TODO: this may be affected by HTTP semconv breaking changes span.setAttributes({ [SemanticAttributes.NET_PEER_IP]: remoteAddress, [SemanticAttributes.NET_PEER_PORT]: remotePort, @@ -230,17 +241,29 @@ export class UndiciInstrumentation extends InstrumentationBase { }; // Get headers with names lowercased but values intact - const resHeaders = response.headers.map((h, idx) => { - const isName = idx % 2 === 0; - const result = h.toString(); - - return isName ? result.toLowerCase() : result; - }); + const resHeaders = new Map(); + for (let idx = 0; idx < response.headers.length; idx = idx + 2) { + resHeaders.set( + response.headers[idx].toString().toLowerCase(), + response.headers[idx + 1].toString(), + ); + } - // TODO: capture headers based on config + // Put response headers as attributes based on config + const config = this._getConfig(); + if (config.headersToSpanAttributes?.responseHeaders) { + config.headersToSpanAttributes.responseHeaders.forEach((name) => { + const headerName = name.toLowerCase(); + const headerValue = resHeaders.get(headerName); + + if (headerValue) { + const normalizedName = headerName.replace(/-/g, '_'); + attrs[`http.response.header.${normalizedName}`] = headerValue; + } + }); + } - const contentLengthIndex = resHeaders.findIndex(h => h === 'content-length'); - const contentLength = Number(contentLengthIndex === -1 ? undefined : resHeaders[contentLengthIndex + 1]); + const contentLength = Number(resHeaders.get('content-length')); if (!isNaN(contentLength)) { attrs[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = contentLength; } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 35524c794d2..ded445efa7c 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -75,7 +75,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { instrumentation.disable(); }); - it.skip('should create valid spans even if the configuration hooks fail', async function () { + it('should create valid spans even if the configuration hooks fail', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -115,7 +115,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); }); - it.skip('should create valid spans with empty configuration', async function () { + it('should create valid spans with empty configuration', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -162,8 +162,8 @@ describe('UndiciInstrumentation `fetch` tests', function () { }; }, headersToSpanAttributes: { - requestHeaders: ['foo-client', 'test.foo.client'], - responseHeaders: ['foo-server', 'test.foo.server'] + requestHeaders: ['foo-client'], + responseHeaders: ['foo-server'], } }); @@ -188,6 +188,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { spans = memoryExporter.getFinishedSpans(); const span = spans[0]; + // TODO: remove this when test finished console.dir(span, { depth: 9 }); assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); @@ -201,12 +202,12 @@ describe('UndiciInstrumentation `fetch` tests', function () { resHeaders: response.headers, }); assert.strictEqual( - span.attributes['test.foo.client'], + span.attributes['http.request.header.foo_client'], 'bar', `request headers are captured`, ); assert.strictEqual( - span.attributes['test.foo.server'], + span.attributes['http.response.header.foo_server'], 'bar', `response headers are captured`, ); From 6035a89ea10d820db2a307a43ff70be8cbc8c7d0 Mon Sep 17 00:00:00 2001 From: David Luna Date: Tue, 23 Jan 2024 12:32:17 +0100 Subject: [PATCH 12/28] chore(instrumentation-undici): add createSpanHook handling --- .../src/types.ts | 5 - .../src/undici.ts | 148 ++++++++++-------- .../test/fetch.test.ts | 44 +++--- 3 files changed, 110 insertions(+), 87 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts index 81f899453ec..30dd2acc694 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts @@ -16,11 +16,6 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import type { Attributes, Span } from '@opentelemetry/api'; -export type UndiciRequestHook = (args: { - request: RequestType; - span: Span; - additionalHeaders: Record; -}) => void; // TODO: notes about support // - `fetch` API is added in node v16.15.0 diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 7b875e394b6..a6a8c6bc32f 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -137,14 +137,6 @@ export class UndiciInstrumentation extends InstrumentationBase { return; } - const requestUrl = new URL(request.origin); - const spanAttributes = { - [SemanticAttributes.HTTP_URL]: request.origin, - [SemanticAttributes.HTTP_METHOD]: request.method, - [SemanticAttributes.HTTP_TARGET]: request.path || '/', - [SemanticAttributes.NET_PEER_NAME]: requestUrl.hostname, - }; - const rawHeaders = request.headers.split('\r\n'); const reqHeaders = new Map(rawHeaders.map(h => { const sepIndex = h.indexOf(':'); @@ -153,6 +145,14 @@ export class UndiciInstrumentation extends InstrumentationBase { return [name, val]; })); + const requestUrl = new URL(request.origin); + const spanAttributes: Attributes = { + [SemanticAttributes.HTTP_URL]: request.origin, + [SemanticAttributes.HTTP_METHOD]: request.method, + [SemanticAttributes.HTTP_TARGET]: request.path || '/', + [SemanticAttributes.NET_PEER_NAME]: requestUrl.hostname, + }; + let hostAttribute = reqHeaders.get('host'); if (!hostAttribute) { const protocolPorts: Record = { https: '443', http: '80' }; @@ -184,6 +184,18 @@ export class UndiciInstrumentation extends InstrumentationBase { }); } + // Get attributes from the hook + const hookAttributes = safeExecuteInTheMiddle( + () => config.startSpanHook?.(request), + (e) => e && this._diag.error('caught startSpanHook error: ', e), + true, + ); + if (hookAttributes) { + Object.entries(hookAttributes).forEach(([key, val]) => { + spanAttributes[key] = val; + }) + } + const span = this.tracer.startSpan(`HTTP ${request.method}`, { kind: SpanKind.CLIENT, attributes: spanAttributes, @@ -214,15 +226,17 @@ export class UndiciInstrumentation extends InstrumentationBase { console.log('onRequestHeaders') const span = this._spanFromReq.get(request as UndiciRequest); - if (span) { - const { remoteAddress, remotePort } = socket; - - // TODO: this may be affected by HTTP semconv breaking changes - span.setAttributes({ - [SemanticAttributes.NET_PEER_IP]: remoteAddress, - [SemanticAttributes.NET_PEER_PORT]: remotePort, - }); + if (!span) { + return } + + const { remoteAddress, remotePort } = socket; + + // TODO: this may be affected by HTTP semconv breaking changes + span.setAttributes({ + [SemanticAttributes.NET_PEER_IP]: remoteAddress, + [SemanticAttributes.NET_PEER_PORT]: remotePort, + }); } // This is the 3rd message we get for each request and it's fired when the server @@ -232,47 +246,49 @@ export class UndiciInstrumentation extends InstrumentationBase { console.log('onResponseHeaders') const span = this._spanFromReq.get(request); - if (span !== undefined) { - // We are currently *not* capturing response headers, even though the - // intake API does allow it, because none of the other `setHttpContext` - // uses currently do - const attrs: Attributes = { - [SemanticAttributes.HTTP_STATUS_CODE]: response.statusCode, - }; - - // Get headers with names lowercased but values intact - const resHeaders = new Map(); - for (let idx = 0; idx < response.headers.length; idx = idx + 2) { - resHeaders.set( - response.headers[idx].toString().toLowerCase(), - response.headers[idx + 1].toString(), - ); - } + if (!span) { + return; + } - // Put response headers as attributes based on config - const config = this._getConfig(); - if (config.headersToSpanAttributes?.responseHeaders) { - config.headersToSpanAttributes.responseHeaders.forEach((name) => { - const headerName = name.toLowerCase(); - const headerValue = resHeaders.get(headerName); - - if (headerValue) { - const normalizedName = headerName.replace(/-/g, '_'); - attrs[`http.response.header.${normalizedName}`] = headerValue; - } - }); - } + // We are currently *not* capturing response headers, even though the + // intake API does allow it, because none of the other `setHttpContext` + // uses currently do + const attrs: Attributes = { + [SemanticAttributes.HTTP_STATUS_CODE]: response.statusCode, + }; - const contentLength = Number(resHeaders.get('content-length')); - if (!isNaN(contentLength)) { - attrs[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = contentLength; - } + // Get headers with names lowercased but values intact + const resHeaders = new Map(); + for (let idx = 0; idx < response.headers.length; idx = idx + 2) { + resHeaders.set( + response.headers[idx].toString().toLowerCase(), + response.headers[idx + 1].toString(), + ); + } - span.setAttributes(attrs); - span.setStatus({ - code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET, + // Put response headers as attributes based on config + const config = this._getConfig(); + if (config.headersToSpanAttributes?.responseHeaders) { + config.headersToSpanAttributes.responseHeaders.forEach((name) => { + const headerName = name.toLowerCase(); + const headerValue = resHeaders.get(headerName); + + if (headerValue) { + const normalizedName = headerName.replace(/-/g, '_'); + attrs[`http.response.header.${normalizedName}`] = headerValue; + } }); } + + const contentLength = Number(resHeaders.get('content-length')); + if (!isNaN(contentLength)) { + attrs[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = contentLength; + } + + span.setAttributes(attrs); + span.setStatus({ + code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET, + }); } @@ -280,14 +296,17 @@ export class UndiciInstrumentation extends InstrumentationBase { private onDone({ request }: any): void { console.log('onDone') const span = this._spanFromReq.get(request); - if (span !== undefined) { - span.end(); - this._spanFromReq.delete(request); + + if (!span) { + return; } + + span.end(); + this._spanFromReq.delete(request); } // TODO: check this - // This messge si triggered if there is any error in the request + // This messge is triggered if there is any error in the request // TODO: in `undici@6.3.0` when request aborted the error type changes from // a custom error (`RequestAbortedError`) to a built-in `DOMException` so // - `code` is from DOMEXception (ABORT_ERR: 20) @@ -295,13 +314,16 @@ export class UndiciInstrumentation extends InstrumentationBase { // - stacktrace is smaller and contains node internal frames private onError({ request, error }: any): void { const span = this._spanFromReq.get(request); - if (span !== undefined) { - span.recordException(error); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message, - }); - span.end(); + + if (!span) { + return; } + + span.recordException(error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); + span.end(); } } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index ded445efa7c..c1c91e93637 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -54,6 +54,13 @@ describe('UndiciInstrumentation `fetch` tests', function () { // propagation.setGlobalPropagator(new DummyPropagation()); context.setGlobalContextManager(new AsyncHooksContextManager().enable()); mockServer.start(done); + mockServer.mockListener((req, res) => { + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.setHeader('foo-server', 'bar'); + res.write(JSON.stringify({ success: true })); + res.end(); + }); }); after(function(done) { @@ -130,7 +137,6 @@ describe('UndiciInstrumentation `fetch` tests', function () { assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); - // console.dir(span, { depth: 9 }); assertSpan(span, { hostname: 'localhost', httpStatusCode: response.status, @@ -141,11 +147,11 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); }); - it('should create valid spans with the given configuration configuration', async function () { + it('should create valid spans with the given configuration', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - // Empty configuration + // Set configuration instrumentation.setConfig({ enabled: true, ignoreRequestHook: (req) => { @@ -153,29 +159,19 @@ describe('UndiciInstrumentation `fetch` tests', function () { }, requestHook: (span, req) => { // TODO: maybe an intermediate request with better API - req.headers += `x-requested-with: undici instrumentation\r\n`; + req.headers += 'x-requested-with: undici\r\n'; }, startSpanHook: (request) => { return { - 'test.request.origin': request.origin, - 'test.request.headers.lengh': request.headers.split('\r\n').length, + 'test.hook.attribute': 'hook-value', }; }, headersToSpanAttributes: { - requestHeaders: ['foo-client'], + requestHeaders: ['foo-client', 'x-requested-with'], responseHeaders: ['foo-server'], } }); - // Add some extra headers in the response - mockServer.mockListener((req, res) => { - res.statusCode = 200; - res.setHeader('content-type', 'application/json'); - res.setHeader('foo-server', 'bar'); - res.write(JSON.stringify({ success: true })); - res.end(); - }); - // Do some requests await fetch(`${protocol}://${hostname}:${mockServer.port}/ignore/path`); const reqInit = { @@ -189,7 +185,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { spans = memoryExporter.getFinishedSpans(); const span = spans[0]; // TODO: remove this when test finished - console.dir(span, { depth: 9 }); + // console.dir(span, { depth: 9 }); assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); assertSpan(span, { @@ -204,12 +200,22 @@ describe('UndiciInstrumentation `fetch` tests', function () { assert.strictEqual( span.attributes['http.request.header.foo_client'], 'bar', - `request headers are captured`, + 'request headers are captured', + ); + assert.strictEqual( + span.attributes['http.request.header.foo_client'], + 'bar', + 'request headers from requestHook are captured', ); assert.strictEqual( span.attributes['http.response.header.foo_server'], 'bar', - `response headers are captured`, + 'response headers are captured', + ); + assert.strictEqual( + span.attributes['test.hook.attribute'], + 'hook-value', + 'startSpanHook is called', ); }); }); From f0cce12675272c8263a083c610fab0ee4ae3cbf2 Mon Sep 17 00:00:00 2001 From: David Luna Date: Tue, 23 Jan 2024 18:43:13 +0100 Subject: [PATCH 13/28] chore(instrumentation-undici): add disabled config test --- .../src/undici.ts | 32 +++++++++++++++---- .../test/fetch.test.ts | 32 +++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index a6a8c6bc32f..db15d1c7f6c 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -22,6 +22,7 @@ import { Attributes, context, diag, + INVALID_SPAN_CONTEXT, propagation, Span, SpanKind, @@ -184,7 +185,8 @@ export class UndiciInstrumentation extends InstrumentationBase { }); } - // Get attributes from the hook + // Get attributes from the hook if present + const hookAttributes = safeExecuteInTheMiddle( () => config.startSpanHook?.(request), (e) => e && this._diag.error('caught startSpanHook error: ', e), @@ -193,13 +195,31 @@ export class UndiciInstrumentation extends InstrumentationBase { if (hookAttributes) { Object.entries(hookAttributes).forEach(([key, val]) => { spanAttributes[key] = val; - }) + }); } - const span = this.tracer.startSpan(`HTTP ${request.method}`, { - kind: SpanKind.CLIENT, - attributes: spanAttributes, - }); + // TODO: check parent if added in config and: + // - create a span if confgi false + // - create a noop span if parent not present and config true + // If a parent is required but not present, we use a `NoopSpan` to still + // propagate context without recording it. + const activeCtx = context.active(); + const currentSpan = trace.getSpan(activeCtx); + let span: Span; + + + if (config.requireParentforSpans && !currentSpan) { + span = trace.wrapSpanContext(INVALID_SPAN_CONTEXT); + } else { + span = this.tracer.startSpan( + `HTTP ${request.method}`, + { + kind: SpanKind.CLIENT, + attributes: spanAttributes, + }, + activeCtx + ); + } // Context propagation const requestContext = trace.setSpan(context.active(), span); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index c1c91e93637..cba609be993 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -74,6 +74,22 @@ describe('UndiciInstrumentation `fetch` tests', function () { memoryExporter.reset(); }); + describe('disable()', function () { + it('should not create spans when disabled', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Disable via config + instrumentation.setConfig({ enabled: false }); + + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + await fetch(fetchUrl); + + spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0, 'no spans are created'); + }); + }); + describe('enable()', function () { beforeEach(function () { instrumentation.enable(); @@ -218,5 +234,21 @@ describe('UndiciInstrumentation `fetch` tests', function () { 'startSpanHook is called', ); }); + + it('should not create spans without parent if configured', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + instrumentation.setConfig({ + enabled: true, + requireParentforSpans: true, + }); + + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + await fetch(fetchUrl); + + spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0, 'no spans are created'); + }); }); }); From f42885bc46827e9f260018b85aaa436e44ed91a9 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 26 Jan 2024 11:18:56 +0100 Subject: [PATCH 14/28] chore: generate new semconv for undici package --- .../src/enums/AttributeNames.ts | 24 - .../src/enums/SemanticAttributes.ts | 2620 +++++++++++++++++ .../src/undici.ts | 83 +- scripts/semconv/generate.sh | 132 +- 4 files changed, 2752 insertions(+), 107 deletions(-) delete mode 100644 experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts deleted file mode 100644 index f9b8be3c8ea..00000000000 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/AttributeNames.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -/** - * https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md - */ -export enum AttributeNames { - HTTP_ERROR_NAME = 'http.error_name', - HTTP_ERROR_MESSAGE = 'http.error_message', - HTTP_STATUS_TEXT = 'http.status_text', -} diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts new file mode 100644 index 00000000000..d70ef67fa75 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts @@ -0,0 +1,2620 @@ +/* + * 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. + */ + +// DO NOT EDIT, this is an Auto-generated file from scripts/semconv/templates//templates/SemanticAttributes.ts.j2 +export const SemanticAttributes = { + + /** + * The name of the invoked function. + * + * Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. + */ + FAAS_INVOKED_NAME: 'faas.invoked_name', + + /** + * The cloud provider of the invoked function. + * + * Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. + */ + FAAS_INVOKED_PROVIDER: 'faas.invoked_provider', + + /** + * The cloud region of the invoked function. + * + * Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. + */ + FAAS_INVOKED_REGION: 'faas.invoked_region', + + /** + * Type of the trigger which caused this function invocation. + */ + FAAS_TRIGGER: 'faas.trigger', + + /** + * The [`service.name`](/docs/resource/README.md#service) of the remote service. SHOULD be equal to the actual `service.name` resource attribute of the remote service if any. + */ + PEER_SERVICE: 'peer.service', + + /** + * Username or client_id extracted from the access token or [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) header in the inbound request from outside the system. + */ + ENDUSER_ID: 'enduser.id', + + /** + * Actual/assumed role the client is making the request under extracted from token or application security context. + */ + ENDUSER_ROLE: 'enduser.role', + + /** + * Scopes or granted authorities the client currently possesses extracted from token or application security context. The value would come from the scope associated with an [OAuth 2.0 Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) or an attribute value in a [SAML 2.0 Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). + */ + ENDUSER_SCOPE: 'enduser.scope', + + /** + * Identifies the class / type of event. + * + * Note: Event names are subject to the same rules as [attribute names](https://github.com/open-telemetry/opentelemetry-specification/tree/v1.26.0/specification/common/attribute-naming.md). Notably, event names are namespaced to avoid collisions and provide a clean separation of semantics for events in separate domains like browser, mobile, and kubernetes. + */ + EVENT_NAME: 'event.name', + + /** + * A unique identifier for the Log Record. + * + * Note: If an id is provided, other log records with the same id will be considered duplicates and can be removed safely. This means, that two distinguishable log records MUST have different values. +The id MAY be an [Universally Unique Lexicographically Sortable Identifier (ULID)](https://github.com/ulid/spec), but other identifiers (e.g. UUID) may be used as needed. + */ + LOG_RECORD_UID: 'log.record.uid', + + /** + * The stream associated with the log. See below for a list of well-known values. + */ + LOG_IOSTREAM: 'log.iostream', + + /** + * The basename of the file. + */ + LOG_FILE_NAME: 'log.file.name', + + /** + * The basename of the file, with symlinks resolved. + */ + LOG_FILE_NAME_RESOLVED: 'log.file.name_resolved', + + /** + * The full path to the file. + */ + LOG_FILE_PATH: 'log.file.path', + + /** + * The full path to the file, with symlinks resolved. + */ + LOG_FILE_PATH_RESOLVED: 'log.file.path_resolved', + + /** + * This attribute represents the state the application has transitioned into at the occurrence of the event. + * + * Note: The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate#1656902), and from which the `OS terminology` column values are derived. + */ + IOS_STATE: 'ios.state', + + /** + * This attribute represents the state the application has transitioned into at the occurrence of the event. + * + * Note: The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lc), and from which the `OS identifiers` are derived. + */ + ANDROID_STATE: 'android.state', + + /** + * The name of the connection pool; unique within the instrumented application. In case the connection pool implementation doesn't provide a name, then the [db.connection_string](/docs/database/database-spans.md#connection-level-attributes) should be used. + */ + POOL_NAME: 'pool.name', + + /** + * The state of a connection in the pool. + */ + STATE: 'state', + + /** + * Full type name of the [`IExceptionHandler`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) implementation that handled the exception. + */ + ASPNETCORE_DIAGNOSTICS_HANDLER_TYPE: 'aspnetcore.diagnostics.handler.type', + + /** + * Rate limiting policy name. + */ + ASPNETCORE_RATE_LIMITING_POLICY: 'aspnetcore.rate_limiting.policy', + + /** + * Rate-limiting result, shows whether the lease was acquired or contains a rejection reason. + */ + ASPNETCORE_RATE_LIMITING_RESULT: 'aspnetcore.rate_limiting.result', + + /** + * Flag indicating if request was handled by the application pipeline. + */ + ASPNETCORE_REQUEST_IS_UNHANDLED: 'aspnetcore.request.is_unhandled', + + /** + * A value that indicates whether the matched route is a fallback route. + */ + ASPNETCORE_ROUTING_IS_FALLBACK: 'aspnetcore.routing.is_fallback', + + /** + * Match result - success or failure. + */ + ASPNETCORE_ROUTING_MATCH_STATUS: 'aspnetcore.routing.match_status', + + /** + * ASP.NET Core exception middleware handling result. + */ + ASPNETCORE_DIAGNOSTICS_EXCEPTION_RESULT: 'aspnetcore.diagnostics.exception.result', + + /** + * The name being queried. + * + * Note: The name being queried. +If the name field contains non-printable characters (below 32 or above 126), those characters should be represented as escaped base 10 integers (\DDD). Back slashes and quotes should be escaped. Tabs, carriage returns, and line feeds should be converted to \t, \r, and \n respectively. + */ + DNS_QUESTION_NAME: 'dns.question.name', + + /** + * State of the HTTP connection in the HTTP connection pool. + */ + HTTP_CONNECTION_STATE: 'http.connection.state', + + /** + * SignalR HTTP connection closure status. + */ + SIGNALR_CONNECTION_STATUS: 'signalr.connection.status', + + /** + * [SignalR transport type](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/TransportProtocols.md). + */ + SIGNALR_TRANSPORT: 'signalr.transport', + + /** + * Name of the buffer pool. + * + * Note: Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()). + */ + JVM_BUFFER_POOL_NAME: 'jvm.buffer.pool.name', + + /** + * Name of the memory pool. + * + * Note: Pool names are generally obtained via [MemoryPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html#getName()). + */ + JVM_MEMORY_POOL_NAME: 'jvm.memory.pool.name', + + /** + * The type of memory. + */ + JVM_MEMORY_TYPE: 'jvm.memory.type', + + /** + * Name of the garbage collector action. + * + * Note: Garbage collector action is generally obtained via [GarbageCollectionNotificationInfo#getGcAction()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcAction()). + */ + JVM_GC_ACTION: 'jvm.gc.action', + + /** + * Name of the garbage collector. + * + * Note: Garbage collector name is generally obtained via [GarbageCollectionNotificationInfo#getGcName()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcName()). + */ + JVM_GC_NAME: 'jvm.gc.name', + + /** + * Whether the thread is daemon or not. + */ + JVM_THREAD_DAEMON: 'jvm.thread.daemon', + + /** + * State of the thread. + */ + JVM_THREAD_STATE: 'jvm.thread.state', + + /** + * The device identifier. + */ + SYSTEM_DEVICE: 'system.device', + + /** + * The logical CPU number [0..n-1]. + */ + SYSTEM_CPU_LOGICAL_NUMBER: 'system.cpu.logical_number', + + /** + * The state of the CPU. + */ + SYSTEM_CPU_STATE: 'system.cpu.state', + + /** + * The memory state. + */ + SYSTEM_MEMORY_STATE: 'system.memory.state', + + /** + * The paging access direction. + */ + SYSTEM_PAGING_DIRECTION: 'system.paging.direction', + + /** + * The memory paging state. + */ + SYSTEM_PAGING_STATE: 'system.paging.state', + + /** + * The memory paging type. + */ + SYSTEM_PAGING_TYPE: 'system.paging.type', + + /** + * The filesystem mode. + */ + SYSTEM_FILESYSTEM_MODE: 'system.filesystem.mode', + + /** + * The filesystem mount path. + */ + SYSTEM_FILESYSTEM_MOUNTPOINT: 'system.filesystem.mountpoint', + + /** + * The filesystem state. + */ + SYSTEM_FILESYSTEM_STATE: 'system.filesystem.state', + + /** + * The filesystem type. + */ + SYSTEM_FILESYSTEM_TYPE: 'system.filesystem.type', + + /** + * A stateless protocol MUST NOT set this attribute. + */ + SYSTEM_NETWORK_STATE: 'system.network.state', + + /** + * The process state, e.g., [Linux Process State Codes](https://man7.org/linux/man-pages/man1/ps.1.html#PROCESS_STATE_CODES). + */ + SYSTEM_PROCESSES_STATUS: 'system.processes.status', + + /** + * Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + * + * Note: When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent the client address behind any intermediaries, for example proxies, if it's available. + */ + CLIENT_ADDRESS: 'client.address', + + /** + * Client port number. + * + * Note: When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent the client port behind any intermediaries, for example proxies, if it's available. + */ + CLIENT_PORT: 'client.port', + + /** + * The column number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + */ + CODE_COLUMN: 'code.column', + + /** + * The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + */ + CODE_FILEPATH: 'code.filepath', + + /** + * The method or function name, or equivalent (usually rightmost part of the code unit's name). + */ + CODE_FUNCTION: 'code.function', + + /** + * The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + */ + CODE_LINENO: 'code.lineno', + + /** + * The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + */ + CODE_NAMESPACE: 'code.namespace', + + /** + * A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. + */ + CODE_STACKTRACE: 'code.stacktrace', + + /** + * The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). + */ + DB_CASSANDRA_CONSISTENCY_LEVEL: 'db.cassandra.consistency_level', + + /** + * The data center of the coordinating node for a query. + */ + DB_CASSANDRA_COORDINATOR_DC: 'db.cassandra.coordinator.dc', + + /** + * The ID of the coordinating node for a query. + */ + DB_CASSANDRA_COORDINATOR_ID: 'db.cassandra.coordinator.id', + + /** + * Whether or not the query is idempotent. + */ + DB_CASSANDRA_IDEMPOTENCE: 'db.cassandra.idempotence', + + /** + * The fetch size used for paging, i.e. how many rows will be returned at once. + */ + DB_CASSANDRA_PAGE_SIZE: 'db.cassandra.page_size', + + /** + * The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. + */ + DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT: 'db.cassandra.speculative_execution_count', + + /** + * The name of the primary Cassandra table that the operation is acting upon, including the keyspace name (if applicable). + * + * Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. + */ + DB_CASSANDRA_TABLE: 'db.cassandra.table', + + /** + * The connection string used to connect to the database. It is recommended to remove embedded credentials. + */ + DB_CONNECTION_STRING: 'db.connection_string', + + /** + * Unique Cosmos client instance id. + */ + DB_COSMOSDB_CLIENT_ID: 'db.cosmosdb.client_id', + + /** + * Cosmos client connection mode. + */ + DB_COSMOSDB_CONNECTION_MODE: 'db.cosmosdb.connection_mode', + + /** + * Cosmos DB container name. + */ + DB_COSMOSDB_CONTAINER: 'db.cosmosdb.container', + + /** + * CosmosDB Operation Type. + */ + DB_COSMOSDB_OPERATION_TYPE: 'db.cosmosdb.operation_type', + + /** + * RU consumed for that operation. + */ + DB_COSMOSDB_REQUEST_CHARGE: 'db.cosmosdb.request_charge', + + /** + * Request payload size in bytes. + */ + DB_COSMOSDB_REQUEST_CONTENT_LENGTH: 'db.cosmosdb.request_content_length', + + /** + * Cosmos DB status code. + */ + DB_COSMOSDB_STATUS_CODE: 'db.cosmosdb.status_code', + + /** + * Cosmos DB sub status code. + */ + DB_COSMOSDB_SUB_STATUS_CODE: 'db.cosmosdb.sub_status_code', + + /** + * Represents the identifier of an Elasticsearch cluster. + */ + DB_ELASTICSEARCH_CLUSTER_NAME: 'db.elasticsearch.cluster.name', + + /** + * Represents the human-readable identifier of the node/instance to which a request was routed. + */ + DB_ELASTICSEARCH_NODE_NAME: 'db.elasticsearch.node.name', + + /** + * An identifier (address, unique name, or any other identifier) of the database instance that is executing queries or mutations on the current connection. This is useful in cases where the database is running in a clustered environment and the instrumentation is able to record the node executing the query. The client may obtain this value in databases like MySQL using queries like `select @@hostname`. + */ + DB_INSTANCE_ID: 'db.instance.id', + + /** + * The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. + */ + DB_JDBC_DRIVER_CLASSNAME: 'db.jdbc.driver_classname', + + /** + * The MongoDB collection being accessed within the database stated in `db.name`. + */ + DB_MONGODB_COLLECTION: 'db.mongodb.collection', + + /** + * The Microsoft SQL Server [instance name](https://docs.microsoft.com/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance. + * + * Note: If setting a `db.mssql.instance_name`, `server.port` is no longer required (but still recommended if non-standard). + */ + DB_MSSQL_INSTANCE_NAME: 'db.mssql.instance_name', + + /** + * This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). + * + * Note: In some SQL databases, the database name to be used is called "schema name". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name). + */ + DB_NAME: 'db.name', + + /** + * The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword. + * + * Note: When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted. + */ + DB_OPERATION: 'db.operation', + + /** + * The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute. + */ + DB_REDIS_DATABASE_INDEX: 'db.redis.database_index', + + /** + * The name of the primary table that the operation is acting upon, including the database name (if applicable). + * + * Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. + */ + DB_SQL_TABLE: 'db.sql.table', + + /** + * The database statement being executed. + */ + DB_STATEMENT: 'db.statement', + + /** + * An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. + */ + DB_SYSTEM: 'db.system', + + /** + * Username for accessing the database. + */ + DB_USER: 'db.user', + + /** + * Deprecated, use `network.protocol.name` instead. + * + * @deprecated Replaced by `network.protocol.name`. + */ + HTTP_FLAVOR: 'http.flavor', + + /** + * Deprecated, use `http.request.method` instead. + * + * @deprecated Replaced by `http.request.method`. + */ + HTTP_METHOD: 'http.method', + + /** + * Deprecated, use `http.request.header.content-length` instead. + * + * @deprecated Replaced by `http.request.header.content-length`. + */ + HTTP_REQUEST_CONTENT_LENGTH: 'http.request_content_length', + + /** + * Deprecated, use `http.response.header.content-length` instead. + * + * @deprecated Replaced by `http.response.header.content-length`. + */ + HTTP_RESPONSE_CONTENT_LENGTH: 'http.response_content_length', + + /** + * Deprecated, use `url.scheme` instead. + * + * @deprecated Replaced by `url.scheme` instead. + */ + HTTP_SCHEME: 'http.scheme', + + /** + * Deprecated, use `http.response.status_code` instead. + * + * @deprecated Replaced by `http.response.status_code`. + */ + HTTP_STATUS_CODE: 'http.status_code', + + /** + * Deprecated, use `url.path` and `url.query` instead. + * + * @deprecated Split to `url.path` and `url.query. + */ + HTTP_TARGET: 'http.target', + + /** + * Deprecated, use `url.full` instead. + * + * @deprecated Replaced by `url.full`. + */ + HTTP_URL: 'http.url', + + /** + * Deprecated, use `user_agent.original` instead. + * + * @deprecated Replaced by `user_agent.original`. + */ + HTTP_USER_AGENT: 'http.user_agent', + + /** + * Deprecated, use `server.address`. + * + * @deprecated Replaced by `server.address`. + */ + NET_HOST_NAME: 'net.host.name', + + /** + * Deprecated, use `server.port`. + * + * @deprecated Replaced by `server.port`. + */ + NET_HOST_PORT: 'net.host.port', + + /** + * Deprecated, use `server.address` on client spans and `client.address` on server spans. + * + * @deprecated Replaced by `server.address` on client spans and `client.address` on server spans. + */ + NET_PEER_NAME: 'net.peer.name', + + /** + * Deprecated, use `server.port` on client spans and `client.port` on server spans. + * + * @deprecated Replaced by `server.port` on client spans and `client.port` on server spans. + */ + NET_PEER_PORT: 'net.peer.port', + + /** + * Deprecated, use `network.protocol.name`. + * + * @deprecated Replaced by `network.protocol.name`. + */ + NET_PROTOCOL_NAME: 'net.protocol.name', + + /** + * Deprecated, use `network.protocol.version`. + * + * @deprecated Replaced by `network.protocol.version`. + */ + NET_PROTOCOL_VERSION: 'net.protocol.version', + + /** + * Deprecated, use `network.transport` and `network.type`. + * + * @deprecated Split to `network.transport` and `network.type`. + */ + NET_SOCK_FAMILY: 'net.sock.family', + + /** + * Deprecated, use `network.local.address`. + * + * @deprecated Replaced by `network.local.address`. + */ + NET_SOCK_HOST_ADDR: 'net.sock.host.addr', + + /** + * Deprecated, use `network.local.port`. + * + * @deprecated Replaced by `network.local.port`. + */ + NET_SOCK_HOST_PORT: 'net.sock.host.port', + + /** + * Deprecated, use `network.peer.address`. + * + * @deprecated Replaced by `network.peer.address`. + */ + NET_SOCK_PEER_ADDR: 'net.sock.peer.addr', + + /** + * Deprecated, no replacement at this time. + * + * @deprecated Removed. + */ + NET_SOCK_PEER_NAME: 'net.sock.peer.name', + + /** + * Deprecated, use `network.peer.port`. + * + * @deprecated Replaced by `network.peer.port`. + */ + NET_SOCK_PEER_PORT: 'net.sock.peer.port', + + /** + * Deprecated, use `network.transport`. + * + * @deprecated Replaced by `network.transport`. + */ + NET_TRANSPORT: 'net.transport', + + /** + * Destination address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + * + * Note: When observed from the source side, and when communicating through an intermediary, `destination.address` SHOULD represent the destination address behind any intermediaries, for example proxies, if it's available. + */ + DESTINATION_ADDRESS: 'destination.address', + + /** + * Destination port number. + */ + DESTINATION_PORT: 'destination.port', + + /** + * The disk IO operation direction. + */ + DISK_IO_DIRECTION: 'disk.io.direction', + + /** + * Describes a class of error the operation ended with. + * + * Note: The `error.type` SHOULD be predictable and SHOULD have low cardinality. +Instrumentations SHOULD document the list of errors they report. + +The cardinality of `error.type` within one instrumentation library SHOULD be low. +Telemetry consumers that aggregate data from multiple instrumentation libraries and applications +should be prepared for `error.type` to have high cardinality at query time when no +additional filters are applied. + +If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. + +If a specific domain defines its own set of error identifiers (such as HTTP or gRPC status codes), +it's RECOMMENDED to: + +* Use a domain-specific attribute +* Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. + */ + ERROR_TYPE: 'error.type', + + /** + * SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. + * + * Note: An exception is considered to have escaped (or left) the scope of a span, +if that span is ended while the exception is still logically "in flight". +This may be actually "in flight" in some languages (e.g. if the exception +is passed to a Context manager's `__exit__` method in Python) but will +usually be caught at the point of recording the exception in most languages. + +It is usually not possible to determine at the point where an exception is thrown +whether it will escape the scope of a span. +However, it is trivial to know that an exception +will escape, if one checks for an active exception just before ending the span, +as done in the [example for recording span exceptions](#recording-an-exception). + +It follows that an exception may still escape the scope of the span +even if the `exception.escaped` attribute was not set or set to false, +since the event might have been recorded at a time where it was not +clear whether the exception will escape. + */ + EXCEPTION_ESCAPED: 'exception.escaped', + + /** + * The exception message. + */ + EXCEPTION_MESSAGE: 'exception.message', + + /** + * A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. + */ + EXCEPTION_STACKTRACE: 'exception.stacktrace', + + /** + * The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. + */ + EXCEPTION_TYPE: 'exception.type', + + /** + * The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. + */ + HTTP_REQUEST_BODY_SIZE: 'http.request.body.size', + + /** + * HTTP request method. + * + * Note: HTTP request method value SHOULD be "known" to the instrumentation. +By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods) +and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). + +If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`. + +If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override +the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named +OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods +(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults). + +HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. +Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. +Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. + */ + HTTP_REQUEST_METHOD: 'http.request.method', + + /** + * Original HTTP method sent by the client in the request line. + */ + HTTP_REQUEST_METHOD_ORIGINAL: 'http.request.method_original', + + /** + * The ordinal number of request resending attempt (for any reason, including redirects). + * + * Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). + */ + HTTP_REQUEST_RESEND_COUNT: 'http.request.resend_count', + + /** + * The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. + */ + HTTP_RESPONSE_BODY_SIZE: 'http.response.body.size', + + /** + * [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). + */ + HTTP_RESPONSE_STATUS_CODE: 'http.response.status_code', + + /** + * The matched route, that is, the path template in the format used by the respective server framework. + * + * Note: MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. +SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. + */ + HTTP_ROUTE: 'http.route', + + /** + * The number of messages sent, received, or processed in the scope of the batching operation. + * + * Note: Instrumentations SHOULD NOT set `messaging.batch.message_count` on spans that operate with a single message. When a messaging client library supports both batch and single-message API for the same operation, instrumentations SHOULD use `messaging.batch.message_count` for batching APIs and SHOULD NOT use it for single-message APIs. + */ + MESSAGING_BATCH_MESSAGE_COUNT: 'messaging.batch.message_count', + + /** + * A unique identifier for the client that consumes or produces a message. + */ + MESSAGING_CLIENT_ID: 'messaging.client_id', + + /** + * A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name). + */ + MESSAGING_DESTINATION_ANONYMOUS: 'messaging.destination.anonymous', + + /** + * The message destination name. + * + * Note: Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If +the broker doesn't have such notion, the destination name SHOULD uniquely identify the broker. + */ + MESSAGING_DESTINATION_NAME: 'messaging.destination.name', + + /** + * Low cardinality representation of the messaging destination name. + * + * Note: Destination names could be constructed from templates. An example would be a destination name involving a user name or product id. Although the destination name in this case is of high cardinality, the underlying template is of low cardinality and can be effectively used for grouping and aggregation. + */ + MESSAGING_DESTINATION_TEMPLATE: 'messaging.destination.template', + + /** + * A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed. + */ + MESSAGING_DESTINATION_TEMPORARY: 'messaging.destination.temporary', + + /** + * A boolean that is true if the publish message destination is anonymous (could be unnamed or have auto-generated name). + */ + MESSAGING_DESTINATION_PUBLISH_ANONYMOUS: 'messaging.destination_publish.anonymous', + + /** + * The name of the original destination the message was published to. + * + * Note: The name SHOULD uniquely identify a specific queue, topic, or other entity within the broker. If +the broker doesn't have such notion, the original destination name SHOULD uniquely identify the broker. + */ + MESSAGING_DESTINATION_PUBLISH_NAME: 'messaging.destination_publish.name', + + /** + * The ordering key for a given message. If the attribute is not present, the message does not have an ordering key. + */ + MESSAGING_GCP_PUBSUB_MESSAGE_ORDERING_KEY: 'messaging.gcp_pubsub.message.ordering_key', + + /** + * Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers. + */ + MESSAGING_KAFKA_CONSUMER_GROUP: 'messaging.kafka.consumer.group', + + /** + * Partition the message is sent to. + */ + MESSAGING_KAFKA_DESTINATION_PARTITION: 'messaging.kafka.destination.partition', + + /** + * Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message.id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. + * + * Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. + */ + MESSAGING_KAFKA_MESSAGE_KEY: 'messaging.kafka.message.key', + + /** + * The offset of a record in the corresponding Kafka partition. + */ + MESSAGING_KAFKA_MESSAGE_OFFSET: 'messaging.kafka.message.offset', + + /** + * A boolean that is true if the message is a tombstone. + */ + MESSAGING_KAFKA_MESSAGE_TOMBSTONE: 'messaging.kafka.message.tombstone', + + /** + * The size of the message body in bytes. + * + * Note: This can refer to both the compressed or uncompressed body size. If both sizes are known, the uncompressed +body size should be used. + */ + MESSAGING_MESSAGE_BODY_SIZE: 'messaging.message.body.size', + + /** + * The conversation ID identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". + */ + MESSAGING_MESSAGE_CONVERSATION_ID: 'messaging.message.conversation_id', + + /** + * The size of the message body and metadata in bytes. + * + * Note: This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed +size should be used. + */ + MESSAGING_MESSAGE_ENVELOPE_SIZE: 'messaging.message.envelope.size', + + /** + * A value used by the messaging system as an identifier for the message, represented as a string. + */ + MESSAGING_MESSAGE_ID: 'messaging.message.id', + + /** + * A string identifying the kind of messaging operation. + * + * Note: If a custom value is used, it MUST be of low cardinality. + */ + MESSAGING_OPERATION: 'messaging.operation', + + /** + * RabbitMQ message routing key. + */ + MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY: 'messaging.rabbitmq.destination.routing_key', + + /** + * Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. + */ + MESSAGING_ROCKETMQ_CLIENT_GROUP: 'messaging.rocketmq.client_group', + + /** + * Model of message consumption. This only applies to consumer spans. + */ + MESSAGING_ROCKETMQ_CONSUMPTION_MODEL: 'messaging.rocketmq.consumption_model', + + /** + * The delay time level for delay message, which determines the message delay time. + */ + MESSAGING_ROCKETMQ_MESSAGE_DELAY_TIME_LEVEL: 'messaging.rocketmq.message.delay_time_level', + + /** + * The timestamp in milliseconds that the delay message is expected to be delivered to consumer. + */ + MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP: 'messaging.rocketmq.message.delivery_timestamp', + + /** + * It is essential for FIFO message. Messages that belong to the same message group are always processed one by one within the same consumer group. + */ + MESSAGING_ROCKETMQ_MESSAGE_GROUP: 'messaging.rocketmq.message.group', + + /** + * Key(s) of message, another way to mark message besides message id. + */ + MESSAGING_ROCKETMQ_MESSAGE_KEYS: 'messaging.rocketmq.message.keys', + + /** + * The secondary classifier of message besides topic. + */ + MESSAGING_ROCKETMQ_MESSAGE_TAG: 'messaging.rocketmq.message.tag', + + /** + * Type of message. + */ + MESSAGING_ROCKETMQ_MESSAGE_TYPE: 'messaging.rocketmq.message.type', + + /** + * Namespace of RocketMQ resources, resources in different namespaces are individual. + */ + MESSAGING_ROCKETMQ_NAMESPACE: 'messaging.rocketmq.namespace', + + /** + * An identifier for the messaging system being used. See below for a list of well-known identifiers. + */ + MESSAGING_SYSTEM: 'messaging.system', + + /** + * The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. + */ + NETWORK_CARRIER_ICC: 'network.carrier.icc', + + /** + * The mobile carrier country code. + */ + NETWORK_CARRIER_MCC: 'network.carrier.mcc', + + /** + * The mobile carrier network code. + */ + NETWORK_CARRIER_MNC: 'network.carrier.mnc', + + /** + * The name of the mobile carrier. + */ + NETWORK_CARRIER_NAME: 'network.carrier.name', + + /** + * This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. + */ + NETWORK_CONNECTION_SUBTYPE: 'network.connection.subtype', + + /** + * The internet connection type. + */ + NETWORK_CONNECTION_TYPE: 'network.connection.type', + + /** + * The network IO operation direction. + */ + NETWORK_IO_DIRECTION: 'network.io.direction', + + /** + * Local address of the network connection - IP address or Unix domain socket name. + */ + NETWORK_LOCAL_ADDRESS: 'network.local.address', + + /** + * Local port number of the network connection. + */ + NETWORK_LOCAL_PORT: 'network.local.port', + + /** + * Peer address of the network connection - IP address or Unix domain socket name. + */ + NETWORK_PEER_ADDRESS: 'network.peer.address', + + /** + * Peer port number of the network connection. + */ + NETWORK_PEER_PORT: 'network.peer.port', + + /** + * [OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent. + * + * Note: The value SHOULD be normalized to lowercase. + */ + NETWORK_PROTOCOL_NAME: 'network.protocol.name', + + /** + * Version of the protocol specified in `network.protocol.name`. + * + * Note: `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + */ + NETWORK_PROTOCOL_VERSION: 'network.protocol.version', + + /** + * [OSI transport layer](https://osi-model.com/transport-layer/) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication). + * + * Note: The value SHOULD be normalized to lowercase. + +Consider always setting the transport when setting a port number, since +a port number is ambiguous without knowing the transport. For example +different processes could be listening on TCP port 12345 and UDP port 12345. + */ + NETWORK_TRANSPORT: 'network.transport', + + /** + * [OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent. + * + * Note: The value SHOULD be normalized to lowercase. + */ + NETWORK_TYPE: 'network.type', + + /** + * The [error codes](https://connect.build/docs/protocol/#error-codes) of the Connect request. Error codes are always string values. + */ + RPC_CONNECT_RPC_ERROR_CODE: 'rpc.connect_rpc.error_code', + + /** + * The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. + */ + RPC_GRPC_STATUS_CODE: 'rpc.grpc.status_code', + + /** + * `error.code` property of response if it is an error response. + */ + RPC_JSONRPC_ERROR_CODE: 'rpc.jsonrpc.error_code', + + /** + * `error.message` property of response if it is an error response. + */ + RPC_JSONRPC_ERROR_MESSAGE: 'rpc.jsonrpc.error_message', + + /** + * `id` property of request or response. Since protocol allows id to be int, string, `null` or missing (for notifications), value is expected to be cast to string for simplicity. Use empty string in case of `null` value. Omit entirely if this is a notification. + */ + RPC_JSONRPC_REQUEST_ID: 'rpc.jsonrpc.request_id', + + /** + * Protocol version as in `jsonrpc` property of request/response. Since JSON-RPC 1.0 doesn't specify this, the value can be omitted. + */ + RPC_JSONRPC_VERSION: 'rpc.jsonrpc.version', + + /** + * The name of the (logical) method being called, must be equal to the $method part in the span name. + * + * Note: This is the logical name of the method from the RPC interface perspective, which can be different from the name of any implementing method/function. The `code.function` attribute may be used to store the latter (e.g., method actually executing the call on the server side, RPC client stub method on the client side). + */ + RPC_METHOD: 'rpc.method', + + /** + * The full (logical) name of the service being called, including its package name, if applicable. + * + * Note: This is the logical name of the service from the RPC interface perspective, which can be different from the name of any implementing class. The `code.namespace` attribute may be used to store the latter (despite the attribute name, it may include a class name; e.g., class with method actually executing the call on the server side, RPC client stub class on the client side). + */ + RPC_SERVICE: 'rpc.service', + + /** + * A string identifying the remoting system. See below for a list of well-known identifiers. + */ + RPC_SYSTEM: 'rpc.system', + + /** + * Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + * + * Note: When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available. + */ + SERVER_ADDRESS: 'server.address', + + /** + * Server port number. + * + * Note: When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available. + */ + SERVER_PORT: 'server.port', + + /** + * Source address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + * + * Note: When observed from the destination side, and when communicating through an intermediary, `source.address` SHOULD represent the source address behind any intermediaries, for example proxies, if it's available. + */ + SOURCE_ADDRESS: 'source.address', + + /** + * Source port number. + */ + SOURCE_PORT: 'source.port', + + /** + * Current "managed" thread ID (as opposed to OS thread ID). + */ + THREAD_ID: 'thread.id', + + /** + * Current thread name. + */ + THREAD_NAME: 'thread.name', + + /** + * String indicating the [cipher](https://datatracker.ietf.org/doc/html/rfc5246#appendix-A.5) used during the current connection. + * + * Note: The values allowed for `tls.cipher` MUST be one of the `Descriptions` of the [registered TLS Cipher Suits](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#table-tls-parameters-4). + */ + TLS_CIPHER: 'tls.cipher', + + /** + * PEM-encoded stand-alone certificate offered by the client. This is usually mutually-exclusive of `client.certificate_chain` since this value also exists in that list. + */ + TLS_CLIENT_CERTIFICATE: 'tls.client.certificate', + + /** + * Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain. + */ + TLS_CLIENT_CERTIFICATE_CHAIN: 'tls.client.certificate_chain', + + /** + * Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. + */ + TLS_CLIENT_HASH_MD5: 'tls.client.hash.md5', + + /** + * Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. + */ + TLS_CLIENT_HASH_SHA1: 'tls.client.hash.sha1', + + /** + * Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. + */ + TLS_CLIENT_HASH_SHA256: 'tls.client.hash.sha256', + + /** + * Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. + */ + TLS_CLIENT_ISSUER: 'tls.client.issuer', + + /** + * A hash that identifies clients based on how they perform an SSL/TLS handshake. + */ + TLS_CLIENT_JA3: 'tls.client.ja3', + + /** + * Date/Time indicating when client certificate is no longer considered valid. + */ + TLS_CLIENT_NOT_AFTER: 'tls.client.not_after', + + /** + * Date/Time indicating when client certificate is first considered valid. + */ + TLS_CLIENT_NOT_BEFORE: 'tls.client.not_before', + + /** + * Also called an SNI, this tells the server which hostname to which the client is attempting to connect to. + */ + TLS_CLIENT_SERVER_NAME: 'tls.client.server_name', + + /** + * Distinguished name of subject of the x.509 certificate presented by the client. + */ + TLS_CLIENT_SUBJECT: 'tls.client.subject', + + /** + * Array of ciphers offered by the client during the client hello. + */ + TLS_CLIENT_SUPPORTED_CIPHERS: 'tls.client.supported_ciphers', + + /** + * String indicating the curve used for the given cipher, when applicable. + */ + TLS_CURVE: 'tls.curve', + + /** + * Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel. + */ + TLS_ESTABLISHED: 'tls.established', + + /** + * String indicating the protocol being tunneled. Per the values in the [IANA registry](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), this string should be lower case. + */ + TLS_NEXT_PROTOCOL: 'tls.next_protocol', + + /** + * Normalized lowercase protocol name parsed from original string of the negotiated [SSL/TLS protocol version](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html#RETURN-VALUES). + */ + TLS_PROTOCOL_NAME: 'tls.protocol.name', + + /** + * Numeric part of the version parsed from the original string of the negotiated [SSL/TLS protocol version](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html#RETURN-VALUES). + */ + TLS_PROTOCOL_VERSION: 'tls.protocol.version', + + /** + * Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation. + */ + TLS_RESUMED: 'tls.resumed', + + /** + * PEM-encoded stand-alone certificate offered by the server. This is usually mutually-exclusive of `server.certificate_chain` since this value also exists in that list. + */ + TLS_SERVER_CERTIFICATE: 'tls.server.certificate', + + /** + * Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain. + */ + TLS_SERVER_CERTIFICATE_CHAIN: 'tls.server.certificate_chain', + + /** + * Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. + */ + TLS_SERVER_HASH_MD5: 'tls.server.hash.md5', + + /** + * Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. + */ + TLS_SERVER_HASH_SHA1: 'tls.server.hash.sha1', + + /** + * Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. + */ + TLS_SERVER_HASH_SHA256: 'tls.server.hash.sha256', + + /** + * Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. + */ + TLS_SERVER_ISSUER: 'tls.server.issuer', + + /** + * A hash that identifies servers based on how they perform an SSL/TLS handshake. + */ + TLS_SERVER_JA3S: 'tls.server.ja3s', + + /** + * Date/Time indicating when server certificate is no longer considered valid. + */ + TLS_SERVER_NOT_AFTER: 'tls.server.not_after', + + /** + * Date/Time indicating when server certificate is first considered valid. + */ + TLS_SERVER_NOT_BEFORE: 'tls.server.not_before', + + /** + * Distinguished name of subject of the x.509 certificate presented by the server. + */ + TLS_SERVER_SUBJECT: 'tls.server.subject', + + /** + * The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component. + */ + URL_FRAGMENT: 'url.fragment', + + /** + * Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). + * + * Note: For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment is not transmitted over HTTP, but if it is known, it SHOULD be included nevertheless. +`url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case username and password SHOULD be redacted and attribute's value SHOULD be `https://REDACTED:REDACTED@www.example.com/`. +`url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes. + */ + URL_FULL: 'url.full', + + /** + * The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. + */ + URL_PATH: 'url.path', + + /** + * The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. + * + * Note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. + */ + URL_QUERY: 'url.query', + + /** + * The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + */ + URL_SCHEME: 'url.scheme', + + /** + * Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. + */ + USER_AGENT_ORIGINAL: 'user_agent.original', + + /** + * A unique id to identify a session. + */ + SESSION_ID: 'session.id', + + /** + * The previous `session.id` for this user, when known. + */ + SESSION_PREVIOUS_ID: 'session.previous_id', + + /** + * The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). + * + * Note: This may be different from `cloud.resource_id` if an alias is involved. + */ + AWS_LAMBDA_INVOKED_ARN: 'aws.lambda.invoked_arn', + + /** + * The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. + */ + CLOUDEVENTS_EVENT_ID: 'cloudevents.event_id', + + /** + * The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. + */ + CLOUDEVENTS_EVENT_SOURCE: 'cloudevents.event_source', + + /** + * The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. + */ + CLOUDEVENTS_EVENT_SPEC_VERSION: 'cloudevents.event_spec_version', + + /** + * The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). + */ + CLOUDEVENTS_EVENT_SUBJECT: 'cloudevents.event_subject', + + /** + * The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. + */ + CLOUDEVENTS_EVENT_TYPE: 'cloudevents.event_type', + + /** + * Parent-child Reference type. + * + * Note: The causal relationship between a child Span and a parent Span. + */ + OPENTRACING_REF_TYPE: 'opentracing.ref_type', + + /** + * Name of the code, either "OK" or "ERROR". MUST NOT be set if the status code is UNSET. + */ + OTEL_STATUS_CODE: 'otel.status_code', + + /** + * Description of the Status if it has a value, otherwise not set. + */ + OTEL_STATUS_DESCRIPTION: 'otel.status_description', + + /** + * The invocation ID of the current function invocation. + */ + FAAS_INVOCATION_ID: 'faas.invocation_id', + + /** + * The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. + */ + FAAS_DOCUMENT_COLLECTION: 'faas.document.collection', + + /** + * The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. + */ + FAAS_DOCUMENT_NAME: 'faas.document.name', + + /** + * Describes the type of the operation that was performed on the data. + */ + FAAS_DOCUMENT_OPERATION: 'faas.document.operation', + + /** + * A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + */ + FAAS_DOCUMENT_TIME: 'faas.document.time', + + /** + * A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). + */ + FAAS_CRON: 'faas.cron', + + /** + * A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). + */ + FAAS_TIME: 'faas.time', + + /** + * A boolean that is true if the serverless function is executed for the first time (aka cold-start). + */ + FAAS_COLDSTART: 'faas.coldstart', + + /** + * The unique identifier of the feature flag. + */ + FEATURE_FLAG_KEY: 'feature_flag.key', + + /** + * The name of the service provider that performs the flag evaluation. + */ + FEATURE_FLAG_PROVIDER_NAME: 'feature_flag.provider_name', + + /** + * SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of the value can be used. + * + * Note: A semantic identifier, commonly referred to as a variant, provides a means +for referring to a value without including the value itself. This can +provide additional context for understanding the meaning behind a value. +For example, the variant `red` maybe be used for the value `#c05543`. + +A stringified version of the value can be used in situations where a +semantic identifier is unavailable. String representation of the value +should be determined by the implementer. + */ + FEATURE_FLAG_VARIANT: 'feature_flag.variant', + + /** + * The AWS request ID as returned in the response headers `x-amz-request-id` or `x-amz-requestid`. + */ + AWS_REQUEST_ID: 'aws.request_id', + + /** + * The value of the `AttributesToGet` request parameter. + */ + AWS_DYNAMODB_ATTRIBUTES_TO_GET: 'aws.dynamodb.attributes_to_get', + + /** + * The value of the `ConsistentRead` request parameter. + */ + AWS_DYNAMODB_CONSISTENT_READ: 'aws.dynamodb.consistent_read', + + /** + * The JSON-serialized value of each item in the `ConsumedCapacity` response field. + */ + AWS_DYNAMODB_CONSUMED_CAPACITY: 'aws.dynamodb.consumed_capacity', + + /** + * The value of the `IndexName` request parameter. + */ + AWS_DYNAMODB_INDEX_NAME: 'aws.dynamodb.index_name', + + /** + * The JSON-serialized value of the `ItemCollectionMetrics` response field. + */ + AWS_DYNAMODB_ITEM_COLLECTION_METRICS: 'aws.dynamodb.item_collection_metrics', + + /** + * The value of the `Limit` request parameter. + */ + AWS_DYNAMODB_LIMIT: 'aws.dynamodb.limit', + + /** + * The value of the `ProjectionExpression` request parameter. + */ + AWS_DYNAMODB_PROJECTION: 'aws.dynamodb.projection', + + /** + * The value of the `ProvisionedThroughput.ReadCapacityUnits` request parameter. + */ + AWS_DYNAMODB_PROVISIONED_READ_CAPACITY: 'aws.dynamodb.provisioned_read_capacity', + + /** + * The value of the `ProvisionedThroughput.WriteCapacityUnits` request parameter. + */ + AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY: 'aws.dynamodb.provisioned_write_capacity', + + /** + * The value of the `Select` request parameter. + */ + AWS_DYNAMODB_SELECT: 'aws.dynamodb.select', + + /** + * The keys in the `RequestItems` object field. + */ + AWS_DYNAMODB_TABLE_NAMES: 'aws.dynamodb.table_names', + + /** + * The JSON-serialized value of each item of the `GlobalSecondaryIndexes` request field. + */ + AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES: 'aws.dynamodb.global_secondary_indexes', + + /** + * The JSON-serialized value of each item of the `LocalSecondaryIndexes` request field. + */ + AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES: 'aws.dynamodb.local_secondary_indexes', + + /** + * The value of the `ExclusiveStartTableName` request parameter. + */ + AWS_DYNAMODB_EXCLUSIVE_START_TABLE: 'aws.dynamodb.exclusive_start_table', + + /** + * The the number of items in the `TableNames` response parameter. + */ + AWS_DYNAMODB_TABLE_COUNT: 'aws.dynamodb.table_count', + + /** + * The value of the `ScanIndexForward` request parameter. + */ + AWS_DYNAMODB_SCAN_FORWARD: 'aws.dynamodb.scan_forward', + + /** + * The value of the `Count` response parameter. + */ + AWS_DYNAMODB_COUNT: 'aws.dynamodb.count', + + /** + * The value of the `ScannedCount` response parameter. + */ + AWS_DYNAMODB_SCANNED_COUNT: 'aws.dynamodb.scanned_count', + + /** + * The value of the `Segment` request parameter. + */ + AWS_DYNAMODB_SEGMENT: 'aws.dynamodb.segment', + + /** + * The value of the `TotalSegments` request parameter. + */ + AWS_DYNAMODB_TOTAL_SEGMENTS: 'aws.dynamodb.total_segments', + + /** + * The JSON-serialized value of each item in the `AttributeDefinitions` request field. + */ + AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS: 'aws.dynamodb.attribute_definitions', + + /** + * The JSON-serialized value of each item in the the `GlobalSecondaryIndexUpdates` request field. + */ + AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES: 'aws.dynamodb.global_secondary_index_updates', + + /** + * The S3 bucket name the request refers to. Corresponds to the `--bucket` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. + * + * Note: The `bucket` attribute is applicable to all S3 operations that reference a bucket, i.e. that require the bucket name as a mandatory parameter. +This applies to almost all S3 operations except `list-buckets`. + */ + AWS_S3_BUCKET: 'aws.s3.bucket', + + /** + * The source object (in the form `bucket`/`key`) for the copy operation. + * + * Note: The `copy_source` attribute applies to S3 copy operations and corresponds to the `--copy-source` parameter +of the [copy-object operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html). +This applies in particular to the following operations: + +- [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) +- [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). + */ + AWS_S3_COPY_SOURCE: 'aws.s3.copy_source', + + /** + * The delete request container that specifies the objects to be deleted. + * + * Note: The `delete` attribute is only applicable to the [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) operation. +The `delete` attribute corresponds to the `--delete` parameter of the +[delete-objects operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html). + */ + AWS_S3_DELETE: 'aws.s3.delete', + + /** + * The S3 object key the request refers to. Corresponds to the `--key` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. + * + * Note: The `key` attribute is applicable to all object-related S3 operations, i.e. that require the object key as a mandatory parameter. +This applies in particular to the following operations: + +- [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) +- [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) +- [get-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-object.html) +- [head-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/head-object.html) +- [put-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-object.html) +- [restore-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/restore-object.html) +- [select-object-content](https://docs.aws.amazon.com/cli/latest/reference/s3api/select-object-content.html) +- [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) +- [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) +- [create-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-multipart-upload.html) +- [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) +- [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) +- [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). + */ + AWS_S3_KEY: 'aws.s3.key', + + /** + * The part number of the part being uploaded in a multipart-upload operation. This is a positive integer between 1 and 10,000. + * + * Note: The `part_number` attribute is only applicable to the [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) +and [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) operations. +The `part_number` attribute corresponds to the `--part-number` parameter of the +[upload-part operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html). + */ + AWS_S3_PART_NUMBER: 'aws.s3.part_number', + + /** + * Upload ID that identifies the multipart upload. + * + * Note: The `upload_id` attribute applies to S3 multipart-upload operations and corresponds to the `--upload-id` parameter +of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) multipart operations. +This applies in particular to the following operations: + +- [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) +- [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) +- [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) +- [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) +- [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). + */ + AWS_S3_UPLOAD_ID: 'aws.s3.upload_id', + + /** + * The GraphQL document being executed. + * + * Note: The value may be sanitized to exclude sensitive information. + */ + GRAPHQL_DOCUMENT: 'graphql.document', + + /** + * The name of the operation being executed. + */ + GRAPHQL_OPERATION_NAME: 'graphql.operation.name', + + /** + * The type of the operation being executed. + */ + GRAPHQL_OPERATION_TYPE: 'graphql.operation.type', + + /** + * Compressed size of the message in bytes. + */ + MESSAGE_COMPRESSED_SIZE: 'message.compressed_size', + + /** + * MUST be calculated as two different counters starting from `1` one for sent messages and one for received message. + * + * Note: This way we guarantee that the values will be consistent between different implementations. + */ + MESSAGE_ID: 'message.id', + + /** + * Whether this is a received or sent message. + */ + MESSAGE_TYPE: 'message.type', + + /** + * Uncompressed size of the message in bytes. + */ + MESSAGE_UNCOMPRESSED_SIZE: 'message.uncompressed_size', +} + + +export const FaasInvokedProviderValues = { + /** Alibaba Cloud. */ + ALIBABA_CLOUD: 'alibaba_cloud', + /** Amazon Web Services. */ + AWS: 'aws', + /** Microsoft Azure. */ + AZURE: 'azure', + /** Google Cloud Platform. */ + GCP: 'gcp', + /** Tencent Cloud. */ + TENCENT_CLOUD: 'tencent_cloud', +} as const +export type FaasInvokedProviderValues = typeof FaasInvokedProviderValues[keyof typeof FaasInvokedProviderValues] + + + + +export const FaasTriggerValues = { + /** A response to some data source operation such as a database or filesystem read/write. */ + DATASOURCE: 'datasource', + /** To provide an answer to an inbound HTTP request. */ + HTTP: 'http', + /** A function is set to be executed when messages are sent to a messaging system. */ + PUBSUB: 'pubsub', + /** A function is scheduled to be executed regularly. */ + TIMER: 'timer', + /** If none of the others apply. */ + OTHER: 'other', +} as const +export type FaasTriggerValues = typeof FaasTriggerValues[keyof typeof FaasTriggerValues] + + + + +export const LogIostreamValues = { + /** Logs from stdout stream. */ + STDOUT: 'stdout', + /** Events from stderr stream. */ + STDERR: 'stderr', +} as const +export type LogIostreamValues = typeof LogIostreamValues[keyof typeof LogIostreamValues] + + + + +export const IosStateValues = { + /** The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. */ + ACTIVE: 'active', + /** The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. */ + INACTIVE: 'inactive', + /** The app is now in the background. This value is associated with UIKit notification `applicationDidEnterBackground`. */ + BACKGROUND: 'background', + /** The app is now in the foreground. This value is associated with UIKit notification `applicationWillEnterForeground`. */ + FOREGROUND: 'foreground', + /** The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. */ + TERMINATE: 'terminate', +} as const +export type IosStateValues = typeof IosStateValues[keyof typeof IosStateValues] + + + + +export const AndroidStateValues = { + /** Any time before Activity.onResume() or, if the app has no Activity, Context.startService() has been called in the app for the first time. */ + CREATED: 'created', + /** Any time after Activity.onPause() or, if the app has no Activity, Context.stopService() has been called when the app was in the foreground state. */ + BACKGROUND: 'background', + /** Any time after Activity.onResume() or, if the app has no Activity, Context.startService() has been called when the app was in either the created or background states. */ + FOREGROUND: 'foreground', +} as const +export type AndroidStateValues = typeof AndroidStateValues[keyof typeof AndroidStateValues] + + + + +export const StateValues = { + /** idle. */ + IDLE: 'idle', + /** used. */ + USED: 'used', +} as const +export type StateValues = typeof StateValues[keyof typeof StateValues] + + + + +export const AspnetcoreRateLimitingResultValues = { + /** Lease was acquired. */ + ACQUIRED: 'acquired', + /** Lease request was rejected by the endpoint limiter. */ + ENDPOINT_LIMITER: 'endpoint_limiter', + /** Lease request was rejected by the global limiter. */ + GLOBAL_LIMITER: 'global_limiter', + /** Lease request was canceled. */ + REQUEST_CANCELED: 'request_canceled', +} as const +export type AspnetcoreRateLimitingResultValues = typeof AspnetcoreRateLimitingResultValues[keyof typeof AspnetcoreRateLimitingResultValues] + + + + +export const AspnetcoreRoutingMatchStatusValues = { + /** Match succeeded. */ + SUCCESS: 'success', + /** Match failed. */ + FAILURE: 'failure', +} as const +export type AspnetcoreRoutingMatchStatusValues = typeof AspnetcoreRoutingMatchStatusValues[keyof typeof AspnetcoreRoutingMatchStatusValues] + + + + +export const AspnetcoreDiagnosticsExceptionResultValues = { + /** Exception was handled by the exception handling middleware. */ + HANDLED: 'handled', + /** Exception was not handled by the exception handling middleware. */ + UNHANDLED: 'unhandled', + /** Exception handling was skipped because the response had started. */ + SKIPPED: 'skipped', + /** Exception handling didn't run because the request was aborted. */ + ABORTED: 'aborted', +} as const +export type AspnetcoreDiagnosticsExceptionResultValues = typeof AspnetcoreDiagnosticsExceptionResultValues[keyof typeof AspnetcoreDiagnosticsExceptionResultValues] + + + + +export const HttpConnectionStateValues = { + /** active state. */ + ACTIVE: 'active', + /** idle state. */ + IDLE: 'idle', +} as const +export type HttpConnectionStateValues = typeof HttpConnectionStateValues[keyof typeof HttpConnectionStateValues] + + + + +export const SignalrConnectionStatusValues = { + /** The connection was closed normally. */ + NORMAL_CLOSURE: 'normal_closure', + /** The connection was closed due to a timeout. */ + TIMEOUT: 'timeout', + /** The connection was closed because the app is shutting down. */ + APP_SHUTDOWN: 'app_shutdown', +} as const +export type SignalrConnectionStatusValues = typeof SignalrConnectionStatusValues[keyof typeof SignalrConnectionStatusValues] + + + + +export const SignalrTransportValues = { + /** ServerSentEvents protocol. */ + SERVER_SENT_EVENTS: 'server_sent_events', + /** LongPolling protocol. */ + LONG_POLLING: 'long_polling', + /** WebSockets protocol. */ + WEB_SOCKETS: 'web_sockets', +} as const +export type SignalrTransportValues = typeof SignalrTransportValues[keyof typeof SignalrTransportValues] + + + + +export const JvmMemoryTypeValues = { + /** Heap memory. */ + HEAP: 'heap', + /** Non-heap memory. */ + NON_HEAP: 'non_heap', +} as const +export type JvmMemoryTypeValues = typeof JvmMemoryTypeValues[keyof typeof JvmMemoryTypeValues] + + + + +export const JvmThreadStateValues = { + /** A thread that has not yet started is in this state. */ + NEW: 'new', + /** A thread executing in the Java virtual machine is in this state. */ + RUNNABLE: 'runnable', + /** A thread that is blocked waiting for a monitor lock is in this state. */ + BLOCKED: 'blocked', + /** A thread that is waiting indefinitely for another thread to perform a particular action is in this state. */ + WAITING: 'waiting', + /** A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state. */ + TIMED_WAITING: 'timed_waiting', + /** A thread that has exited is in this state. */ + TERMINATED: 'terminated', +} as const +export type JvmThreadStateValues = typeof JvmThreadStateValues[keyof typeof JvmThreadStateValues] + + + + +export const SystemCpuStateValues = { + /** user. */ + USER: 'user', + /** system. */ + SYSTEM: 'system', + /** nice. */ + NICE: 'nice', + /** idle. */ + IDLE: 'idle', + /** iowait. */ + IOWAIT: 'iowait', + /** interrupt. */ + INTERRUPT: 'interrupt', + /** steal. */ + STEAL: 'steal', +} as const +export type SystemCpuStateValues = typeof SystemCpuStateValues[keyof typeof SystemCpuStateValues] + + + + +export const SystemMemoryStateValues = { + /** used. */ + USED: 'used', + /** free. */ + FREE: 'free', + /** shared. */ + SHARED: 'shared', + /** buffers. */ + BUFFERS: 'buffers', + /** cached. */ + CACHED: 'cached', +} as const +export type SystemMemoryStateValues = typeof SystemMemoryStateValues[keyof typeof SystemMemoryStateValues] + + + + +export const SystemPagingDirectionValues = { + /** in. */ + IN: 'in', + /** out. */ + OUT: 'out', +} as const +export type SystemPagingDirectionValues = typeof SystemPagingDirectionValues[keyof typeof SystemPagingDirectionValues] + + + + +export const SystemPagingStateValues = { + /** used. */ + USED: 'used', + /** free. */ + FREE: 'free', +} as const +export type SystemPagingStateValues = typeof SystemPagingStateValues[keyof typeof SystemPagingStateValues] + + + + +export const SystemPagingTypeValues = { + /** major. */ + MAJOR: 'major', + /** minor. */ + MINOR: 'minor', +} as const +export type SystemPagingTypeValues = typeof SystemPagingTypeValues[keyof typeof SystemPagingTypeValues] + + + + +export const SystemFilesystemStateValues = { + /** used. */ + USED: 'used', + /** free. */ + FREE: 'free', + /** reserved. */ + RESERVED: 'reserved', +} as const +export type SystemFilesystemStateValues = typeof SystemFilesystemStateValues[keyof typeof SystemFilesystemStateValues] + + + + +export const SystemFilesystemTypeValues = { + /** fat32. */ + FAT32: 'fat32', + /** exfat. */ + EXFAT: 'exfat', + /** ntfs. */ + NTFS: 'ntfs', + /** refs. */ + REFS: 'refs', + /** hfsplus. */ + HFSPLUS: 'hfsplus', + /** ext4. */ + EXT4: 'ext4', +} as const +export type SystemFilesystemTypeValues = typeof SystemFilesystemTypeValues[keyof typeof SystemFilesystemTypeValues] + + + + +export const SystemNetworkStateValues = { + /** close. */ + CLOSE: 'close', + /** close_wait. */ + CLOSE_WAIT: 'close_wait', + /** closing. */ + CLOSING: 'closing', + /** delete. */ + DELETE: 'delete', + /** established. */ + ESTABLISHED: 'established', + /** fin_wait_1. */ + FIN_WAIT_1: 'fin_wait_1', + /** fin_wait_2. */ + FIN_WAIT_2: 'fin_wait_2', + /** last_ack. */ + LAST_ACK: 'last_ack', + /** listen. */ + LISTEN: 'listen', + /** syn_recv. */ + SYN_RECV: 'syn_recv', + /** syn_sent. */ + SYN_SENT: 'syn_sent', + /** time_wait. */ + TIME_WAIT: 'time_wait', +} as const +export type SystemNetworkStateValues = typeof SystemNetworkStateValues[keyof typeof SystemNetworkStateValues] + + + + +export const SystemProcessesStatusValues = { + /** running. */ + RUNNING: 'running', + /** sleeping. */ + SLEEPING: 'sleeping', + /** stopped. */ + STOPPED: 'stopped', + /** defunct. */ + DEFUNCT: 'defunct', +} as const +export type SystemProcessesStatusValues = typeof SystemProcessesStatusValues[keyof typeof SystemProcessesStatusValues] + + + + +export const DbCassandraConsistencyLevelValues = { + /** all. */ + ALL: 'all', + /** each_quorum. */ + EACH_QUORUM: 'each_quorum', + /** quorum. */ + QUORUM: 'quorum', + /** local_quorum. */ + LOCAL_QUORUM: 'local_quorum', + /** one. */ + ONE: 'one', + /** two. */ + TWO: 'two', + /** three. */ + THREE: 'three', + /** local_one. */ + LOCAL_ONE: 'local_one', + /** any. */ + ANY: 'any', + /** serial. */ + SERIAL: 'serial', + /** local_serial. */ + LOCAL_SERIAL: 'local_serial', +} as const +export type DbCassandraConsistencyLevelValues = typeof DbCassandraConsistencyLevelValues[keyof typeof DbCassandraConsistencyLevelValues] + + + + +export const DbCosmosdbConnectionModeValues = { + /** Gateway (HTTP) connections mode. */ + GATEWAY: 'gateway', + /** Direct connection. */ + DIRECT: 'direct', +} as const +export type DbCosmosdbConnectionModeValues = typeof DbCosmosdbConnectionModeValues[keyof typeof DbCosmosdbConnectionModeValues] + + + + +export const DbCosmosdbOperationTypeValues = { + /** invalid. */ + INVALID: 'Invalid', + /** create. */ + CREATE: 'Create', + /** patch. */ + PATCH: 'Patch', + /** read. */ + READ: 'Read', + /** read_feed. */ + READ_FEED: 'ReadFeed', + /** delete. */ + DELETE: 'Delete', + /** replace. */ + REPLACE: 'Replace', + /** execute. */ + EXECUTE: 'Execute', + /** query. */ + QUERY: 'Query', + /** head. */ + HEAD: 'Head', + /** head_feed. */ + HEAD_FEED: 'HeadFeed', + /** upsert. */ + UPSERT: 'Upsert', + /** batch. */ + BATCH: 'Batch', + /** query_plan. */ + QUERY_PLAN: 'QueryPlan', + /** execute_javascript. */ + EXECUTE_JAVASCRIPT: 'ExecuteJavaScript', +} as const +export type DbCosmosdbOperationTypeValues = typeof DbCosmosdbOperationTypeValues[keyof typeof DbCosmosdbOperationTypeValues] + + + + +export const DbSystemValues = { + /** Some other SQL database. Fallback only. See notes. */ + OTHER_SQL: 'other_sql', + /** Microsoft SQL Server. */ + MSSQL: 'mssql', + /** Microsoft SQL Server Compact. */ + MSSQLCOMPACT: 'mssqlcompact', + /** MySQL. */ + MYSQL: 'mysql', + /** Oracle Database. */ + ORACLE: 'oracle', + /** IBM Db2. */ + DB2: 'db2', + /** PostgreSQL. */ + POSTGRESQL: 'postgresql', + /** Amazon Redshift. */ + REDSHIFT: 'redshift', + /** Apache Hive. */ + HIVE: 'hive', + /** Cloudscape. */ + CLOUDSCAPE: 'cloudscape', + /** HyperSQL DataBase. */ + HSQLDB: 'hsqldb', + /** Progress Database. */ + PROGRESS: 'progress', + /** SAP MaxDB. */ + MAXDB: 'maxdb', + /** SAP HANA. */ + HANADB: 'hanadb', + /** Ingres. */ + INGRES: 'ingres', + /** FirstSQL. */ + FIRSTSQL: 'firstsql', + /** EnterpriseDB. */ + EDB: 'edb', + /** InterSystems Caché. */ + CACHE: 'cache', + /** Adabas (Adaptable Database System). */ + ADABAS: 'adabas', + /** Firebird. */ + FIREBIRD: 'firebird', + /** Apache Derby. */ + DERBY: 'derby', + /** FileMaker. */ + FILEMAKER: 'filemaker', + /** Informix. */ + INFORMIX: 'informix', + /** InstantDB. */ + INSTANTDB: 'instantdb', + /** InterBase. */ + INTERBASE: 'interbase', + /** MariaDB. */ + MARIADB: 'mariadb', + /** Netezza. */ + NETEZZA: 'netezza', + /** Pervasive PSQL. */ + PERVASIVE: 'pervasive', + /** PointBase. */ + POINTBASE: 'pointbase', + /** SQLite. */ + SQLITE: 'sqlite', + /** Sybase. */ + SYBASE: 'sybase', + /** Teradata. */ + TERADATA: 'teradata', + /** Vertica. */ + VERTICA: 'vertica', + /** H2. */ + H2: 'h2', + /** ColdFusion IMQ. */ + COLDFUSION: 'coldfusion', + /** Apache Cassandra. */ + CASSANDRA: 'cassandra', + /** Apache HBase. */ + HBASE: 'hbase', + /** MongoDB. */ + MONGODB: 'mongodb', + /** Redis. */ + REDIS: 'redis', + /** Couchbase. */ + COUCHBASE: 'couchbase', + /** CouchDB. */ + COUCHDB: 'couchdb', + /** Microsoft Azure Cosmos DB. */ + COSMOSDB: 'cosmosdb', + /** Amazon DynamoDB. */ + DYNAMODB: 'dynamodb', + /** Neo4j. */ + NEO4J: 'neo4j', + /** Apache Geode. */ + GEODE: 'geode', + /** Elasticsearch. */ + ELASTICSEARCH: 'elasticsearch', + /** Memcached. */ + MEMCACHED: 'memcached', + /** CockroachDB. */ + COCKROACHDB: 'cockroachdb', + /** OpenSearch. */ + OPENSEARCH: 'opensearch', + /** ClickHouse. */ + CLICKHOUSE: 'clickhouse', + /** Cloud Spanner. */ + SPANNER: 'spanner', + /** Trino. */ + TRINO: 'trino', +} as const +export type DbSystemValues = typeof DbSystemValues[keyof typeof DbSystemValues] + + + + +export const HttpFlavorValues = { + /** HTTP/1.0. */ + HTTP_1_0: '1.0', + /** HTTP/1.1. */ + HTTP_1_1: '1.1', + /** HTTP/2. */ + HTTP_2_0: '2.0', + /** HTTP/3. */ + HTTP_3_0: '3.0', + /** SPDY protocol. */ + SPDY: 'SPDY', + /** QUIC protocol. */ + QUIC: 'QUIC', +} as const +export type HttpFlavorValues = typeof HttpFlavorValues[keyof typeof HttpFlavorValues] + + + + +export const NetSockFamilyValues = { + /** IPv4 address. */ + INET: 'inet', + /** IPv6 address. */ + INET6: 'inet6', + /** Unix domain socket path. */ + UNIX: 'unix', +} as const +export type NetSockFamilyValues = typeof NetSockFamilyValues[keyof typeof NetSockFamilyValues] + + + + +export const NetTransportValues = { + /** ip_tcp. */ + IP_TCP: 'ip_tcp', + /** ip_udp. */ + IP_UDP: 'ip_udp', + /** Named or anonymous pipe. */ + PIPE: 'pipe', + /** In-process communication. */ + INPROC: 'inproc', + /** Something else (non IP-based). */ + OTHER: 'other', +} as const +export type NetTransportValues = typeof NetTransportValues[keyof typeof NetTransportValues] + + + + +export const DiskIoDirectionValues = { + /** read. */ + READ: 'read', + /** write. */ + WRITE: 'write', +} as const +export type DiskIoDirectionValues = typeof DiskIoDirectionValues[keyof typeof DiskIoDirectionValues] + + + + +export const ErrorTypeValues = { + /** A fallback error value to be used when the instrumentation doesn't define a custom value. */ + OTHER: '_OTHER', +} as const +export type ErrorTypeValues = typeof ErrorTypeValues[keyof typeof ErrorTypeValues] + + + + +export const HttpRequestMethodValues = { + /** CONNECT method. */ + CONNECT: 'CONNECT', + /** DELETE method. */ + DELETE: 'DELETE', + /** GET method. */ + GET: 'GET', + /** HEAD method. */ + HEAD: 'HEAD', + /** OPTIONS method. */ + OPTIONS: 'OPTIONS', + /** PATCH method. */ + PATCH: 'PATCH', + /** POST method. */ + POST: 'POST', + /** PUT method. */ + PUT: 'PUT', + /** TRACE method. */ + TRACE: 'TRACE', + /** Any HTTP method that the instrumentation has no prior knowledge of. */ + OTHER: '_OTHER', +} as const +export type HttpRequestMethodValues = typeof HttpRequestMethodValues[keyof typeof HttpRequestMethodValues] + + + + +export const MessagingOperationValues = { + /** One or more messages are provided for publishing to an intermediary. If a single message is published, the context of the "Publish" span can be used as the creation context and no "Create" span needs to be created. */ + PUBLISH: 'publish', + /** A message is created. "Create" spans always refer to a single message and are used to provide a unique creation context for messages in batch publishing scenarios. */ + CREATE: 'create', + /** One or more messages are requested by a consumer. This operation refers to pull-based scenarios, where consumers explicitly call methods of messaging SDKs to receive messages. */ + RECEIVE: 'receive', + /** One or more messages are passed to a consumer. This operation refers to push-based scenarios, where consumer register callbacks which get called by messaging SDKs. */ + DELIVER: 'deliver', +} as const +export type MessagingOperationValues = typeof MessagingOperationValues[keyof typeof MessagingOperationValues] + + + + +export const MessagingRocketmqConsumptionModelValues = { + /** Clustering consumption model. */ + CLUSTERING: 'clustering', + /** Broadcasting consumption model. */ + BROADCASTING: 'broadcasting', +} as const +export type MessagingRocketmqConsumptionModelValues = typeof MessagingRocketmqConsumptionModelValues[keyof typeof MessagingRocketmqConsumptionModelValues] + + + + +export const MessagingRocketmqMessageTypeValues = { + /** Normal message. */ + NORMAL: 'normal', + /** FIFO message. */ + FIFO: 'fifo', + /** Delay message. */ + DELAY: 'delay', + /** Transaction message. */ + TRANSACTION: 'transaction', +} as const +export type MessagingRocketmqMessageTypeValues = typeof MessagingRocketmqMessageTypeValues[keyof typeof MessagingRocketmqMessageTypeValues] + + + + +export const MessagingSystemValues = { + /** Apache ActiveMQ. */ + ACTIVEMQ: 'activemq', + /** Amazon Simple Queue Service (SQS). */ + AWS_SQS: 'aws_sqs', + /** Azure Event Grid. */ + AZURE_EVENTGRID: 'azure_eventgrid', + /** Azure Event Hubs. */ + AZURE_EVENTHUBS: 'azure_eventhubs', + /** Azure Service Bus. */ + AZURE_SERVICEBUS: 'azure_servicebus', + /** Google Cloud Pub/Sub. */ + GCP_PUBSUB: 'gcp_pubsub', + /** Java Message Service. */ + JMS: 'jms', + /** Apache Kafka. */ + KAFKA: 'kafka', + /** RabbitMQ. */ + RABBITMQ: 'rabbitmq', + /** Apache RocketMQ. */ + ROCKETMQ: 'rocketmq', +} as const +export type MessagingSystemValues = typeof MessagingSystemValues[keyof typeof MessagingSystemValues] + + + + +export const NetworkConnectionSubtypeValues = { + /** GPRS. */ + GPRS: 'gprs', + /** EDGE. */ + EDGE: 'edge', + /** UMTS. */ + UMTS: 'umts', + /** CDMA. */ + CDMA: 'cdma', + /** EVDO Rel. 0. */ + EVDO_0: 'evdo_0', + /** EVDO Rev. A. */ + EVDO_A: 'evdo_a', + /** CDMA2000 1XRTT. */ + CDMA2000_1XRTT: 'cdma2000_1xrtt', + /** HSDPA. */ + HSDPA: 'hsdpa', + /** HSUPA. */ + HSUPA: 'hsupa', + /** HSPA. */ + HSPA: 'hspa', + /** IDEN. */ + IDEN: 'iden', + /** EVDO Rev. B. */ + EVDO_B: 'evdo_b', + /** LTE. */ + LTE: 'lte', + /** EHRPD. */ + EHRPD: 'ehrpd', + /** HSPAP. */ + HSPAP: 'hspap', + /** GSM. */ + GSM: 'gsm', + /** TD-SCDMA. */ + TD_SCDMA: 'td_scdma', + /** IWLAN. */ + IWLAN: 'iwlan', + /** 5G NR (New Radio). */ + NR: 'nr', + /** 5G NRNSA (New Radio Non-Standalone). */ + NRNSA: 'nrnsa', + /** LTE CA. */ + LTE_CA: 'lte_ca', +} as const +export type NetworkConnectionSubtypeValues = typeof NetworkConnectionSubtypeValues[keyof typeof NetworkConnectionSubtypeValues] + + + + +export const NetworkConnectionTypeValues = { + /** wifi. */ + WIFI: 'wifi', + /** wired. */ + WIRED: 'wired', + /** cell. */ + CELL: 'cell', + /** unavailable. */ + UNAVAILABLE: 'unavailable', + /** unknown. */ + UNKNOWN: 'unknown', +} as const +export type NetworkConnectionTypeValues = typeof NetworkConnectionTypeValues[keyof typeof NetworkConnectionTypeValues] + + + + +export const NetworkIoDirectionValues = { + /** transmit. */ + TRANSMIT: 'transmit', + /** receive. */ + RECEIVE: 'receive', +} as const +export type NetworkIoDirectionValues = typeof NetworkIoDirectionValues[keyof typeof NetworkIoDirectionValues] + + + + +export const NetworkTransportValues = { + /** TCP. */ + TCP: 'tcp', + /** UDP. */ + UDP: 'udp', + /** Named or anonymous pipe. */ + PIPE: 'pipe', + /** Unix domain socket. */ + UNIX: 'unix', +} as const +export type NetworkTransportValues = typeof NetworkTransportValues[keyof typeof NetworkTransportValues] + + + + +export const NetworkTypeValues = { + /** IPv4. */ + IPV4: 'ipv4', + /** IPv6. */ + IPV6: 'ipv6', +} as const +export type NetworkTypeValues = typeof NetworkTypeValues[keyof typeof NetworkTypeValues] + + + + +export const RpcConnectRpcErrorCodeValues = { + /** cancelled. */ + CANCELLED: 'cancelled', + /** unknown. */ + UNKNOWN: 'unknown', + /** invalid_argument. */ + INVALID_ARGUMENT: 'invalid_argument', + /** deadline_exceeded. */ + DEADLINE_EXCEEDED: 'deadline_exceeded', + /** not_found. */ + NOT_FOUND: 'not_found', + /** already_exists. */ + ALREADY_EXISTS: 'already_exists', + /** permission_denied. */ + PERMISSION_DENIED: 'permission_denied', + /** resource_exhausted. */ + RESOURCE_EXHAUSTED: 'resource_exhausted', + /** failed_precondition. */ + FAILED_PRECONDITION: 'failed_precondition', + /** aborted. */ + ABORTED: 'aborted', + /** out_of_range. */ + OUT_OF_RANGE: 'out_of_range', + /** unimplemented. */ + UNIMPLEMENTED: 'unimplemented', + /** internal. */ + INTERNAL: 'internal', + /** unavailable. */ + UNAVAILABLE: 'unavailable', + /** data_loss. */ + DATA_LOSS: 'data_loss', + /** unauthenticated. */ + UNAUTHENTICATED: 'unauthenticated', +} as const +export type RpcConnectRpcErrorCodeValues = typeof RpcConnectRpcErrorCodeValues[keyof typeof RpcConnectRpcErrorCodeValues] + + + + +export const RpcGrpcStatusCodeValues = { + /** OK. */ + OK: 0, + /** CANCELLED. */ + CANCELLED: 1, + /** UNKNOWN. */ + UNKNOWN: 2, + /** INVALID_ARGUMENT. */ + INVALID_ARGUMENT: 3, + /** DEADLINE_EXCEEDED. */ + DEADLINE_EXCEEDED: 4, + /** NOT_FOUND. */ + NOT_FOUND: 5, + /** ALREADY_EXISTS. */ + ALREADY_EXISTS: 6, + /** PERMISSION_DENIED. */ + PERMISSION_DENIED: 7, + /** RESOURCE_EXHAUSTED. */ + RESOURCE_EXHAUSTED: 8, + /** FAILED_PRECONDITION. */ + FAILED_PRECONDITION: 9, + /** ABORTED. */ + ABORTED: 10, + /** OUT_OF_RANGE. */ + OUT_OF_RANGE: 11, + /** UNIMPLEMENTED. */ + UNIMPLEMENTED: 12, + /** INTERNAL. */ + INTERNAL: 13, + /** UNAVAILABLE. */ + UNAVAILABLE: 14, + /** DATA_LOSS. */ + DATA_LOSS: 15, + /** UNAUTHENTICATED. */ + UNAUTHENTICATED: 16, +} as const +export type RpcGrpcStatusCodeValues = typeof RpcGrpcStatusCodeValues[keyof typeof RpcGrpcStatusCodeValues] + + + + +export const RpcSystemValues = { + /** gRPC. */ + GRPC: 'grpc', + /** Java RMI. */ + JAVA_RMI: 'java_rmi', + /** .NET WCF. */ + DOTNET_WCF: 'dotnet_wcf', + /** Apache Dubbo. */ + APACHE_DUBBO: 'apache_dubbo', + /** Connect RPC. */ + CONNECT_RPC: 'connect_rpc', +} as const +export type RpcSystemValues = typeof RpcSystemValues[keyof typeof RpcSystemValues] + + + + +export const TlsProtocolNameValues = { + /** ssl. */ + SSL: 'ssl', + /** tls. */ + TLS: 'tls', +} as const +export type TlsProtocolNameValues = typeof TlsProtocolNameValues[keyof typeof TlsProtocolNameValues] + + + + +export const OpentracingRefTypeValues = { + /** The parent Span depends on the child Span in some capacity. */ + CHILD_OF: 'child_of', + /** The parent Span doesn't depend in any way on the result of the child Span. */ + FOLLOWS_FROM: 'follows_from', +} as const +export type OpentracingRefTypeValues = typeof OpentracingRefTypeValues[keyof typeof OpentracingRefTypeValues] + + + + +export const OtelStatusCodeValues = { + /** The operation has been validated by an Application developer or Operator to have completed successfully. */ + OK: 'OK', + /** The operation contains an error. */ + ERROR: 'ERROR', +} as const +export type OtelStatusCodeValues = typeof OtelStatusCodeValues[keyof typeof OtelStatusCodeValues] + + + + +export const FaasDocumentOperationValues = { + /** When a new object is created. */ + INSERT: 'insert', + /** When an object is modified. */ + EDIT: 'edit', + /** When an object is deleted. */ + DELETE: 'delete', +} as const +export type FaasDocumentOperationValues = typeof FaasDocumentOperationValues[keyof typeof FaasDocumentOperationValues] + + + + +export const GraphqlOperationTypeValues = { + /** GraphQL query. */ + QUERY: 'query', + /** GraphQL mutation. */ + MUTATION: 'mutation', + /** GraphQL subscription. */ + SUBSCRIPTION: 'subscription', +} as const +export type GraphqlOperationTypeValues = typeof GraphqlOperationTypeValues[keyof typeof GraphqlOperationTypeValues] + + + + +export const MessageTypeValues = { + /** sent. */ + SENT: 'SENT', + /** received. */ + RECEIVED: 'RECEIVED', +} as const +export type MessageTypeValues = typeof MessageTypeValues[keyof typeof MessageTypeValues] + diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index db15d1c7f6c..5a370dde3cd 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -16,7 +16,6 @@ import * as diagch from 'diagnostics_channel'; import { URL } from 'url'; -import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { InstrumentationBase, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; import { Attributes, @@ -34,6 +33,7 @@ import { VERSION } from './version'; import { HeadersMessage, ListenerRecord, RequestMessage } from './internal-types'; import { UndiciInstrumentationConfig, UndiciRequest } from './types'; +import { SemanticAttributes } from './enums/SemanticAttributes'; // A combination of https://github.com/elastic/apm-agent-nodejs and @@ -148,41 +148,33 @@ export class UndiciInstrumentation extends InstrumentationBase { const requestUrl = new URL(request.origin); const spanAttributes: Attributes = { - [SemanticAttributes.HTTP_URL]: request.origin, - [SemanticAttributes.HTTP_METHOD]: request.method, - [SemanticAttributes.HTTP_TARGET]: request.path || '/', - [SemanticAttributes.NET_PEER_NAME]: requestUrl.hostname, + [SemanticAttributes.URL_FULL]: request.origin, + [SemanticAttributes.URL_PATH]: requestUrl.pathname, + [SemanticAttributes.URL_QUERY]: requestUrl.search, }; - let hostAttribute = reqHeaders.get('host'); - if (!hostAttribute) { - const protocolPorts: Record = { https: '443', http: '80' }; - const defaultPort = protocolPorts[requestUrl.protocol] || ''; - const port = requestUrl.port || defaultPort; - - hostAttribute = requestUrl.hostname; - if (port) { - hostAttribute += `:${port}`; - } + const protocolPorts: Record = { https: '443', http: '80' }; + const serverAddress = reqHeaders.get('host') || requestUrl.hostname; + const serverPort = requestUrl.port || protocolPorts[requestUrl.protocol]; + + spanAttributes[SemanticAttributes.SERVER_ADDRESS] = serverAddress; + if (serverPort) { + spanAttributes[SemanticAttributes.SERVER_PORT] = serverPort; } - spanAttributes[SemanticAttributes.HTTP_HOST] = hostAttribute; const userAgent = reqHeaders.get('user-agent'); if (userAgent) { - spanAttributes[SemanticAttributes.HTTP_USER_AGENT] = userAgent; + spanAttributes[SemanticAttributes.USER_AGENT_ORIGINAL] = userAgent; } // Put headers as attributes based on config if (config.headersToSpanAttributes?.requestHeaders) { - config.headersToSpanAttributes.requestHeaders.forEach((name) => { - const headerName = name.toLowerCase(); - const headerValue = reqHeaders.get(headerName); - - if (headerValue) { - const normalizedName = headerName.replace(/-/g, '_'); - spanAttributes[`http.request.header.${normalizedName}`] = headerValue; - } - }); + config.headersToSpanAttributes.requestHeaders + .map((name) => name.toLowerCase()) + .filter((name) => reqHeaders.has(name)) + .forEach((name) => { + spanAttributes[`http.request.header.${name}`] = reqHeaders.get(name); + }); } // Get attributes from the hook if present @@ -251,11 +243,9 @@ export class UndiciInstrumentation extends InstrumentationBase { } const { remoteAddress, remotePort } = socket; - - // TODO: this may be affected by HTTP semconv breaking changes span.setAttributes({ - [SemanticAttributes.NET_PEER_IP]: remoteAddress, - [SemanticAttributes.NET_PEER_PORT]: remotePort, + [SemanticAttributes.NETWORK_PEER_ADDRESS]: remoteAddress, + [SemanticAttributes.NETWORK_PEER_PORT]: remotePort, }); } @@ -273,8 +263,8 @@ export class UndiciInstrumentation extends InstrumentationBase { // We are currently *not* capturing response headers, even though the // intake API does allow it, because none of the other `setHttpContext` // uses currently do - const attrs: Attributes = { - [SemanticAttributes.HTTP_STATUS_CODE]: response.statusCode, + const spanAttributes: Attributes = { + [SemanticAttributes.HTTP_RESPONSE_STATUS_CODE]: response.statusCode, }; // Get headers with names lowercased but values intact @@ -289,23 +279,22 @@ export class UndiciInstrumentation extends InstrumentationBase { // Put response headers as attributes based on config const config = this._getConfig(); if (config.headersToSpanAttributes?.responseHeaders) { - config.headersToSpanAttributes.responseHeaders.forEach((name) => { - const headerName = name.toLowerCase(); - const headerValue = resHeaders.get(headerName); - - if (headerValue) { - const normalizedName = headerName.replace(/-/g, '_'); - attrs[`http.response.header.${normalizedName}`] = headerValue; - } - }); - } - - const contentLength = Number(resHeaders.get('content-length')); - if (!isNaN(contentLength)) { - attrs[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = contentLength; + config.headersToSpanAttributes.responseHeaders + .map((name) => name.toLowerCase()) + .filter((name) => resHeaders.has(name)) + .forEach((name) => { + const key = `http.request.header.${name}`; + const value = resHeaders.get(name); + + if (name === 'content-length' && !isNaN(Number(value))) { + spanAttributes[key] = Number(value); + } else { + spanAttributes[key] = value; + } + }); } - span.setAttributes(attrs); + span.setAttributes(spanAttributes); span.setStatus({ code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET, }); diff --git a/scripts/semconv/generate.sh b/scripts/semconv/generate.sh index dbd99464eac..5c78cba91e5 100755 --- a/scripts/semconv/generate.sh +++ b/scripts/semconv/generate.sh @@ -3,43 +3,103 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/../../" +# Path whre to place the new semanticon conventions +# empty if you want to run the old genarator code +SRC_PATH=$1 + +echo "Path is ${SRC_PATH}" + +# With no path we're generating the leaacy semconv in its package +if [ "$SRC_PATH" == "" ]; then + # freeze the spec version to make SpanAttributess generation reproducible + SPEC_VERSION=v1.7.0 + GENERATOR_VERSION=0.7.0 + + echo "Generating semantic conventions for spec version ${SPEC_VERSION}"; + + cd ${SCRIPT_DIR} + + rm -rf opentelemetry-specification || true + mkdir opentelemetry-specification + cd opentelemetry-specification + + git init + git remote add origin https://github.com/open-telemetry/opentelemetry-specification.git + git fetch origin "$SPEC_VERSION" --depth=1 + git reset --hard FETCH_HEAD + cd ${SCRIPT_DIR} + + docker run --rm \ + -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/trace:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/packages/opentelemetry-semantic-conventions/src/trace/:/output \ + otel/semconvgen:${GENERATOR_VERSION} \ + -f /source \ + code \ + --template /templates/SemanticAttributes.ts.j2 \ + --output /output/SemanticAttributes.ts \ + -Dclass=SemanticAttributes + + docker run --rm \ + -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/resource:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/packages/opentelemetry-semantic-conventions/src/resource/:/output \ + otel/semconvgen:${GENERATOR_VERSION} \ + -f /source \ + code \ + --template /templates/SemanticAttributes.ts.j2 \ + --output /output/SemanticResourceAttributes.ts \ + -Dclass=SemanticResourceAttributes +fi + +# TODO: add comments here. REfs +# - https://github.com/open-telemetry/build-tools/pull/157/files +# - https://github.com/open-telemetry/semantic-conventions-java/blob/2be178a7fd62d1073fa9b4f0f0520772a6496e0b/build.gradle.kts#L107 +if [ "$SRC_PATH" != "" ]; then # freeze the spec version to make SpanAttributess generation reproducible -SPEC_VERSION=v1.7.0 -GENERATOR_VERSION=0.7.0 - -cd ${SCRIPT_DIR} - -rm -rf opentelemetry-specification || true -mkdir opentelemetry-specification -cd opentelemetry-specification - -git init -git remote add origin https://github.com/open-telemetry/opentelemetry-specification.git -git fetch origin "$SPEC_VERSION" --depth=1 -git reset --hard FETCH_HEAD -cd ${SCRIPT_DIR} - -docker run --rm \ - -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/trace:/source \ - -v ${SCRIPT_DIR}/templates:/templates \ - -v ${ROOT_DIR}/packages/opentelemetry-semantic-conventions/src/trace/:/output \ - otel/semconvgen:${GENERATOR_VERSION} \ - -f /source \ - code \ - --template /templates/SemanticAttributes.ts.j2 \ - --output /output/SemanticAttributes.ts \ - -Dclass=SemanticAttributes - -docker run --rm \ - -v ${SCRIPT_DIR}/opentelemetry-specification/semantic_conventions/resource:/source \ - -v ${SCRIPT_DIR}/templates:/templates \ - -v ${ROOT_DIR}/packages/opentelemetry-semantic-conventions/src/resource/:/output \ - otel/semconvgen:${GENERATOR_VERSION} \ - -f /source \ - code \ - --template /templates/SemanticAttributes.ts.j2 \ - --output /output/SemanticResourceAttributes.ts \ - -Dclass=SemanticResourceAttributes + SPEC_VERSION=v1.24.0 + GENERATOR_VERSION=0.23.0 + REPO=semantic-conventions + + echo "Generating semantic conventions for spec version ${SPEC_VERSION}"; + + cd ${SCRIPT_DIR} + + rm -rf ${REPO} || true + mkdir ${REPO} + cd ${REPO} + + git init + git remote add origin https://github.com/open-telemetry/${REPO}.git + git fetch origin "$SPEC_VERSION" --depth=1 + git reset --hard FETCH_HEAD + cd ${SCRIPT_DIR} + + docker run --rm \ + -v ${SCRIPT_DIR}/${REPO}/model:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/${SRC_PATH}/:/output \ + otel/semconvgen:${GENERATOR_VERSION} \ + --only span,event,attribute_group,scope,metric\ + --yaml-root /source \ + code \ + --template /templates/SemanticAttributes.ts.j2 \ + --output /output/SemanticAttributes.ts \ + -Dclass=SemanticAttributes + + docker run --rm \ + -v ${SCRIPT_DIR}/${REPO}/model:/source \ + -v ${SCRIPT_DIR}/templates:/templates \ + -v ${ROOT_DIR}/${SRC_PATH}/:/output \ + otel/semconvgen:${GENERATOR_VERSION} \ + --only resource\ + --yaml-root /source \ + code \ + --template /templates/SemanticAttributes.ts.j2 \ + --output /output/SemanticResourceAttributes.ts \ + -Dclass=SemanticResourceAttributes +fi + # Run the automatic linting fixing task to ensure it will pass eslint cd "$ROOT_DIR" From a616402b077097b00a2167f75ac4a9735d17a4cd Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 26 Jan 2024 11:54:46 +0100 Subject: [PATCH 15/28] chore(instrumentation-undici): add new semconv in tests --- .../src/undici.ts | 20 ++-- .../test/fetch.test.ts | 23 ++-- .../test/utils/assertSpan.ts | 105 ++++++++++-------- 3 files changed, 83 insertions(+), 65 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 5a370dde3cd..71822b74b0a 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -146,9 +146,10 @@ export class UndiciInstrumentation extends InstrumentationBase { return [name, val]; })); - const requestUrl = new URL(request.origin); + const requestUrl = new URL(request.origin + request.path); const spanAttributes: Attributes = { - [SemanticAttributes.URL_FULL]: request.origin, + [SemanticAttributes.HTTP_REQUEST_METHOD]: request.method, + [SemanticAttributes.URL_FULL]: requestUrl.toString(), [SemanticAttributes.URL_PATH]: requestUrl.pathname, [SemanticAttributes.URL_QUERY]: requestUrl.search, }; @@ -283,17 +284,16 @@ export class UndiciInstrumentation extends InstrumentationBase { .map((name) => name.toLowerCase()) .filter((name) => resHeaders.has(name)) .forEach((name) => { - const key = `http.request.header.${name}`; - const value = resHeaders.get(name); - - if (name === 'content-length' && !isNaN(Number(value))) { - spanAttributes[key] = Number(value); - } else { - spanAttributes[key] = value; - } + spanAttributes[`http.response.header.${name}`] = resHeaders.get(name); }); } + // `content-length` header is a special case + const contentLength = Number(resHeaders.get('content-length')); + if (!isNaN(contentLength)) { + spanAttributes['http.response.header.content-length'] = contentLength; + } + span.setAttributes(spanAttributes); span.setStatus({ code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET, diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index cba609be993..f65a265169a 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -132,8 +132,8 @@ describe('UndiciInstrumentation `fetch` tests', function () { hostname: 'localhost', httpStatusCode: response.status, httpMethod: 'GET', - pathname: '/', - path: '/?query=test', + path: '/', + query:'?query=test', resHeaders: response.headers, }); }); @@ -157,8 +157,8 @@ describe('UndiciInstrumentation `fetch` tests', function () { hostname: 'localhost', httpStatusCode: response.status, httpMethod: 'GET', - pathname: '/', - path: '/?query=test', + path: '/', + query:'?query=test', resHeaders: response.headers, }); }); @@ -208,25 +208,26 @@ describe('UndiciInstrumentation `fetch` tests', function () { hostname: 'localhost', httpStatusCode: response.status, httpMethod: 'GET', - pathname: '/', - path: '/?query=test', + path: '/', + query:'?query=test', reqHeaders: reqInit.headers, resHeaders: response.headers, }); + console.log(span.attributes) assert.strictEqual( - span.attributes['http.request.header.foo_client'], + span.attributes['http.request.header.foo-client'], 'bar', - 'request headers are captured', + 'request headers from fetch options are captured', ); assert.strictEqual( - span.attributes['http.request.header.foo_client'], + span.attributes['http.request.header.x-requested-with'], 'bar', 'request headers from requestHook are captured', ); assert.strictEqual( - span.attributes['http.response.header.foo_server'], + span.attributes['http.response.header.foo-server'], 'bar', - 'response headers are captured', + 'response headers from the server are captured', ); assert.strictEqual( span.attributes['test.hook.attribute'], diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index 7dc5eac866f..d4aae0aa353 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -21,10 +21,9 @@ import { } from '@opentelemetry/api'; import { hrTimeToNanoseconds } from '@opentelemetry/core'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import * as assert from 'assert'; // import { DummyPropagation } from './DummyPropagation'; -import { AttributeNames } from '../../src/enums/AttributeNames'; +import { SemanticAttributes } from '../../src/enums/SemanticAttributes'; export const assertSpan = ( span: ReadableSpan, @@ -33,9 +32,9 @@ export const assertSpan = ( httpMethod: string; resHeaders: Headers; hostname: string; - pathname: string; reqHeaders?: Headers; path?: string | null; + query?: string | null; forceStatus?: SpanStatus; noNetPeer?: boolean; // we don't expect net peer info when request throw before being sent error?: Exception; @@ -45,25 +44,38 @@ export const assertSpan = ( assert.strictEqual(span.spanContext().spanId.length, 16); assert.strictEqual(span.kind, SpanKind.CLIENT, 'span.kind is correct'); assert.strictEqual(span.name, `HTTP ${validations.httpMethod}`, 'span.name is correct'); + // TODO: check this + // assert.strictEqual( + // span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], + // span.status.message, + // `attributes['${AttributeNames.HTTP_ERROR_MESSAGE}'] is correct`, + // ); assert.strictEqual( - span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], - span.status.message, - `attributes['${AttributeNames.HTTP_ERROR_MESSAGE}'] is correct`, - ); - assert.strictEqual( - span.attributes[SemanticAttributes.HTTP_METHOD], + span.attributes[SemanticAttributes.HTTP_REQUEST_METHOD], validations.httpMethod, - `attributes['${SemanticAttributes.HTTP_METHOD}'] is correct`, - ); - assert.strictEqual( - span.attributes[SemanticAttributes.HTTP_TARGET], - validations.path || validations.pathname, - `attributes['${SemanticAttributes.HTTP_TARGET}'] is correct`, + `attributes['${SemanticAttributes.HTTP_REQUEST_METHOD}'] is correct`, ); + + if (validations.path) { + assert.strictEqual( + span.attributes[SemanticAttributes.URL_PATH], + validations.path, + `attributes['${SemanticAttributes.URL_PATH}'] is correct`, + ); + } + + if (validations.query) { + assert.strictEqual( + span.attributes[SemanticAttributes.URL_QUERY], + validations.query, + `attributes['${SemanticAttributes.URL_QUERY}'] is correct`, + ); + } + assert.strictEqual( - span.attributes[SemanticAttributes.HTTP_STATUS_CODE], + span.attributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE], validations.httpStatusCode, - `attributes['${SemanticAttributes.HTTP_STATUS_CODE}'] is correct`, + `attributes['${SemanticAttributes.HTTP_RESPONSE_STATUS_CODE}'] is correct`, ); assert.strictEqual(span.links.length, 0, 'there are no links'); @@ -101,44 +113,49 @@ export const assertSpan = ( if (contentLengthHeader) { const contentLength = Number(contentLengthHeader); - const contentEncodingHeader = validations.resHeaders.get('content-encoding'); - if ( - contentEncodingHeader && - contentEncodingHeader !== 'identity' - ) { - assert.strictEqual( - span.attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH], - contentLength - ); - } else { - assert.strictEqual( - span.attributes[ - SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED - ], - contentLength - ); - } + assert.strictEqual( + span.attributes['http.response.header.content-length'], + contentLength + ); + // TODO: check compresssed/uncompressed in semantic conventions + // const contentEncodingHeader = validations.resHeaders.get('content-encoding'); + // if ( + // contentEncodingHeader && + // contentEncodingHeader !== 'identity' + // ) { + // assert.strictEqual( + // span.attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH], + // contentLength + // ); + // } else { + // assert.strictEqual( + // span.attributes[ + // SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED + // ], + // contentLength + // ); + // } } assert.strictEqual( - span.attributes[SemanticAttributes.NET_PEER_NAME], + span.attributes[SemanticAttributes.SERVER_ADDRESS], validations.hostname, - 'must be consistent (PEER_NAME and hostname)' + 'must be consistent (SERVER_ADDRESS and hostname)' ); if (!validations.noNetPeer) { assert.ok( - span.attributes[SemanticAttributes.NET_PEER_IP], - 'must have PEER_IP' + span.attributes[SemanticAttributes.NETWORK_PEER_ADDRESS], + `must have ${SemanticAttributes.NETWORK_PEER_ADDRESS}` ); assert.ok( - span.attributes[SemanticAttributes.NET_PEER_PORT], - 'must have PEER_PORT' + span.attributes[SemanticAttributes.NETWORK_PEER_PORT], + `must have ${SemanticAttributes.NETWORK_PEER_PORT}` ); } assert.ok( - (span.attributes[SemanticAttributes.HTTP_URL] as string).indexOf( - span.attributes[SemanticAttributes.NET_PEER_NAME] as string + (span.attributes[SemanticAttributes.URL_FULL] as string).indexOf( + span.attributes[SemanticAttributes.SERVER_ADDRESS] as string ) > -1, - 'must be consistent' + `${SemanticAttributes.URL_FULL} & ${SemanticAttributes.SERVER_ADDRESS} must be consistent` ); @@ -146,7 +163,7 @@ export const assertSpan = ( const userAgent = validations.reqHeaders.get('user-agent'); if (userAgent) { assert.strictEqual( - span.attributes[SemanticAttributes.HTTP_USER_AGENT], + span.attributes[SemanticAttributes.USER_AGENT_ORIGINAL], userAgent ); } From c5ca3dcb13e9c18af68696157e688819435495fa Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 26 Jan 2024 12:11:35 +0100 Subject: [PATCH 16/28] chore(instrumentation-undici): update tests for headers --- .../src/internal-types.ts | 7 ++- .../src/undici.ts | 43 ++++++++++++------- .../test/fetch.test.ts | 5 +-- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts index 4206664c565..45e1133fe5a 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts @@ -27,7 +27,12 @@ export interface RequestMessage { request: UndiciRequest; } -export interface HeadersMessage { +export interface RequestHeadersMessage { + request: UndiciRequest; + socket: any; +} + +export interface ResponseHeadersMessage { request: UndiciRequest; response: UnidiciResponse; } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 71822b74b0a..9cbf6df4e4a 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -31,7 +31,7 @@ import { import { VERSION } from './version'; -import { HeadersMessage, ListenerRecord, RequestMessage } from './internal-types'; +import { HeadersMessage, ListenerRecord, RequestHeadersMessage, RequestMessage, ResponseHeadersMessage } from './internal-types'; import { UndiciInstrumentationConfig, UndiciRequest } from './types'; import { SemanticAttributes } from './enums/SemanticAttributes'; @@ -168,18 +168,7 @@ export class UndiciInstrumentation extends InstrumentationBase { spanAttributes[SemanticAttributes.USER_AGENT_ORIGINAL] = userAgent; } - // Put headers as attributes based on config - if (config.headersToSpanAttributes?.requestHeaders) { - config.headersToSpanAttributes.requestHeaders - .map((name) => name.toLowerCase()) - .filter((name) => reqHeaders.has(name)) - .forEach((name) => { - spanAttributes[`http.request.header.${name}`] = reqHeaders.get(name); - }); - } - // Get attributes from the hook if present - const hookAttributes = safeExecuteInTheMiddle( () => config.startSpanHook?.(request), (e) => e && this._diag.error('caught startSpanHook error: ', e), @@ -235,7 +224,7 @@ export class UndiciInstrumentation extends InstrumentationBase { // This is the 2nd message we recevie for each request. It is fired when connection with // the remote is stablished and about to send the first byte. Here do have info about the // remote addres an port so we can poupulate some `net.*` attributes into the span - private onRequestHeaders({ request, socket }: any): void { + private onRequestHeaders({ request, socket }: RequestHeadersMessage): void { console.log('onRequestHeaders') const span = this._spanFromReq.get(request as UndiciRequest); @@ -243,17 +232,39 @@ export class UndiciInstrumentation extends InstrumentationBase { return } + const config = this._getConfig(); const { remoteAddress, remotePort } = socket; - span.setAttributes({ + const spanAttributes: Attributes = { [SemanticAttributes.NETWORK_PEER_ADDRESS]: remoteAddress, [SemanticAttributes.NETWORK_PEER_PORT]: remotePort, - }); + }; + + // After hooks have been processed (which may modify request headers) + // we can collect the headers based on the configuration + const rawHeaders = request.headers.split('\r\n'); + const reqHeaders = new Map(rawHeaders.map(h => { + const sepIndex = h.indexOf(':'); + const name = h.substring(0, sepIndex).toLowerCase(); + const val = h.substring(sepIndex + 1).trim(); + return [name, val]; + })); + + if (config.headersToSpanAttributes?.requestHeaders) { + config.headersToSpanAttributes.requestHeaders + .map((name) => name.toLowerCase()) + .filter((name) => reqHeaders.has(name)) + .forEach((name) => { + spanAttributes[`http.request.header.${name}`] = reqHeaders.get(name); + }); + } + + span.setAttributes(spanAttributes); } // This is the 3rd message we get for each request and it's fired when the server // headers are received, body may not be accessible yet (TODO: check this). // From the response headers we can set the status and content length - private onResponseHeaders({ request, response }: HeadersMessage): void { + private onResponseHeaders({ request, response }: ResponseHeadersMessage): void { console.log('onResponseHeaders') const span = this._spanFromReq.get(request); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index f65a265169a..ce4f3014e74 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -174,7 +174,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { return req.path.indexOf('/ignore/path') !== -1; }, requestHook: (span, req) => { - // TODO: maybe an intermediate request with better API + // TODO: maybe an intermediate request with better API req.headers += 'x-requested-with: undici\r\n'; }, startSpanHook: (request) => { @@ -213,7 +213,6 @@ describe('UndiciInstrumentation `fetch` tests', function () { reqHeaders: reqInit.headers, resHeaders: response.headers, }); - console.log(span.attributes) assert.strictEqual( span.attributes['http.request.header.foo-client'], 'bar', @@ -221,7 +220,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { ); assert.strictEqual( span.attributes['http.request.header.x-requested-with'], - 'bar', + 'undici', 'request headers from requestHook are captured', ); assert.strictEqual( From 8ebdfc8f2abd80f61b1f9b2030864285b3b4cee5 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 26 Jan 2024 15:43:31 +0100 Subject: [PATCH 17/28] chore(instrumentation-undici): add tests for error capturing --- .../test/fetch.test.ts | 61 ++++++++++++++++--- .../test/utils/assertSpan.ts | 6 +- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index ce4f3014e74..56483fd1057 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert'; -import { context, propagation } from '@opentelemetry/api'; +import { SpanStatusCode, context, propagation } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { InMemorySpanExporter, @@ -55,11 +55,18 @@ describe('UndiciInstrumentation `fetch` tests', function () { context.setGlobalContextManager(new AsyncHooksContextManager().enable()); mockServer.start(done); mockServer.mockListener((req, res) => { - res.statusCode = 200; - res.setHeader('content-type', 'application/json'); - res.setHeader('foo-server', 'bar'); - res.write(JSON.stringify({ success: true })); - res.end(); + if (req.url === '/throw') { + res.statusCode = 500; + res.setHeader('content-type', 'text/plain'); + res.write('Intenal server error :('); + res.end(); + } else { + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.setHeader('foo-server', 'bar'); + res.write(JSON.stringify({ success: true })); + res.end(); + } }); }); @@ -95,7 +102,8 @@ describe('UndiciInstrumentation `fetch` tests', function () { instrumentation.enable(); }); afterEach(function () { - instrumentation.disable(); + // Empty configuration & disable + instrumentation.setConfig({ enabled: false }); }); it('should create valid spans even if the configuration hooks fail', async function () { @@ -142,9 +150,6 @@ describe('UndiciInstrumentation `fetch` tests', function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - // Empty configuration - instrumentation.setConfig({ enabled: true }); - const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const response = await fetch(fetchUrl); @@ -235,6 +240,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { ); }); + // TODO: another test with a parent span. Check HTTP tests it('should not create spans without parent if configured', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -250,5 +256,40 @@ describe('UndiciInstrumentation `fetch` tests', function () { spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0, 'no spans are created'); }); + + + it('should capture erros from the server', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/throw`; + let fetchError; + try { + const fetchUrl = `http://unexistent-host-name/path`; + await fetch(fetchUrl); + } catch (err) { + // Expected error since webdav schema is not supported + fetchError = err; + } + + await new Promise((r) => setTimeout(r, 10)); + + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'unexistent-host-name', + httpMethod: 'GET', + path: '/path', + error: fetchError, + noNetPeer: true, // do not check network attribs + forceStatus: { + code: SpanStatusCode.ERROR, + message: 'getaddrinfo ENOTFOUND unexistent-host-name' + } + }); + }); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index d4aae0aa353..8f84a4b01cd 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -30,7 +30,7 @@ export const assertSpan = ( validations: { httpStatusCode?: number; httpMethod: string; - resHeaders: Headers; + resHeaders?: Headers; hostname: string; reqHeaders?: Headers; path?: string | null; @@ -101,7 +101,7 @@ export const assertSpan = ( assert.deepStrictEqual( span.status, validations.forceStatus || { - code: isStatusUnset ? SpanStatusCode.UNSET : SpanStatusCode.ERROR + code: isStatusUnset ? SpanStatusCode.UNSET : SpanStatusCode.ERROR, }, 'span status is correct' ); @@ -109,7 +109,7 @@ export const assertSpan = ( assert.ok(span.endTime, 'must be finished'); assert.ok(hrTimeToNanoseconds(span.duration) > 0, 'must have positive duration'); - const contentLengthHeader = validations.resHeaders.get('content-length'); + const contentLengthHeader = validations.resHeaders?.get('content-length'); if (contentLengthHeader) { const contentLength = Number(contentLengthHeader); From bd61d755da4820d156a7f42d4e78fba6d27db719 Mon Sep 17 00:00:00 2001 From: David Luna Date: Sun, 28 Jan 2024 10:30:14 +0100 Subject: [PATCH 18/28] chore(instrumentation-undici): add context propagation --- .../src/undici.ts | 12 +-- .../test/fetch.test.ts | 87 +++++++++++++------ .../test/utils/assertSpan.ts | 2 - .../test/utils/mock-propagation.ts | 52 +++++++++++ 4 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 9cbf6df4e4a..b4e680f232c 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -189,7 +189,6 @@ export class UndiciInstrumentation extends InstrumentationBase { const currentSpan = trace.getSpan(activeCtx); let span: Span; - if (config.requireParentforSpans && !currentSpan) { span = trace.wrapSpanContext(INVALID_SPAN_CONTEXT); } else { @@ -203,11 +202,6 @@ export class UndiciInstrumentation extends InstrumentationBase { ); } - // Context propagation - const requestContext = trace.setSpan(context.active(), span); - const addedHeaders: Record = {}; - propagation.inject(requestContext, addedHeaders); - // Execute the request hook if defined safeExecuteInTheMiddle( () => config.requestHook?.(span, request), @@ -215,6 +209,12 @@ export class UndiciInstrumentation extends InstrumentationBase { true, ); + // Context propagation goes last so no hook can tamper + // the propagation headers + const requestContext = trace.setSpan(context.active(), span); + const addedHeaders: Record = {}; + propagation.inject(requestContext, addedHeaders); + request.headers += Object.entries(addedHeaders) .map(([k, v]) => `${k}: ${v}\r\n`) .join(''); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 56483fd1057..9dbc2fa6d06 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -25,6 +25,7 @@ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { UndiciInstrumentation } from '../src/undici'; +import { MockPropagation } from './utils/mock-propagation'; import { MockServer } from './utils/mock-server'; import { assertSpan } from './utils/assertSpan'; @@ -51,22 +52,35 @@ describe('UndiciInstrumentation `fetch` tests', function () { } // TODO: mock propagation and test it - // propagation.setGlobalPropagator(new DummyPropagation()); + propagation.setGlobalPropagator(new MockPropagation()); context.setGlobalContextManager(new AsyncHooksContextManager().enable()); mockServer.start(done); mockServer.mockListener((req, res) => { - if (req.url === '/throw') { - res.statusCode = 500; - res.setHeader('content-type', 'text/plain'); - res.write('Intenal server error :('); - res.end(); - } else { - res.statusCode = 200; - res.setHeader('content-type', 'application/json'); - res.setHeader('foo-server', 'bar'); - res.write(JSON.stringify({ success: true })); - res.end(); + // There are some situations where there is no way to access headers + // for trace propagation asserts like: + // const resp = await fetch('http://host:port') + // so we need to do the assertion here + try { + assert.ok( + req.headers[MockPropagation.TRACE_CONTEXT_KEY], + `trace propagation for ${MockPropagation.TRACE_CONTEXT_KEY} works`, + ); + assert.ok( + req.headers[MockPropagation.SPAN_CONTEXT_KEY], + `trace propagation for ${MockPropagation.SPAN_CONTEXT_KEY} works`, + ); + } catch (assertErr) { + // The exception will hang the server and the test so we set a header + // back to the test to make an assertion + res.setHeader('propagation-error', assertErr.message); } + + // Retur a valid response always + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.setHeader('foo-server', 'bar'); + res.write(JSON.stringify({ success: true })); + res.end(); }); }); @@ -90,7 +104,11 @@ describe('UndiciInstrumentation `fetch` tests', function () { instrumentation.setConfig({ enabled: false }); const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - await fetch(fetchUrl); + const response = await fetch(fetchUrl); + assert.ok( + response.headers.get('propagation-error') != null, + 'propagation is not set if instrumentation disabled' + ); spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0, 'no spans are created'); @@ -129,13 +147,16 @@ describe('UndiciInstrumentation `fetch` tests', function () { const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const response = await fetch(fetchUrl); + assert.ok( + response.headers.get('propagation-error') == null, + 'propagation is set for instrumented requests' + ); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); - // console.dir(span, { depth: 9 }); assertSpan(span, { hostname: 'localhost', httpStatusCode: response.status, @@ -152,6 +173,10 @@ describe('UndiciInstrumentation `fetch` tests', function () { const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const response = await fetch(fetchUrl); + assert.ok( + response.headers.get('propagation-error') == null, + 'propagation is set for instrumented requests' + ); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; @@ -194,29 +219,36 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); // Do some requests - await fetch(`${protocol}://${hostname}:${mockServer.port}/ignore/path`); + const ignoreResponse = await fetch(`${protocol}://${hostname}:${mockServer.port}/ignore/path`); const reqInit = { headers: new Headers({ 'user-agent': 'custom', 'foo-client': 'bar' }), }; - const response = await fetch(`${protocol}://${hostname}:${mockServer.port}/?query=test`, reqInit); + assert.ok( + ignoreResponse.headers.get('propagation-error'), + 'propagation is not set for ignored requests' + ); + + const queryResponse = await fetch(`${protocol}://${hostname}:${mockServer.port}/?query=test`, reqInit); + assert.ok( + queryResponse.headers.get('propagation-error') == null, + 'propagation is set for instrumented requests' + ); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; - // TODO: remove this when test finished - // console.dir(span, { depth: 9 }); assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); assertSpan(span, { hostname: 'localhost', - httpStatusCode: response.status, + httpStatusCode: queryResponse.status, httpMethod: 'GET', path: '/', query:'?query=test', reqHeaders: reqInit.headers, - resHeaders: response.headers, + resHeaders: queryResponse.headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], @@ -251,30 +283,31 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - await fetch(fetchUrl); + const response = await fetch(fetchUrl); + // TODO: should we propagate here???? + // assert.ok( + // response.headers.get('propagation-error') == null, + // 'propagation is set for instrumented requests' + // ); spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0, 'no spans are created'); }); - it('should capture erros from the server', async function () { + it('should capture errors using fetch API', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - // const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/throw`; let fetchError; try { const fetchUrl = `http://unexistent-host-name/path`; await fetch(fetchUrl); } catch (err) { - // Expected error since webdav schema is not supported + // Expected error fetchError = err; } - await new Promise((r) => setTimeout(r, 10)); - - spans = memoryExporter.getFinishedSpans(); const span = spans[0]; assert.ok(span, 'a span is present'); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index 8f84a4b01cd..45ecc484ad7 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -167,7 +167,5 @@ export const assertSpan = ( userAgent ); } - // assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); - // assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); } }; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts new file mode 100644 index 00000000000..e9cbe9d80b2 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ +import { + Context, + TextMapPropagator, + trace, + TraceFlags, +} from '@opentelemetry/api'; + +export class MockPropagation implements TextMapPropagator { + static TRACE_CONTEXT_KEY = 'x-mock-trace-id'; + static SPAN_CONTEXT_KEY = 'x-mock-span-id'; + extract(context: Context, carrier: Record) { + const extractedSpanContext = { + traceId: carrier[MockPropagation.TRACE_CONTEXT_KEY] as string, + spanId: carrier[MockPropagation.SPAN_CONTEXT_KEY] as string, + traceFlags: TraceFlags.SAMPLED, + isRemote: true, + }; + if (extractedSpanContext.traceId && extractedSpanContext.spanId) { + return trace.setSpanContext(context, extractedSpanContext); + } + return context; + } + inject(context: Context, carrier: Record): void { + const spanContext = trace.getSpanContext(context); + + if (spanContext) { + carrier[MockPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId; + carrier[MockPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId; + } + } + fields(): string[] { + return [ + MockPropagation.TRACE_CONTEXT_KEY, + MockPropagation.SPAN_CONTEXT_KEY, + ]; + } +} From 62cdcc647b7e81b495b6910244f3f7bd845c6624 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 5 Feb 2024 16:08:45 +0100 Subject: [PATCH 19/28] chore(instrumentation-undici): add test for requireParentSpan config --- .../test/fetch.test.ts | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 9dbc2fa6d06..07023b4d504 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert'; -import { SpanStatusCode, context, propagation } from '@opentelemetry/api'; +import { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { InMemorySpanExporter, @@ -273,7 +273,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); // TODO: another test with a parent span. Check HTTP tests - it('should not create spans without parent if configured', async function () { + it('should not create spans without parent if required in configuration', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -284,16 +284,60 @@ describe('UndiciInstrumentation `fetch` tests', function () { const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const response = await fetch(fetchUrl); - // TODO: should we propagate here???? - // assert.ok( - // response.headers.get('propagation-error') == null, - // 'propagation is set for instrumented requests' - // ); + // TODO: here we're checking the propagation works even if the instrumentation + // is not starting any span. Not 100% sure this is the behaviour we want + assert.ok( + response.headers.get('propagation-error') == null, + 'propagation is set for instrumented requests' + ); spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0, 'no spans are created'); }); + it('should not create spans with parent if required in configuration', function (done) { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + instrumentation.setConfig({ + enabled: true, + requireParentforSpans: true, + }); + + const tracer = provider.getTracer('default'); + const span = tracer.startSpan('parentSpan', { + kind: SpanKind.INTERNAL, + }); + + context.with(trace.setSpan(context.active(), span), async () => { + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const response = await fetch(fetchUrl); + + span.end(); + // TODO: here we're checking the propagation works even if the instrumentation + // is not starting any span. Not 100% sure this is the behaviour we want + assert.ok( + response.headers.get('propagation-error') == null, + 'propagation is set for instrumented requests' + ); + + spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2, 'child span is created'); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.CLIENT).length, + 1, + 'child span is created' + ); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.INTERNAL).length, + 1, + 'parent span is present' + ); + + done(); + }); + }); + it('should capture errors using fetch API', async function () { let spans = memoryExporter.getFinishedSpans(); From f9b40f23033ed746a1b49a8df1ced82145a0fe4f Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 5 Feb 2024 17:24:01 +0100 Subject: [PATCH 20/28] chore(instrumentation-undici): add tests for undici request --- .../src/undici.ts | 4 - .../test/undici.test.ts | 369 ++++++++++++++++++ .../test/utils/assertSpan.ts | 67 ++-- 3 files changed, 407 insertions(+), 33 deletions(-) create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index b4e680f232c..32ee303563d 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -122,7 +122,6 @@ export class UndiciInstrumentation extends InstrumentationBase { // create the span and populate some atttributes, then link the span to the request for further // span processing private onRequestCreated({ request }: RequestMessage): void { - console.log('onRequestCreated') // Ignore if: // - instrumentation is disabled // - ignored by config @@ -225,7 +224,6 @@ export class UndiciInstrumentation extends InstrumentationBase { // the remote is stablished and about to send the first byte. Here do have info about the // remote addres an port so we can poupulate some `net.*` attributes into the span private onRequestHeaders({ request, socket }: RequestHeadersMessage): void { - console.log('onRequestHeaders') const span = this._spanFromReq.get(request as UndiciRequest); if (!span) { @@ -265,7 +263,6 @@ export class UndiciInstrumentation extends InstrumentationBase { // headers are received, body may not be accessible yet (TODO: check this). // From the response headers we can set the status and content length private onResponseHeaders({ request, response }: ResponseHeadersMessage): void { - console.log('onResponseHeaders') const span = this._spanFromReq.get(request); if (!span) { @@ -314,7 +311,6 @@ export class UndiciInstrumentation extends InstrumentationBase { // This is the last event we receive if the request went without any errors (TODO: check this) private onDone({ request }: any): void { - console.log('onDone') const span = this._spanFromReq.get(request); if (!span) { diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts new file mode 100644 index 00000000000..f55cea8e718 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts @@ -0,0 +1,369 @@ +/* + * 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. + */ +import * as assert from 'assert'; + +import { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; + +import { UndiciInstrumentation } from '../src/undici'; + +import { MockPropagation } from './utils/mock-propagation'; +import { MockServer } from './utils/mock-server'; +import { assertSpan } from './utils/assertSpan'; + +const instrumentation = new UndiciInstrumentation(); +instrumentation.enable(); +instrumentation.disable(); + +import { request } from 'undici'; + +const protocol = 'http'; +const hostname = 'localhost'; +const mockServer = new MockServer(); +const memoryExporter = new InMemorySpanExporter(); +const provider = new NodeTracerProvider(); +provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); +instrumentation.setTracerProvider(provider); + +describe('UndiciInstrumentation `undici` tests', function () { + before(function (done) { + // TODO: mock propagation and test it + propagation.setGlobalPropagator(new MockPropagation()); + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + mockServer.start(done); + mockServer.mockListener((req, res) => { + // There are some situations where there is no way to access headers + // for trace propagation asserts like: + // const resp = await fetch('http://host:port') + // so we need to do the assertion here + try { + assert.ok( + req.headers[MockPropagation.TRACE_CONTEXT_KEY], + `trace propagation for ${MockPropagation.TRACE_CONTEXT_KEY} works`, + ); + assert.ok( + req.headers[MockPropagation.SPAN_CONTEXT_KEY], + `trace propagation for ${MockPropagation.SPAN_CONTEXT_KEY} works`, + ); + } catch (assertErr) { + // The exception will hang the server and the test so we set a header + // back to the test to make an assertion + res.setHeader('propagation-error', assertErr.message); + } + + // Retur a valid response always + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.setHeader('foo-server', 'bar'); + res.write(JSON.stringify({ success: true })); + res.end(); + }); + }); + + after(function(done) { + context.disable(); + propagation.disable(); + mockServer.mockListener(undefined); + mockServer.stop(done); + }); + + beforeEach(function () { + memoryExporter.reset(); + }); + + describe('disable()', function () { + it('should not create spans when disabled', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Disable via config + instrumentation.setConfig({ enabled: false }); + + const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const { headers } = await request(requestUrl); + assert.ok( + headers['propagation-error'] != null, + 'propagation is not set if instrumentation disabled' + ); + + spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0, 'no spans are created'); + }); + }); + + describe('enable()', function () { + beforeEach(function () { + instrumentation.enable(); + }); + afterEach(function () { + // Empty configuration & disable + instrumentation.setConfig({ enabled: false }); + }); + + it('should create valid spans even if the configuration hooks fail', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Set the bad configuration + instrumentation.setConfig({ + enabled: true, + ignoreRequestHook: () => { + throw new Error('ignoreRequestHook error'); + }, + applyCustomAttributesOnSpan: () => { + throw new Error('ignoreRequestHook error'); + }, + requestHook: () => { + throw new Error('requestHook error'); + }, + startSpanHook: () => { + throw new Error('startSpanHook error'); + }, + }) + + const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const { headers, statusCode } = await request(requestUrl); + assert.ok( + headers['propagation-error'] == null, + 'propagation is set for instrumented requests' + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: statusCode, + httpMethod: 'GET', + path: '/', + query:'?query=test', + resHeaders: headers, + }); + }); + + it('should create valid spans with empty configuration', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const { headers, statusCode } = await request(requestUrl); + assert.ok( + headers['propagation-error'] == null, + 'propagation is set for instrumented requests' + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: statusCode, + httpMethod: 'GET', + path: '/', + query:'?query=test', + resHeaders: headers, + }); + }); + + it('should create valid spans with the given configuration', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Set configuration + instrumentation.setConfig({ + enabled: true, + ignoreRequestHook: (req) => { + return req.path.indexOf('/ignore/path') !== -1; + }, + requestHook: (span, req) => { + // TODO: maybe an intermediate request with better API + req.headers += 'x-requested-with: undici\r\n'; + }, + startSpanHook: (request) => { + return { + 'test.hook.attribute': 'hook-value', + }; + }, + headersToSpanAttributes: { + requestHeaders: ['foo-client', 'x-requested-with'], + responseHeaders: ['foo-server'], + } + }); + + // Do some requests + + const headers = { + 'user-agent': 'custom', + 'foo-client': 'bar' + }; + const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; + const ignoreResponse = await request(ignoreRequestUrl, { headers }); + assert.ok( + ignoreResponse.headers['propagation-error'], + 'propagation is not set for ignored requests' + ); + + const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const queryResponse = await request(queryRequestUrl, { headers }); + assert.ok( + queryResponse.headers['propagation-error'] == null, + 'propagation is set for instrumented requests' + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: queryResponse.statusCode, + httpMethod: 'GET', + path: '/', + query:'?query=test', + reqHeaders: headers, + resHeaders: queryResponse.headers, + }); + assert.strictEqual( + span.attributes['http.request.header.foo-client'], + 'bar', + 'request headers from fetch options are captured', + ); + assert.strictEqual( + span.attributes['http.request.header.x-requested-with'], + 'undici', + 'request headers from requestHook are captured', + ); + assert.strictEqual( + span.attributes['http.response.header.foo-server'], + 'bar', + 'response headers from the server are captured', + ); + assert.strictEqual( + span.attributes['test.hook.attribute'], + 'hook-value', + 'startSpanHook is called', + ); + }); + + // TODO: another test with a parent span. Check HTTP tests + it('should not create spans without parent if required in configuration', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + instrumentation.setConfig({ + enabled: true, + requireParentforSpans: true, + }); + + const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const response = await request(requestUrl); + // TODO: here we're checking the propagation works even if the instrumentation + // is not starting any span. Not 100% sure this is the behaviour we want + assert.ok( + response.headers['propagation-error'] == null, + 'propagation is set for instrumented requests' + ); + + spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0, 'no spans are created'); + }); + + it('should not create spans with parent if required in configuration', function (done) { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + instrumentation.setConfig({ + enabled: true, + requireParentforSpans: true, + }); + + const tracer = provider.getTracer('default'); + const span = tracer.startSpan('parentSpan', { + kind: SpanKind.INTERNAL, + }); + + context.with(trace.setSpan(context.active(), span), async () => { + const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const response = await request(requestUrl); + + + span.end(); + // TODO: here we're checking the propagation works even if the instrumentation + // is not starting any span. Not 100% sure this is the behaviour we want + assert.ok( + response.headers['propagation-error'] == null, + 'propagation is set for instrumented requests' + ); + + spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2, 'child span is created'); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.CLIENT).length, + 1, + 'child span is created' + ); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.INTERNAL).length, + 1, + 'parent span is present' + ); + + done(); + }); + }); + + + it('should capture errors using fetch API', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + let fetchError; + try { + const requestUrl = 'http://unexistent-host-name/path'; + await request(requestUrl); + + } catch (err) { + // Expected error + fetchError = err; + } + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'unexistent-host-name', + httpMethod: 'GET', + path: '/path', + error: fetchError, + noNetPeer: true, // do not check network attribs + forceStatus: { + code: SpanStatusCode.ERROR, + message: 'getaddrinfo ENOTFOUND unexistent-host-name' + } + }); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index 45ecc484ad7..d137e8294c4 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -24,15 +24,16 @@ import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import * as assert from 'assert'; // import { DummyPropagation } from './DummyPropagation'; import { SemanticAttributes } from '../../src/enums/SemanticAttributes'; +import type { IncomingHttpHeaders } from 'undici/types/header'; export const assertSpan = ( span: ReadableSpan, validations: { httpStatusCode?: number; httpMethod: string; - resHeaders?: Headers; + resHeaders?: Headers | IncomingHttpHeaders; hostname: string; - reqHeaders?: Headers; + reqHeaders?: Headers | IncomingHttpHeaders; path?: string | null; query?: string | null; forceStatus?: SpanStatus; @@ -109,33 +110,39 @@ export const assertSpan = ( assert.ok(span.endTime, 'must be finished'); assert.ok(hrTimeToNanoseconds(span.duration) > 0, 'must have positive duration'); - const contentLengthHeader = validations.resHeaders?.get('content-length'); - if (contentLengthHeader) { - const contentLength = Number(contentLengthHeader); - - assert.strictEqual( - span.attributes['http.response.header.content-length'], - contentLength - ); - // TODO: check compresssed/uncompressed in semantic conventions - // const contentEncodingHeader = validations.resHeaders.get('content-encoding'); - // if ( - // contentEncodingHeader && - // contentEncodingHeader !== 'identity' - // ) { - // assert.strictEqual( - // span.attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH], - // contentLength - // ); - // } else { - // assert.strictEqual( - // span.attributes[ - // SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED - // ], - // contentLength - // ); - // } + if (validations.resHeaders) { + const contentLengthHeader = validations.resHeaders instanceof Headers ? + validations.resHeaders.get('content-length') : + validations.resHeaders['content-length']; + + if (contentLengthHeader) { + const contentLength = Number(contentLengthHeader); + + assert.strictEqual( + span.attributes['http.response.header.content-length'], + contentLength + ); + // TODO: check compresssed/uncompressed in semantic conventions + // const contentEncodingHeader = validations.resHeaders.get('content-encoding'); + // if ( + // contentEncodingHeader && + // contentEncodingHeader !== 'identity' + // ) { + // assert.strictEqual( + // span.attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH], + // contentLength + // ); + // } else { + // assert.strictEqual( + // span.attributes[ + // SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED + // ], + // contentLength + // ); + // } + } } + assert.strictEqual( span.attributes[SemanticAttributes.SERVER_ADDRESS], validations.hostname, @@ -160,7 +167,9 @@ export const assertSpan = ( if (validations.reqHeaders) { - const userAgent = validations.reqHeaders.get('user-agent'); + const userAgent = validations.reqHeaders instanceof Headers ? + validations.reqHeaders.get('user-agent') : + validations.reqHeaders['user-agent']; if (userAgent) { assert.strictEqual( span.attributes[SemanticAttributes.USER_AGENT_ORIGINAL], From 91831088db3727d4b0f69af56ee5590f36a2a1f0 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 5 Feb 2024 22:34:50 +0100 Subject: [PATCH 21/28] chore(instrumentation-undici): add tests for request/fetch abort use cases --- .../src/internal-types.ts | 2 +- .../src/undici.ts | 36 ++++++++++-------- .../test/fetch.test.ts | 38 ++++++++++++++++++- .../test/undici.test.ts | 37 ++++++++++++++++++ .../test/utils/assertSpan.ts | 2 +- 5 files changed, 96 insertions(+), 19 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts index 45e1133fe5a..e3a12c3d140 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/internal-types.ts @@ -15,7 +15,7 @@ */ import type { Channel } from 'diagnostics_channel'; -import { UndiciRequest, UnidiciResponse }from './types'; +import { UndiciRequest, UnidiciResponse } from './types'; export interface ListenerRecord { name: string; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 32ee303563d..d64bef37c2f 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -31,7 +31,7 @@ import { import { VERSION } from './version'; -import { HeadersMessage, ListenerRecord, RequestHeadersMessage, RequestMessage, ResponseHeadersMessage } from './internal-types'; +import { ListenerRecord, RequestHeadersMessage, RequestMessage, ResponseHeadersMessage } from './internal-types'; import { UndiciInstrumentationConfig, UndiciRequest } from './types'; import { SemanticAttributes } from './enums/SemanticAttributes'; @@ -125,7 +125,7 @@ export class UndiciInstrumentation extends InstrumentationBase { // Ignore if: // - instrumentation is disabled // - ignored by config - // - method is 'CONNECT' (TODO: check for limitations) + // - method is 'CONNECT' const config = this._getConfig(); const shouldIgnoreReq = safeExecuteInTheMiddle( () => !config.enabled || request.method === 'CONNECT' || config.ignoreRequestHook?.(request), @@ -179,11 +179,10 @@ export class UndiciInstrumentation extends InstrumentationBase { }); } - // TODO: check parent if added in config and: - // - create a span if confgi false - // - create a noop span if parent not present and config true - // If a parent is required but not present, we use a `NoopSpan` to still - // propagate context without recording it. + // Check if parent span is required via config and: + // - ff a parent is required but not present, we use a `NoopSpan` to still + // propagate context without recording it. + // - create a span otherwise const activeCtx = context.active(); const currentSpan = trace.getSpan(activeCtx); let span: Span; @@ -260,7 +259,7 @@ export class UndiciInstrumentation extends InstrumentationBase { } // This is the 3rd message we get for each request and it's fired when the server - // headers are received, body may not be accessible yet (TODO: check this). + // headers are received, body may not be accessible yet. // From the response headers we can set the status and content length private onResponseHeaders({ request, response }: ResponseHeadersMessage): void { const span = this._spanFromReq.get(request); @@ -309,7 +308,7 @@ export class UndiciInstrumentation extends InstrumentationBase { } - // This is the last event we receive if the request went without any errors (TODO: check this) + // This is the last event we receive if the request went without any errors private onDone({ request }: any): void { const span = this._spanFromReq.get(request); @@ -321,13 +320,12 @@ export class UndiciInstrumentation extends InstrumentationBase { this._spanFromReq.delete(request); } - // TODO: check this - // This messge is triggered if there is any error in the request - // TODO: in `undici@6.3.0` when request aborted the error type changes from - // a custom error (`RequestAbortedError`) to a built-in `DOMException` so - // - `code` is from DOMEXception (ABORT_ERR: 20) - // - `message` changes - // - stacktrace is smaller and contains node internal frames + // This is the event we get when something is wrong in the request like + // - invalid options + // - connectivity errors such as unreachable host + // - requests aborted through a signal + // NOTE: server errors are considered valid responses and it's the lib consumer + // whi should deal with that. private onError({ request, error }: any): void { const span = this._spanFromReq.get(request); @@ -335,6 +333,12 @@ export class UndiciInstrumentation extends InstrumentationBase { return; } + // NOTE: in `undici@6.3.0` when request aborted the error type changes from + // a custom error (`RequestAbortedError`) to a built-in `DOMException` carrying + // some differences: + // - `code` is from DOMEXception (ABORT_ERR: 20) + // - `message` changes + // - stacktrace is smaller and contains node internal frames span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 07023b4d504..91a77e1746f 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -272,7 +272,6 @@ describe('UndiciInstrumentation `fetch` tests', function () { ); }); - // TODO: another test with a parent span. Check HTTP tests it('should not create spans without parent if required in configuration', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -368,5 +367,42 @@ describe('UndiciInstrumentation `fetch` tests', function () { } }); }); + + it('should capture error if fetch request is aborted', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + let fetchError; + const controller = new AbortController(); + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const fetchPromise = fetch(fetchUrl, { signal: controller.signal }); + controller.abort(); + try { + await fetchPromise; + } catch (err) { + // Expected error + fetchError = err; + } + + // Let the error be published to diagnostics channel + await new Promise((r) => setTimeout(r,5)); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpMethod: 'GET', + path: '/', + query:'?query=test', + error: fetchError, + noNetPeer: true, // do not check network attribs + forceStatus: { + code: SpanStatusCode.ERROR, + message: 'The operation was aborted.' + } + }); + }); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts index f55cea8e718..97b31e5ea71 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts @@ -365,5 +365,42 @@ describe('UndiciInstrumentation `undici` tests', function () { } }); }); + + it('should capture error if fetch request is aborted', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + let requestError; + const controller = new AbortController(); + const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const requestPromise = request(requestUrl, { signal: controller.signal }); + controller.abort(); + try { + await requestPromise; + } catch (err) { + // Expected error + requestError = err; + } + + // Let the error be published to diagnostics channel + await new Promise((r) => setTimeout(r,5)); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpMethod: 'GET', + path: '/', + query:'?query=test', + error: requestError, + noNetPeer: true, // do not check network attribs + forceStatus: { + code: SpanStatusCode.ERROR, + message: requestError.message + } + }); + }); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index d137e8294c4..72ee2bc180a 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -76,7 +76,7 @@ export const assertSpan = ( assert.strictEqual( span.attributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE], validations.httpStatusCode, - `attributes['${SemanticAttributes.HTTP_RESPONSE_STATUS_CODE}'] is correct`, + `attributes['${SemanticAttributes.HTTP_RESPONSE_STATUS_CODE}'] is correct ${span.attributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE]}`, ); assert.strictEqual(span.links.length, 0, 'there are no links'); From e82dc40805149fd4c415591e82b68b447229676c Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 15 Feb 2024 11:33:37 +0100 Subject: [PATCH 22/28] chore(instrumentation-undici): add tests for fetch and stram methods --- .../test/undici.test.ts | 290 +++++++++++++----- 1 file changed, 221 insertions(+), 69 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts index 97b31e5ea71..9cd3ca919c3 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import * as assert from 'assert'; +import { Writable } from 'stream'; import { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; @@ -33,7 +34,8 @@ const instrumentation = new UndiciInstrumentation(); instrumentation.enable(); instrumentation.disable(); -import { request } from 'undici'; +import type { Dispatcher } from 'undici'; +import * as undici from 'undici'; const protocol = 'http'; const hostname = 'localhost'; @@ -43,9 +45,23 @@ const provider = new NodeTracerProvider(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); instrumentation.setTracerProvider(provider); + +// Undici docs (https://github.com/nodejs/undici#garbage-collection) suggest +// that an undici response body should always be consumed. +async function consumeResponseBody(body: Dispatcher.ResponseData["body"]) { + return new Promise((resolve) => { + const devNull = new Writable({ + write(_chunk, _encoding, cb) { + setImmediate(cb); + }, + }); + body.pipe(devNull); + body.on('end', resolve); + }); +} + describe('UndiciInstrumentation `undici` tests', function () { before(function (done) { - // TODO: mock propagation and test it propagation.setGlobalPropagator(new MockPropagation()); context.setGlobalContextManager(new AsyncHooksContextManager().enable()); mockServer.start(done); @@ -98,7 +114,9 @@ describe('UndiciInstrumentation `undici` tests', function () { instrumentation.setConfig({ enabled: false }); const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const { headers } = await request(requestUrl); + const { headers, body } = await undici.request(requestUrl); + await consumeResponseBody(body); + assert.ok( headers['propagation-error'] != null, 'propagation is not set if instrumentation disabled' @@ -112,121 +130,209 @@ describe('UndiciInstrumentation `undici` tests', function () { describe('enable()', function () { beforeEach(function () { instrumentation.enable(); + // Set configuration + instrumentation.setConfig({ + enabled: true, + ignoreRequestHook: (req) => { + return req.path.indexOf('/ignore/path') !== -1; + }, + requestHook: (span, req) => { + // TODO: maybe an intermediate request with better API + req.headers += 'x-requested-with: undici\r\n'; + }, + startSpanHook: (request) => { + return { + 'test.hook.attribute': 'hook-value', + }; + }, + headersToSpanAttributes: { + requestHeaders: ['foo-client', 'x-requested-with'], + responseHeaders: ['foo-server'], + } + }); }); afterEach(function () { // Empty configuration & disable instrumentation.setConfig({ enabled: false }); }); - it('should create valid spans even if the configuration hooks fail', async function () { + it('should create valid spans for "request" method', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - // Set the bad configuration - instrumentation.setConfig({ - enabled: true, - ignoreRequestHook: () => { - throw new Error('ignoreRequestHook error'); - }, - applyCustomAttributesOnSpan: () => { - throw new Error('ignoreRequestHook error'); - }, - requestHook: () => { - throw new Error('requestHook error'); - }, - startSpanHook: () => { - throw new Error('startSpanHook error'); - }, - }) + // Do some requests + const headers = { + 'user-agent': 'custom', + 'foo-client': 'bar' + }; + const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; + const ignoreResponse = await undici.request(ignoreRequestUrl, { headers }); + await consumeResponseBody(ignoreResponse.body); - const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const { headers, statusCode } = await request(requestUrl); assert.ok( - headers['propagation-error'] == null, + ignoreResponse.headers['propagation-error'], + 'propagation is not set for ignored requests' + ); + + const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const queryResponse = await undici.request(queryRequestUrl, { headers }); + await consumeResponseBody(queryResponse.body); + + assert.ok( + queryResponse.headers['propagation-error'] == null, 'propagation is set for instrumented requests' ); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; - assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); assertSpan(span, { hostname: 'localhost', - httpStatusCode: statusCode, + httpStatusCode: queryResponse.statusCode, httpMethod: 'GET', path: '/', query:'?query=test', - resHeaders: headers, + reqHeaders: headers, + resHeaders: queryResponse.headers, }); + assert.strictEqual( + span.attributes['http.request.header.foo-client'], + 'bar', + 'request headers from fetch options are captured', + ); + assert.strictEqual( + span.attributes['http.request.header.x-requested-with'], + 'undici', + 'request headers from requestHook are captured', + ); + assert.strictEqual( + span.attributes['http.response.header.foo-server'], + 'bar', + 'response headers from the server are captured', + ); + assert.strictEqual( + span.attributes['test.hook.attribute'], + 'hook-value', + 'startSpanHook is called', + ); }); - it('should create valid spans with empty configuration', async function () { + it('should create valid spans for "fetch" method', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const { headers, statusCode } = await request(requestUrl); + // Do some requests + const headers = { + 'user-agent': 'custom', + 'foo-client': 'bar' + }; + const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; + const ignoreResponse = await undici.fetch(ignoreRequestUrl, { headers }); + await ignoreResponse.text(); + assert.ok( - headers['propagation-error'] == null, + ignoreResponse.headers.get('propagation-error'), + 'propagation is not set for ignored requests' + ); + + const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const queryResponse = await undici.fetch(queryRequestUrl, { headers }); + await queryResponse.text(); + + + assert.ok( + queryResponse.headers.get('propagation-error') == null, 'propagation is set for instrumented requests' ); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; - assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); assertSpan(span, { hostname: 'localhost', - httpStatusCode: statusCode, + httpStatusCode: queryResponse.status, httpMethod: 'GET', path: '/', query:'?query=test', - resHeaders: headers, + reqHeaders: headers, + resHeaders: queryResponse.headers as Headers, }); + assert.strictEqual( + span.attributes['http.request.header.foo-client'], + 'bar', + 'request headers from fetch options are captured', + ); + assert.strictEqual( + span.attributes['http.request.header.x-requested-with'], + 'undici', + 'request headers from requestHook are captured', + ); + assert.strictEqual( + span.attributes['http.response.header.foo-server'], + 'bar', + 'response headers from the server are captured', + ); + assert.strictEqual( + span.attributes['test.hook.attribute'], + 'hook-value', + 'startSpanHook is called', + ); }); - it('should create valid spans with the given configuration', async function () { + it('should create valid spans for "stream" method', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - // Set configuration - instrumentation.setConfig({ - enabled: true, - ignoreRequestHook: (req) => { - return req.path.indexOf('/ignore/path') !== -1; - }, - requestHook: (span, req) => { - // TODO: maybe an intermediate request with better API - req.headers += 'x-requested-with: undici\r\n'; - }, - startSpanHook: (request) => { - return { - 'test.hook.attribute': 'hook-value', - }; - }, - headersToSpanAttributes: { - requestHeaders: ['foo-client', 'x-requested-with'], - responseHeaders: ['foo-server'], - } - }); - // Do some requests - const headers = { 'user-agent': 'custom', 'foo-client': 'bar' }; const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; - const ignoreResponse = await request(ignoreRequestUrl, { headers }); + // https://undici.nodejs.org/#/docs/api/Dispatcher?id=example-1-basic-get-stream-request + const bufs: any[] = []; + const ignoreResponse: Record = {}; + await undici.stream( + ignoreRequestUrl, + { opaque: { bufs }, headers } as any, + ({ statusCode, headers, opaque }) => { + ignoreResponse.statusCode = statusCode; + ignoreResponse.headers = headers; + return new Writable({ + write (chunk, encoding, callback) { + (opaque as any).bufs.push(chunk) + callback() + } + }); + } + ); + assert.ok( ignoreResponse.headers['propagation-error'], 'propagation is not set for ignored requests' ); + bufs.length = 0; const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const queryResponse = await request(queryRequestUrl, { headers }); + const queryResponse: Record = {}; + await undici.stream( + queryRequestUrl, + { opaque: { bufs }, headers } as any, + ({ statusCode, headers, opaque }) => { + queryResponse.statusCode = statusCode; + queryResponse.headers = headers; + return new Writable({ + write (chunk, encoding, callback) { + (opaque as any).bufs.push(chunk) + callback() + } + }); + } + ); + + assert.ok( queryResponse.headers['propagation-error'] == null, 'propagation is set for instrumented requests' @@ -236,6 +342,7 @@ describe('UndiciInstrumentation `undici` tests', function () { const span = spans[0]; assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); + console.log(span) assertSpan(span, { hostname: 'localhost', httpStatusCode: queryResponse.statusCode, @@ -243,7 +350,7 @@ describe('UndiciInstrumentation `undici` tests', function () { path: '/', query:'?query=test', reqHeaders: headers, - resHeaders: queryResponse.headers, + resHeaders: queryResponse.headers as Headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], @@ -267,7 +374,51 @@ describe('UndiciInstrumentation `undici` tests', function () { ); }); - // TODO: another test with a parent span. Check HTTP tests + it('should create valid spans even if the configuration hooks fail', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Set the bad configuration + instrumentation.setConfig({ + enabled: true, + ignoreRequestHook: () => { + throw new Error('ignoreRequestHook error'); + }, + applyCustomAttributesOnSpan: () => { + throw new Error('ignoreRequestHook error'); + }, + requestHook: () => { + throw new Error('requestHook error'); + }, + startSpanHook: () => { + throw new Error('startSpanHook error'); + }, + }) + + const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const { headers, statusCode, body } = await undici.request(requestUrl); + await consumeResponseBody(body); + + assert.ok( + headers['propagation-error'] == null, + 'propagation is set for instrumented requests' + ); + + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: statusCode, + httpMethod: 'GET', + path: '/', + query:'?query=test', + resHeaders: headers, + }); + }); + it('should not create spans without parent if required in configuration', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -278,7 +429,9 @@ describe('UndiciInstrumentation `undici` tests', function () { }); const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const response = await request(requestUrl); + const response = await undici.request(requestUrl); + await consumeResponseBody(response.body); + // TODO: here we're checking the propagation works even if the instrumentation // is not starting any span. Not 100% sure this is the behaviour we want assert.ok( @@ -290,7 +443,7 @@ describe('UndiciInstrumentation `undici` tests', function () { assert.strictEqual(spans.length, 0, 'no spans are created'); }); - it('should not create spans with parent if required in configuration', function (done) { + it('should create spans with parent if required in configuration', function (done) { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -306,8 +459,8 @@ describe('UndiciInstrumentation `undici` tests', function () { context.with(trace.setSpan(context.active(), span), async () => { const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const response = await request(requestUrl); - + const response = await undici.request(requestUrl); + await consumeResponseBody(response.body); span.end(); // TODO: here we're checking the propagation works even if the instrumentation @@ -335,15 +488,14 @@ describe('UndiciInstrumentation `undici` tests', function () { }); - it('should capture errors using fetch API', async function () { + it('should capture errors while doing request', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); let fetchError; try { const requestUrl = 'http://unexistent-host-name/path'; - await request(requestUrl); - + await undici.request(requestUrl); } catch (err) { // Expected error fetchError = err; @@ -366,14 +518,14 @@ describe('UndiciInstrumentation `undici` tests', function () { }); }); - it('should capture error if fetch request is aborted', async function () { + it('should capture error if undici request is aborted', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); let requestError; const controller = new AbortController(); const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const requestPromise = request(requestUrl, { signal: controller.signal }); + const requestPromise = undici.request(requestUrl, { signal: controller.signal }); controller.abort(); try { await requestPromise; From 2e5f09a2b4420dc2cd06d82fe6d217305e66db73 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 19 Feb 2024 16:38:51 +0100 Subject: [PATCH 23/28] chore(instrumentation-undici): add http.client.request.duration metric --- .../src/undici.ts | 112 +++++++--- .../test/fetch.test.ts | 1 - .../test/metrics.test.ts | 197 ++++++++++++++++++ .../test/undici.test.ts | 136 +++++++++--- .../test/utils/mock-metrics-reader.ts | 47 +++++ 5 files changed, 432 insertions(+), 61 deletions(-) create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts create mode 100644 experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-metrics-reader.ts diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index d64bef37c2f..951f76b7040 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -21,12 +21,15 @@ import { Attributes, context, diag, + Histogram, + HrTime, INVALID_SPAN_CONTEXT, propagation, Span, SpanKind, SpanStatusCode, trace, + ValueType, } from '@opentelemetry/api'; import { VERSION } from './version'; @@ -34,6 +37,13 @@ import { VERSION } from './version'; import { ListenerRecord, RequestHeadersMessage, RequestMessage, ResponseHeadersMessage } from './internal-types'; import { UndiciInstrumentationConfig, UndiciRequest } from './types'; import { SemanticAttributes } from './enums/SemanticAttributes'; +import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core'; + +interface IntrumentationRecord { + span: Span; + attributes: Attributes; + startTime: HrTime; +} // A combination of https://github.com/elastic/apm-agent-nodejs and @@ -42,9 +52,9 @@ export class UndiciInstrumentation extends InstrumentationBase { // Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for // unsubscribing. private _channelSubs!: Array; - - private _spanFromReq = new WeakMap(); - + private _recordFromReq = new WeakMap(); + + private _httpClientDurationHistogram!: Histogram; constructor(config?: UndiciInstrumentationConfig) { super('@opentelemetry/instrumentation-undici', VERSION, config); // Force load fetch API (since it's lazy loaded in Node 18) @@ -100,6 +110,17 @@ export class UndiciInstrumentation extends InstrumentationBase { this.disable(); } } + + protected override _updateMetricInstruments() { + this._httpClientDurationHistogram = this.meter.createHistogram( + 'http.client.request.duration', + { + description: 'Measures the duration of outbound HTTP requests.', + unit: 'ms', + valueType: ValueType.DOUBLE, + } + ); + } private _getConfig(): UndiciInstrumentationConfig { return this._config as UndiciInstrumentationConfig; @@ -137,6 +158,7 @@ export class UndiciInstrumentation extends InstrumentationBase { return; } + const startTime = hrTime(); const rawHeaders = request.headers.split('\r\n'); const reqHeaders = new Map(rawHeaders.map(h => { const sepIndex = h.indexOf(':'); @@ -146,25 +168,29 @@ export class UndiciInstrumentation extends InstrumentationBase { })); const requestUrl = new URL(request.origin + request.path); - const spanAttributes: Attributes = { + const urlScheme = requestUrl.protocol.replace(':', ''); + const attributes: Attributes = { [SemanticAttributes.HTTP_REQUEST_METHOD]: request.method, [SemanticAttributes.URL_FULL]: requestUrl.toString(), [SemanticAttributes.URL_PATH]: requestUrl.pathname, [SemanticAttributes.URL_QUERY]: requestUrl.search, + [SemanticAttributes.URL_SCHEME]: urlScheme, }; - const protocolPorts: Record = { https: '443', http: '80' }; + const schemePorts: Record = { https: '443', http: '80' }; + // TODO: check this resolution based on headers + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes const serverAddress = reqHeaders.get('host') || requestUrl.hostname; - const serverPort = requestUrl.port || protocolPorts[requestUrl.protocol]; + const serverPort = requestUrl.port || schemePorts[urlScheme]; - spanAttributes[SemanticAttributes.SERVER_ADDRESS] = serverAddress; - if (serverPort) { - spanAttributes[SemanticAttributes.SERVER_PORT] = serverPort; + attributes[SemanticAttributes.SERVER_ADDRESS] = serverAddress; + if (serverPort && !isNaN(Number(serverPort))) { + attributes[SemanticAttributes.SERVER_PORT] = Number(serverPort); } const userAgent = reqHeaders.get('user-agent'); if (userAgent) { - spanAttributes[SemanticAttributes.USER_AGENT_ORIGINAL] = userAgent; + attributes[SemanticAttributes.USER_AGENT_ORIGINAL] = userAgent; } // Get attributes from the hook if present @@ -175,7 +201,7 @@ export class UndiciInstrumentation extends InstrumentationBase { ); if (hookAttributes) { Object.entries(hookAttributes).forEach(([key, val]) => { - spanAttributes[key] = val; + attributes[key] = val; }); } @@ -194,7 +220,7 @@ export class UndiciInstrumentation extends InstrumentationBase { `HTTP ${request.method}`, { kind: SpanKind.CLIENT, - attributes: spanAttributes, + attributes: attributes, }, activeCtx ); @@ -216,20 +242,21 @@ export class UndiciInstrumentation extends InstrumentationBase { request.headers += Object.entries(addedHeaders) .map(([k, v]) => `${k}: ${v}\r\n`) .join(''); - this._spanFromReq.set(request, span); + this._recordFromReq.set(request, {span, attributes, startTime}); } // This is the 2nd message we recevie for each request. It is fired when connection with // the remote is stablished and about to send the first byte. Here do have info about the // remote addres an port so we can poupulate some `net.*` attributes into the span private onRequestHeaders({ request, socket }: RequestHeadersMessage): void { - const span = this._spanFromReq.get(request as UndiciRequest); + const record = this._recordFromReq.get(request as UndiciRequest); - if (!span) { + if (!record) { return } const config = this._getConfig(); + const { span } = record; const { remoteAddress, remotePort } = socket; const spanAttributes: Attributes = { [SemanticAttributes.NETWORK_PEER_ADDRESS]: remoteAddress, @@ -262,12 +289,13 @@ export class UndiciInstrumentation extends InstrumentationBase { // headers are received, body may not be accessible yet. // From the response headers we can set the status and content length private onResponseHeaders({ request, response }: ResponseHeadersMessage): void { - const span = this._spanFromReq.get(request); + const record = this._recordFromReq.get(request); - if (!span) { + if (!record) { return; } + const {span, attributes, startTime} = record; // We are currently *not* capturing response headers, even though the // intake API does allow it, because none of the other `setHttpContext` // uses currently do @@ -305,34 +333,66 @@ export class UndiciInstrumentation extends InstrumentationBase { span.setStatus({ code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET, }); + this._recordFromReq.set( + request, + {span, startTime, attributes: Object.assign(attributes, spanAttributes)} + ); } // This is the last event we receive if the request went without any errors - private onDone({ request }: any): void { - const span = this._spanFromReq.get(request); + private onDone({ request }: RequestMessage): void { + const record = this._recordFromReq.get(request); - if (!span) { + if (!record) { return; } + + const {span, attributes, startTime} = record; + // End the span span.end(); - this._spanFromReq.delete(request); + this._recordFromReq.delete(request); + + // Time to record metrics + const metricsAttributes: Attributes = {}; + // Get the attribs already in span attributes + const keysToCopy = [ + SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, + SemanticAttributes.HTTP_REQUEST_METHOD, + SemanticAttributes.SERVER_ADDRESS, + SemanticAttributes.SERVER_PORT, + SemanticAttributes.URL_SCHEME, + ]; + keysToCopy.forEach((key) => { + if (key in attributes) { + metricsAttributes[key] = attributes[key]; + } + }); + + // Take the duration and record it + const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())); + this._httpClientDurationHistogram.record(duration, metricsAttributes); } // This is the event we get when something is wrong in the request like - // - invalid options + // - invalid options when calling `fetch` global API or any undici method for request // - connectivity errors such as unreachable host - // - requests aborted through a signal + // - requests aborted through an `AbortController.signal` // NOTE: server errors are considered valid responses and it's the lib consumer - // whi should deal with that. + // who should deal with that. private onError({ request, error }: any): void { - const span = this._spanFromReq.get(request); + const record = this._recordFromReq.get(request); - if (!span) { + if (!record) { return; } + const {span, attributes} = record; + + // TODO: add metrics + + // NOTE: in `undici@6.3.0` when request aborted the error type changes from // a custom error (`RequestAbortedError`) to a built-in `DOMException` carrying // some differences: diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index 91a77e1746f..d95a22a97aa 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -51,7 +51,6 @@ describe('UndiciInstrumentation `fetch` tests', function () { this.skip(); } - // TODO: mock propagation and test it propagation.setGlobalPropagator(new MockPropagation()); context.setGlobalContextManager(new AsyncHooksContextManager().enable()); mockServer.start(done); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts new file mode 100644 index 00000000000..db6ce728dd2 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts @@ -0,0 +1,197 @@ +/* + * 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. + */ +import * as assert from 'assert'; + +import { context, propagation } from '@opentelemetry/api'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { + AggregationTemporality, + DataPointType, + InMemoryMetricExporter, + MeterProvider, +} from '@opentelemetry/sdk-metrics'; + +import { UndiciInstrumentation } from '../src/undici'; + +import { MockServer } from './utils/mock-server'; +import { MockMetricsReader } from './utils/mock-metrics-reader'; +import { SemanticAttributes } from '../src/enums/SemanticAttributes'; + +const instrumentation = new UndiciInstrumentation(); +instrumentation.enable(); +instrumentation.disable(); + +const protocol = 'http'; +const hostname = 'localhost'; +const mockServer = new MockServer(); +const provider = new NodeTracerProvider(); +const meterProvider = new MeterProvider(); +// const memoryExporter = new InMemorySpanExporter(); +const metricsMemoryExporter = new InMemoryMetricExporter( + AggregationTemporality.DELTA +); +const metricReader = new MockMetricsReader(metricsMemoryExporter); +meterProvider.addMetricReader(metricReader); +// provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); +instrumentation.setTracerProvider(provider); +instrumentation.setMeterProvider(meterProvider); + +describe('UndiciInstrumentation metrics tests', function () { + + before(function (done) { + // Do not test if the `fetch` global API is not available + // This applies to nodejs < v18 or nodejs < v16.15 wihtout the flag + // `--experimental-global-fetch` set + // https://nodejs.org/api/globals.html#fetch + if (typeof globalThis.fetch !== 'function') { + this.skip(); + } + + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); + mockServer.start(done); + mockServer.mockListener((req, res) => { + // Return a valid response always + res.statusCode = 200; + res.setHeader('content-type', 'application/json'); + res.write(JSON.stringify({ success: true })); + res.end(); + }); + + // enable instrumentation for all tests + instrumentation.enable(); + }); + + after(function(done) { + instrumentation.disable(); + context.disable(); + propagation.disable(); + mockServer.mockListener(undefined); + mockServer.stop(done); + }); + + beforeEach(function () { + metricsMemoryExporter.reset(); + }); + + describe('with fetch API', function () { + before(function (done) { + // Do not test if the `fetch` global API is not available + // This applies to nodejs < v18 or nodejs < v16.15 wihtout the flag + // `--experimental-global-fetch` set + // https://nodejs.org/api/globals.html#fetch + if (typeof globalThis.fetch !== 'function') { + this.skip(); + } + + done(); + }); + + it('should report "http.client.request.duration" metric', async () => { + const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + await fetch(fetchUrl); + + await metricReader.collectAndExport(); + const resourceMetrics = metricsMemoryExporter.getMetrics(); + const scopeMetrics = resourceMetrics[0].scopeMetrics; + const metrics = scopeMetrics[0].metrics; + + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); + assert.strictEqual(metrics.length, 1, 'metrics count'); + assert.strictEqual(metrics[0].descriptor.name, 'http.client.request.duration'); + assert.strictEqual( + metrics[0].descriptor.description, + 'Measures the duration of outbound HTTP requests.' + ); + assert.strictEqual(metrics[0].descriptor.unit, 'ms'); + assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual(metrics[0].dataPoints.length, 1); + + const metricAttributes = metrics[0].dataPoints[0].attributes; + assert.strictEqual( + metricAttributes[SemanticAttributes.URL_SCHEME], + 'http' + ); + assert.strictEqual( + metricAttributes[SemanticAttributes.HTTP_REQUEST_METHOD], + 'GET', + ); + assert.strictEqual( + metricAttributes[SemanticAttributes.SERVER_ADDRESS], + 'localhost', + ); + assert.strictEqual( + metricAttributes[SemanticAttributes.SERVER_PORT], + mockServer.port, + ); + assert.strictEqual( + metricAttributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE], + 200, + ); + }); + + it.only('should have error.type in "http.client.request.duration" metric', async () => { + const fetchUrl = 'http://unknownhost.com/'; + + try { + await fetch(fetchUrl); + } catch (err) { + // Expected error, do nothing + console.log('expected err', err) + } + + await metricReader.collectAndExport(); + const resourceMetrics = metricsMemoryExporter.getMetrics(); + const scopeMetrics = resourceMetrics[0].scopeMetrics; + const metrics = scopeMetrics[0].metrics; + console.dir(resourceMetrics, {depth: 9}); + + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); + assert.strictEqual(metrics.length, 1, 'metrics count'); + assert.strictEqual(metrics[0].descriptor.name, 'http.client.request.duration'); + assert.strictEqual( + metrics[0].descriptor.description, + 'Measures the duration of outbound HTTP requests.' + ); + assert.strictEqual(metrics[0].descriptor.unit, 'ms'); + assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual(metrics[0].dataPoints.length, 1); + + const metricAttributes = metrics[0].dataPoints[0].attributes; + assert.strictEqual( + metricAttributes[SemanticAttributes.URL_SCHEME], + 'http' + ); + assert.strictEqual( + metricAttributes[SemanticAttributes.HTTP_REQUEST_METHOD], + 'GET', + ); + assert.strictEqual( + metricAttributes[SemanticAttributes.SERVER_ADDRESS], + 'unknownhost.com', + ); + assert.strictEqual( + metricAttributes[SemanticAttributes.SERVER_PORT], + 80, + ); + // TODO: check error.type + // assert.strictEqual( + // metricAttributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE], + // 200, + // ); + }); + }); +}); \ No newline at end of file diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts index 9cd3ca919c3..4f0d3252664 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts @@ -156,6 +156,32 @@ describe('UndiciInstrumentation `undici` tests', function () { instrumentation.setConfig({ enabled: false }); }); + it('should ingore requests based on the result of ignoreRequestHook', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Do some requests + const headers = { + 'user-agent': 'custom', + 'foo-client': 'bar' + }; + + const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; + const ignoreResponse = await undici.request(ignoreRequestUrl, { headers }); + await consumeResponseBody(ignoreResponse.body); + + assert.ok( + ignoreResponse.headers['propagation-error'], + 'propagation is not set for ignored requests' + ); + + spans = memoryExporter.getFinishedSpans(); + assert.ok( + spans.length === 0, + 'ignoreRequestHook is filtering requests' + ); + }); + it('should create valid spans for "request" method', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -165,6 +191,7 @@ describe('UndiciInstrumentation `undici` tests', function () { 'user-agent': 'custom', 'foo-client': 'bar' }; + const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; const ignoreResponse = await undici.request(ignoreRequestUrl, { headers }); await consumeResponseBody(ignoreResponse.body); @@ -227,20 +254,10 @@ describe('UndiciInstrumentation `undici` tests', function () { 'user-agent': 'custom', 'foo-client': 'bar' }; - const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; - const ignoreResponse = await undici.fetch(ignoreRequestUrl, { headers }); - await ignoreResponse.text(); - - assert.ok( - ignoreResponse.headers.get('propagation-error'), - 'propagation is not set for ignored requests' - ); - const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const queryResponse = await undici.fetch(queryRequestUrl, { headers }); await queryResponse.text(); - assert.ok( queryResponse.headers.get('propagation-error') == null, 'propagation is set for instrumented requests' @@ -290,16 +307,16 @@ describe('UndiciInstrumentation `undici` tests', function () { 'user-agent': 'custom', 'foo-client': 'bar' }; - const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; // https://undici.nodejs.org/#/docs/api/Dispatcher?id=example-1-basic-get-stream-request + const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; + const queryResponse: Record = {}; const bufs: any[] = []; - const ignoreResponse: Record = {}; await undici.stream( - ignoreRequestUrl, + queryRequestUrl, { opaque: { bufs }, headers } as any, ({ statusCode, headers, opaque }) => { - ignoreResponse.statusCode = statusCode; - ignoreResponse.headers = headers; + queryResponse.statusCode = statusCode; + queryResponse.headers = headers; return new Writable({ write (chunk, encoding, callback) { (opaque as any).bufs.push(chunk) @@ -310,28 +327,80 @@ describe('UndiciInstrumentation `undici` tests', function () { ); assert.ok( - ignoreResponse.headers['propagation-error'], - 'propagation is not set for ignored requests' + queryResponse.headers['propagation-error'] == null, + 'propagation is set for instrumented requests' ); - bufs.length = 0; - const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const queryResponse: Record = {}; - await undici.stream( - queryRequestUrl, - { opaque: { bufs }, headers } as any, - ({ statusCode, headers, opaque }) => { - queryResponse.statusCode = statusCode; - queryResponse.headers = headers; - return new Writable({ - write (chunk, encoding, callback) { - (opaque as any).bufs.push(chunk) - callback() - } - }); - } + spans = memoryExporter.getFinishedSpans(); + const span = spans[0]; + assert.ok(span, 'a span is present'); + assert.strictEqual(spans.length, 1); + assertSpan(span, { + hostname: 'localhost', + httpStatusCode: queryResponse.statusCode, + httpMethod: 'GET', + path: '/', + query:'?query=test', + reqHeaders: headers, + resHeaders: queryResponse.headers as Headers, + }); + assert.strictEqual( + span.attributes['http.request.header.foo-client'], + 'bar', + 'request headers from fetch options are captured', + ); + assert.strictEqual( + span.attributes['http.request.header.x-requested-with'], + 'undici', + 'request headers from requestHook are captured', + ); + assert.strictEqual( + span.attributes['http.response.header.foo-server'], + 'bar', + 'response headers from the server are captured', ); + assert.strictEqual( + span.attributes['test.hook.attribute'], + 'hook-value', + 'startSpanHook is called', + ); + }); + it('should create valid spans for "dispatch" method', async function () { + let spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + + // Do some requests + const headers = { + 'user-agent': 'custom', + 'foo-client': 'bar' + }; + + const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}`; + const queryResponse: Record = {}; + const client = new undici.Client(queryRequestUrl); + await new Promise((resolve, reject) => { + client.dispatch( + { + path: '/?query=test', + method: 'GET', + headers, + }, + { + onHeaders: (statusCode, headers) => { + queryResponse.statusCode = statusCode; + queryResponse.headers = headers; + return true; // unidici types require to return boolean + }, + onError: reject, + onComplete: resolve, + // Although the types say these following handlers are optional they must + // be defined to avoid a TypeError + onConnect: () => undefined, + onData: () => true, + } + ); + }); assert.ok( queryResponse.headers['propagation-error'] == null, @@ -342,7 +411,6 @@ describe('UndiciInstrumentation `undici` tests', function () { const span = spans[0]; assert.ok(span, 'a span is present'); assert.strictEqual(spans.length, 1); - console.log(span) assertSpan(span, { hostname: 'localhost', httpStatusCode: queryResponse.statusCode, diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-metrics-reader.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-metrics-reader.ts new file mode 100644 index 00000000000..c3ffbf0ae63 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-metrics-reader.ts @@ -0,0 +1,47 @@ +/* + * 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. + */ + +import { MetricReader, PushMetricExporter } from '@opentelemetry/sdk-metrics'; + +export class MockMetricsReader extends MetricReader { + constructor(private _exporter: PushMetricExporter) { + super({ + aggregationTemporalitySelector: + _exporter.selectAggregationTemporality?.bind(_exporter), + }); + } + + protected onForceFlush(): Promise { + return Promise.resolve(undefined); + } + + protected onShutdown(): Promise { + return Promise.resolve(undefined); + } + + public async collectAndExport(): Promise { + const result = await this.collect(); + await new Promise((resolve, reject) => { + this._exporter.export(result.resourceMetrics, result => { + if (result.error != null) { + reject(result.error); + } else { + resolve(); + } + }); + }); + } +} From 032e05722fe464e15cc4332766804a4025808e05 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 19 Feb 2024 23:56:53 +0100 Subject: [PATCH 24/28] chore(instrumentation-undici): add error.type attribute in request duration metric --- .../src/undici.ts | 54 +++++++++++-------- .../test/metrics.test.ts | 19 +++---- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 951f76b7040..07f470db0af 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -354,25 +354,8 @@ export class UndiciInstrumentation extends InstrumentationBase { span.end(); this._recordFromReq.delete(request); - // Time to record metrics - const metricsAttributes: Attributes = {}; - // Get the attribs already in span attributes - const keysToCopy = [ - SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, - SemanticAttributes.HTTP_REQUEST_METHOD, - SemanticAttributes.SERVER_ADDRESS, - SemanticAttributes.SERVER_PORT, - SemanticAttributes.URL_SCHEME, - ]; - keysToCopy.forEach((key) => { - if (key in attributes) { - metricsAttributes[key] = attributes[key]; - } - }); - - // Take the duration and record it - const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())); - this._httpClientDurationHistogram.record(duration, metricsAttributes); + // Record metrics + this.recordRequestDuration(attributes, startTime); } // This is the event we get when something is wrong in the request like @@ -388,10 +371,7 @@ export class UndiciInstrumentation extends InstrumentationBase { return; } - const {span, attributes} = record; - - // TODO: add metrics - + const {span, attributes, startTime} = record; // NOTE: in `undici@6.3.0` when request aborted the error type changes from // a custom error (`RequestAbortedError`) to a built-in `DOMException` carrying @@ -405,5 +385,33 @@ export class UndiciInstrumentation extends InstrumentationBase { message: error.message, }); span.end(); + this._recordFromReq.delete(request); + + // Record metrics (with the error) + attributes[SemanticAttributes.ERROR_TYPE] = error.message; + this.recordRequestDuration(attributes, startTime); + } + + private recordRequestDuration(attributes: Attributes, startTime: HrTime) { + // Time to record metrics + const metricsAttributes: Attributes = {}; + // Get the attribs already in span attributes + const keysToCopy = [ + SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, + SemanticAttributes.HTTP_REQUEST_METHOD, + SemanticAttributes.SERVER_ADDRESS, + SemanticAttributes.SERVER_PORT, + SemanticAttributes.URL_SCHEME, + SemanticAttributes.ERROR_TYPE, + ]; + keysToCopy.forEach((key) => { + if (key in attributes) { + metricsAttributes[key] = attributes[key]; + } + }); + + // Take the duration and record it + const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())); + this._httpClientDurationHistogram.record(duration, metricsAttributes); } } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts index db6ce728dd2..c6703754abe 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts @@ -143,22 +143,20 @@ describe('UndiciInstrumentation metrics tests', function () { ); }); - it.only('should have error.type in "http.client.request.duration" metric', async () => { - const fetchUrl = 'http://unknownhost.com/'; + it('should have error.type in "http.client.request.duration" metric', async () => { + const fetchUrl = 'http://unknownhost/'; try { await fetch(fetchUrl); } catch (err) { // Expected error, do nothing - console.log('expected err', err) } await metricReader.collectAndExport(); const resourceMetrics = metricsMemoryExporter.getMetrics(); const scopeMetrics = resourceMetrics[0].scopeMetrics; const metrics = scopeMetrics[0].metrics; - console.dir(resourceMetrics, {depth: 9}); - + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); assert.strictEqual(metrics.length, 1, 'metrics count'); assert.strictEqual(metrics[0].descriptor.name, 'http.client.request.duration'); @@ -181,17 +179,16 @@ describe('UndiciInstrumentation metrics tests', function () { ); assert.strictEqual( metricAttributes[SemanticAttributes.SERVER_ADDRESS], - 'unknownhost.com', + 'unknownhost', ); assert.strictEqual( metricAttributes[SemanticAttributes.SERVER_PORT], 80, ); - // TODO: check error.type - // assert.strictEqual( - // metricAttributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE], - // 200, - // ); + assert.ok( + metricAttributes[SemanticAttributes.ERROR_TYPE], + `the metric contains "${SemanticAttributes.ERROR_TYPE}" attribute if request failed`, + ); }); }); }); \ No newline at end of file From 0c0e1693e73a136e481f2b5fc6faa9717891d8cc Mon Sep 17 00:00:00 2001 From: David Luna Date: Tue, 20 Feb 2024 00:43:19 +0100 Subject: [PATCH 25/28] chore(instrumentation-undici): clean up semantic attributes --- .../src/enums/SemanticAttributes.ts | 2597 +---------------- .../src/undici.ts | 16 +- .../test/utils/assertSpan.ts | 18 - 3 files changed, 81 insertions(+), 2550 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts index d70ef67fa75..b6121b00770 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts @@ -18,2603 +18,152 @@ export const SemanticAttributes = { /** - * The name of the invoked function. - * - * Note: SHOULD be equal to the `faas.name` resource attribute of the invoked function. - */ - FAAS_INVOKED_NAME: 'faas.invoked_name', - - /** - * The cloud provider of the invoked function. - * - * Note: SHOULD be equal to the `cloud.provider` resource attribute of the invoked function. + * State of the HTTP connection in the HTTP connection pool. */ - FAAS_INVOKED_PROVIDER: 'faas.invoked_provider', + HTTP_CONNECTION_STATE: 'http.connection.state', /** - * The cloud region of the invoked function. + * Describes a class of error the operation ended with. * - * Note: SHOULD be equal to the `cloud.region` resource attribute of the invoked function. - */ - FAAS_INVOKED_REGION: 'faas.invoked_region', - - /** - * Type of the trigger which caused this function invocation. - */ - FAAS_TRIGGER: 'faas.trigger', + * Note: The `error.type` SHOULD be predictable and SHOULD have low cardinality. +Instrumentations SHOULD document the list of errors they report. - /** - * The [`service.name`](/docs/resource/README.md#service) of the remote service. SHOULD be equal to the actual `service.name` resource attribute of the remote service if any. - */ - PEER_SERVICE: 'peer.service', +The cardinality of `error.type` within one instrumentation library SHOULD be low. +Telemetry consumers that aggregate data from multiple instrumentation libraries and applications +should be prepared for `error.type` to have high cardinality at query time when no +additional filters are applied. - /** - * Username or client_id extracted from the access token or [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) header in the inbound request from outside the system. - */ - ENDUSER_ID: 'enduser.id', +If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. - /** - * Actual/assumed role the client is making the request under extracted from token or application security context. - */ - ENDUSER_ROLE: 'enduser.role', +If a specific domain defines its own set of error identifiers (such as HTTP or gRPC status codes), +it's RECOMMENDED to: - /** - * Scopes or granted authorities the client currently possesses extracted from token or application security context. The value would come from the scope associated with an [OAuth 2.0 Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) or an attribute value in a [SAML 2.0 Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). +* Use a domain-specific attribute +* Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. */ - ENDUSER_SCOPE: 'enduser.scope', + ERROR_TYPE: 'error.type', /** - * Identifies the class / type of event. - * - * Note: Event names are subject to the same rules as [attribute names](https://github.com/open-telemetry/opentelemetry-specification/tree/v1.26.0/specification/common/attribute-naming.md). Notably, event names are namespaced to avoid collisions and provide a clean separation of semantics for events in separate domains like browser, mobile, and kubernetes. + * The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. */ - EVENT_NAME: 'event.name', + HTTP_REQUEST_BODY_SIZE: 'http.request.body.size', /** - * A unique identifier for the Log Record. + * HTTP request method. * - * Note: If an id is provided, other log records with the same id will be considered duplicates and can be removed safely. This means, that two distinguishable log records MUST have different values. -The id MAY be an [Universally Unique Lexicographically Sortable Identifier (ULID)](https://github.com/ulid/spec), but other identifiers (e.g. UUID) may be used as needed. - */ - LOG_RECORD_UID: 'log.record.uid', - - /** - * The stream associated with the log. See below for a list of well-known values. - */ - LOG_IOSTREAM: 'log.iostream', - - /** - * The basename of the file. - */ - LOG_FILE_NAME: 'log.file.name', + * Note: HTTP request method value SHOULD be "known" to the instrumentation. +By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods) +and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). - /** - * The basename of the file, with symlinks resolved. - */ - LOG_FILE_NAME_RESOLVED: 'log.file.name_resolved', +If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`. - /** - * The full path to the file. - */ - LOG_FILE_PATH: 'log.file.path', +If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override +the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named +OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods +(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults). - /** - * The full path to the file, with symlinks resolved. +HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. +Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. +Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. */ - LOG_FILE_PATH_RESOLVED: 'log.file.path_resolved', + HTTP_REQUEST_METHOD: 'http.request.method', /** - * This attribute represents the state the application has transitioned into at the occurrence of the event. - * - * Note: The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate#1656902), and from which the `OS terminology` column values are derived. + * Original HTTP method sent by the client in the request line. */ - IOS_STATE: 'ios.state', + HTTP_REQUEST_METHOD_ORIGINAL: 'http.request.method_original', /** - * This attribute represents the state the application has transitioned into at the occurrence of the event. + * The ordinal number of request resending attempt (for any reason, including redirects). * - * Note: The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lc), and from which the `OS identifiers` are derived. - */ - ANDROID_STATE: 'android.state', - - /** - * The name of the connection pool; unique within the instrumented application. In case the connection pool implementation doesn't provide a name, then the [db.connection_string](/docs/database/database-spans.md#connection-level-attributes) should be used. - */ - POOL_NAME: 'pool.name', - - /** - * The state of a connection in the pool. - */ - STATE: 'state', - - /** - * Full type name of the [`IExceptionHandler`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) implementation that handled the exception. - */ - ASPNETCORE_DIAGNOSTICS_HANDLER_TYPE: 'aspnetcore.diagnostics.handler.type', - - /** - * Rate limiting policy name. - */ - ASPNETCORE_RATE_LIMITING_POLICY: 'aspnetcore.rate_limiting.policy', - - /** - * Rate-limiting result, shows whether the lease was acquired or contains a rejection reason. - */ - ASPNETCORE_RATE_LIMITING_RESULT: 'aspnetcore.rate_limiting.result', - - /** - * Flag indicating if request was handled by the application pipeline. - */ - ASPNETCORE_REQUEST_IS_UNHANDLED: 'aspnetcore.request.is_unhandled', - - /** - * A value that indicates whether the matched route is a fallback route. + * Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). */ - ASPNETCORE_ROUTING_IS_FALLBACK: 'aspnetcore.routing.is_fallback', + HTTP_REQUEST_RESEND_COUNT: 'http.request.resend_count', /** - * Match result - success or failure. + * The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. */ - ASPNETCORE_ROUTING_MATCH_STATUS: 'aspnetcore.routing.match_status', + HTTP_RESPONSE_BODY_SIZE: 'http.response.body.size', /** - * ASP.NET Core exception middleware handling result. + * [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). */ - ASPNETCORE_DIAGNOSTICS_EXCEPTION_RESULT: 'aspnetcore.diagnostics.exception.result', + HTTP_RESPONSE_STATUS_CODE: 'http.response.status_code', /** - * The name being queried. + * The matched route, that is, the path template in the format used by the respective server framework. * - * Note: The name being queried. -If the name field contains non-printable characters (below 32 or above 126), those characters should be represented as escaped base 10 integers (\DDD). Back slashes and quotes should be escaped. Tabs, carriage returns, and line feeds should be converted to \t, \r, and \n respectively. - */ - DNS_QUESTION_NAME: 'dns.question.name', - - /** - * State of the HTTP connection in the HTTP connection pool. - */ - HTTP_CONNECTION_STATE: 'http.connection.state', - - /** - * SignalR HTTP connection closure status. + * Note: MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. +SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. */ - SIGNALR_CONNECTION_STATUS: 'signalr.connection.status', + HTTP_ROUTE: 'http.route', /** - * [SignalR transport type](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/TransportProtocols.md). + * Peer address of the network connection - IP address or Unix domain socket name. */ - SIGNALR_TRANSPORT: 'signalr.transport', + NETWORK_PEER_ADDRESS: 'network.peer.address', /** - * Name of the buffer pool. - * - * Note: Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()). + * Peer port number of the network connection. */ - JVM_BUFFER_POOL_NAME: 'jvm.buffer.pool.name', + NETWORK_PEER_PORT: 'network.peer.port', /** - * Name of the memory pool. + * [OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent. * - * Note: Pool names are generally obtained via [MemoryPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html#getName()). - */ - JVM_MEMORY_POOL_NAME: 'jvm.memory.pool.name', - - /** - * The type of memory. + * Note: The value SHOULD be normalized to lowercase. */ - JVM_MEMORY_TYPE: 'jvm.memory.type', + NETWORK_PROTOCOL_NAME: 'network.protocol.name', /** - * Name of the garbage collector action. + * Version of the protocol specified in `network.protocol.name`. * - * Note: Garbage collector action is generally obtained via [GarbageCollectionNotificationInfo#getGcAction()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcAction()). + * Note: `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. */ - JVM_GC_ACTION: 'jvm.gc.action', + NETWORK_PROTOCOL_VERSION: 'network.protocol.version', /** - * Name of the garbage collector. + * Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. * - * Note: Garbage collector name is generally obtained via [GarbageCollectionNotificationInfo#getGcName()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcName()). - */ - JVM_GC_NAME: 'jvm.gc.name', - - /** - * Whether the thread is daemon or not. - */ - JVM_THREAD_DAEMON: 'jvm.thread.daemon', - - /** - * State of the thread. - */ - JVM_THREAD_STATE: 'jvm.thread.state', - - /** - * The device identifier. - */ - SYSTEM_DEVICE: 'system.device', - - /** - * The logical CPU number [0..n-1]. - */ - SYSTEM_CPU_LOGICAL_NUMBER: 'system.cpu.logical_number', - - /** - * The state of the CPU. - */ - SYSTEM_CPU_STATE: 'system.cpu.state', - - /** - * The memory state. - */ - SYSTEM_MEMORY_STATE: 'system.memory.state', - - /** - * The paging access direction. - */ - SYSTEM_PAGING_DIRECTION: 'system.paging.direction', - - /** - * The memory paging state. - */ - SYSTEM_PAGING_STATE: 'system.paging.state', - - /** - * The memory paging type. - */ - SYSTEM_PAGING_TYPE: 'system.paging.type', - - /** - * The filesystem mode. - */ - SYSTEM_FILESYSTEM_MODE: 'system.filesystem.mode', - - /** - * The filesystem mount path. - */ - SYSTEM_FILESYSTEM_MOUNTPOINT: 'system.filesystem.mountpoint', - - /** - * The filesystem state. - */ - SYSTEM_FILESYSTEM_STATE: 'system.filesystem.state', - - /** - * The filesystem type. - */ - SYSTEM_FILESYSTEM_TYPE: 'system.filesystem.type', - - /** - * A stateless protocol MUST NOT set this attribute. - */ - SYSTEM_NETWORK_STATE: 'system.network.state', - - /** - * The process state, e.g., [Linux Process State Codes](https://man7.org/linux/man-pages/man1/ps.1.html#PROCESS_STATE_CODES). + * Note: When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available. */ - SYSTEM_PROCESSES_STATUS: 'system.processes.status', + SERVER_ADDRESS: 'server.address', /** - * Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + * Server port number. * - * Note: When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent the client address behind any intermediaries, for example proxies, if it's available. + * Note: When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available. */ - CLIENT_ADDRESS: 'client.address', + SERVER_PORT: 'server.port', /** - * Client port number. + * Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). * - * Note: When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent the client port behind any intermediaries, for example proxies, if it's available. - */ - CLIENT_PORT: 'client.port', - - /** - * The column number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. - */ - CODE_COLUMN: 'code.column', - - /** - * The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). - */ - CODE_FILEPATH: 'code.filepath', - - /** - * The method or function name, or equivalent (usually rightmost part of the code unit's name). - */ - CODE_FUNCTION: 'code.function', - - /** - * The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. - */ - CODE_LINENO: 'code.lineno', - - /** - * The "namespace" within which `code.function` is defined. Usually the qualified class or module name, such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. - */ - CODE_NAMESPACE: 'code.namespace', - - /** - * A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. - */ - CODE_STACKTRACE: 'code.stacktrace', - - /** - * The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). - */ - DB_CASSANDRA_CONSISTENCY_LEVEL: 'db.cassandra.consistency_level', - - /** - * The data center of the coordinating node for a query. - */ - DB_CASSANDRA_COORDINATOR_DC: 'db.cassandra.coordinator.dc', - - /** - * The ID of the coordinating node for a query. - */ - DB_CASSANDRA_COORDINATOR_ID: 'db.cassandra.coordinator.id', - - /** - * Whether or not the query is idempotent. - */ - DB_CASSANDRA_IDEMPOTENCE: 'db.cassandra.idempotence', - - /** - * The fetch size used for paging, i.e. how many rows will be returned at once. + * Note: For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment is not transmitted over HTTP, but if it is known, it SHOULD be included nevertheless. +`url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case username and password SHOULD be redacted and attribute's value SHOULD be `https://REDACTED:REDACTED@www.example.com/`. +`url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes. */ - DB_CASSANDRA_PAGE_SIZE: 'db.cassandra.page_size', + URL_FULL: 'url.full', /** - * The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. + * The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. */ - DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT: 'db.cassandra.speculative_execution_count', + URL_PATH: 'url.path', /** - * The name of the primary Cassandra table that the operation is acting upon, including the keyspace name (if applicable). + * The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. * - * Note: This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. - */ - DB_CASSANDRA_TABLE: 'db.cassandra.table', - - /** - * The connection string used to connect to the database. It is recommended to remove embedded credentials. - */ - DB_CONNECTION_STRING: 'db.connection_string', - - /** - * Unique Cosmos client instance id. - */ - DB_COSMOSDB_CLIENT_ID: 'db.cosmosdb.client_id', - - /** - * Cosmos client connection mode. - */ - DB_COSMOSDB_CONNECTION_MODE: 'db.cosmosdb.connection_mode', - - /** - * Cosmos DB container name. - */ - DB_COSMOSDB_CONTAINER: 'db.cosmosdb.container', - - /** - * CosmosDB Operation Type. - */ - DB_COSMOSDB_OPERATION_TYPE: 'db.cosmosdb.operation_type', - - /** - * RU consumed for that operation. - */ - DB_COSMOSDB_REQUEST_CHARGE: 'db.cosmosdb.request_charge', - - /** - * Request payload size in bytes. - */ - DB_COSMOSDB_REQUEST_CONTENT_LENGTH: 'db.cosmosdb.request_content_length', - - /** - * Cosmos DB status code. - */ - DB_COSMOSDB_STATUS_CODE: 'db.cosmosdb.status_code', - - /** - * Cosmos DB sub status code. - */ - DB_COSMOSDB_SUB_STATUS_CODE: 'db.cosmosdb.sub_status_code', - - /** - * Represents the identifier of an Elasticsearch cluster. - */ - DB_ELASTICSEARCH_CLUSTER_NAME: 'db.elasticsearch.cluster.name', - - /** - * Represents the human-readable identifier of the node/instance to which a request was routed. - */ - DB_ELASTICSEARCH_NODE_NAME: 'db.elasticsearch.node.name', - - /** - * An identifier (address, unique name, or any other identifier) of the database instance that is executing queries or mutations on the current connection. This is useful in cases where the database is running in a clustered environment and the instrumentation is able to record the node executing the query. The client may obtain this value in databases like MySQL using queries like `select @@hostname`. - */ - DB_INSTANCE_ID: 'db.instance.id', - - /** - * The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. + * Note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. */ - DB_JDBC_DRIVER_CLASSNAME: 'db.jdbc.driver_classname', + URL_QUERY: 'url.query', /** - * The MongoDB collection being accessed within the database stated in `db.name`. + * The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. */ - DB_MONGODB_COLLECTION: 'db.mongodb.collection', + URL_SCHEME: 'url.scheme', /** - * The Microsoft SQL Server [instance name](https://docs.microsoft.com/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance. - * - * Note: If setting a `db.mssql.instance_name`, `server.port` is no longer required (but still recommended if non-standard). + * Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. */ - DB_MSSQL_INSTANCE_NAME: 'db.mssql.instance_name', - - /** - * This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails). - * - * Note: In some SQL databases, the database name to be used is called "schema name". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name). - */ - DB_NAME: 'db.name', - - /** - * The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword. - * - * Note: When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted. - */ - DB_OPERATION: 'db.operation', - - /** - * The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute. - */ - DB_REDIS_DATABASE_INDEX: 'db.redis.database_index', - - /** - * The name of the primary table that the operation is acting upon, including the database name (if applicable). - * - * Note: It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. - */ - DB_SQL_TABLE: 'db.sql.table', - - /** - * The database statement being executed. - */ - DB_STATEMENT: 'db.statement', - - /** - * An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. - */ - DB_SYSTEM: 'db.system', - - /** - * Username for accessing the database. - */ - DB_USER: 'db.user', - - /** - * Deprecated, use `network.protocol.name` instead. - * - * @deprecated Replaced by `network.protocol.name`. - */ - HTTP_FLAVOR: 'http.flavor', - - /** - * Deprecated, use `http.request.method` instead. - * - * @deprecated Replaced by `http.request.method`. - */ - HTTP_METHOD: 'http.method', - - /** - * Deprecated, use `http.request.header.content-length` instead. - * - * @deprecated Replaced by `http.request.header.content-length`. - */ - HTTP_REQUEST_CONTENT_LENGTH: 'http.request_content_length', - - /** - * Deprecated, use `http.response.header.content-length` instead. - * - * @deprecated Replaced by `http.response.header.content-length`. - */ - HTTP_RESPONSE_CONTENT_LENGTH: 'http.response_content_length', - - /** - * Deprecated, use `url.scheme` instead. - * - * @deprecated Replaced by `url.scheme` instead. - */ - HTTP_SCHEME: 'http.scheme', - - /** - * Deprecated, use `http.response.status_code` instead. - * - * @deprecated Replaced by `http.response.status_code`. - */ - HTTP_STATUS_CODE: 'http.status_code', - - /** - * Deprecated, use `url.path` and `url.query` instead. - * - * @deprecated Split to `url.path` and `url.query. - */ - HTTP_TARGET: 'http.target', - - /** - * Deprecated, use `url.full` instead. - * - * @deprecated Replaced by `url.full`. - */ - HTTP_URL: 'http.url', - - /** - * Deprecated, use `user_agent.original` instead. - * - * @deprecated Replaced by `user_agent.original`. - */ - HTTP_USER_AGENT: 'http.user_agent', - - /** - * Deprecated, use `server.address`. - * - * @deprecated Replaced by `server.address`. - */ - NET_HOST_NAME: 'net.host.name', - - /** - * Deprecated, use `server.port`. - * - * @deprecated Replaced by `server.port`. - */ - NET_HOST_PORT: 'net.host.port', - - /** - * Deprecated, use `server.address` on client spans and `client.address` on server spans. - * - * @deprecated Replaced by `server.address` on client spans and `client.address` on server spans. - */ - NET_PEER_NAME: 'net.peer.name', - - /** - * Deprecated, use `server.port` on client spans and `client.port` on server spans. - * - * @deprecated Replaced by `server.port` on client spans and `client.port` on server spans. - */ - NET_PEER_PORT: 'net.peer.port', - - /** - * Deprecated, use `network.protocol.name`. - * - * @deprecated Replaced by `network.protocol.name`. - */ - NET_PROTOCOL_NAME: 'net.protocol.name', - - /** - * Deprecated, use `network.protocol.version`. - * - * @deprecated Replaced by `network.protocol.version`. - */ - NET_PROTOCOL_VERSION: 'net.protocol.version', - - /** - * Deprecated, use `network.transport` and `network.type`. - * - * @deprecated Split to `network.transport` and `network.type`. - */ - NET_SOCK_FAMILY: 'net.sock.family', - - /** - * Deprecated, use `network.local.address`. - * - * @deprecated Replaced by `network.local.address`. - */ - NET_SOCK_HOST_ADDR: 'net.sock.host.addr', - - /** - * Deprecated, use `network.local.port`. - * - * @deprecated Replaced by `network.local.port`. - */ - NET_SOCK_HOST_PORT: 'net.sock.host.port', - - /** - * Deprecated, use `network.peer.address`. - * - * @deprecated Replaced by `network.peer.address`. - */ - NET_SOCK_PEER_ADDR: 'net.sock.peer.addr', - - /** - * Deprecated, no replacement at this time. - * - * @deprecated Removed. - */ - NET_SOCK_PEER_NAME: 'net.sock.peer.name', - - /** - * Deprecated, use `network.peer.port`. - * - * @deprecated Replaced by `network.peer.port`. - */ - NET_SOCK_PEER_PORT: 'net.sock.peer.port', - - /** - * Deprecated, use `network.transport`. - * - * @deprecated Replaced by `network.transport`. - */ - NET_TRANSPORT: 'net.transport', - - /** - * Destination address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. - * - * Note: When observed from the source side, and when communicating through an intermediary, `destination.address` SHOULD represent the destination address behind any intermediaries, for example proxies, if it's available. - */ - DESTINATION_ADDRESS: 'destination.address', - - /** - * Destination port number. - */ - DESTINATION_PORT: 'destination.port', - - /** - * The disk IO operation direction. - */ - DISK_IO_DIRECTION: 'disk.io.direction', - - /** - * Describes a class of error the operation ended with. - * - * Note: The `error.type` SHOULD be predictable and SHOULD have low cardinality. -Instrumentations SHOULD document the list of errors they report. - -The cardinality of `error.type` within one instrumentation library SHOULD be low. -Telemetry consumers that aggregate data from multiple instrumentation libraries and applications -should be prepared for `error.type` to have high cardinality at query time when no -additional filters are applied. - -If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. - -If a specific domain defines its own set of error identifiers (such as HTTP or gRPC status codes), -it's RECOMMENDED to: - -* Use a domain-specific attribute -* Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. - */ - ERROR_TYPE: 'error.type', - - /** - * SHOULD be set to true if the exception event is recorded at a point where it is known that the exception is escaping the scope of the span. - * - * Note: An exception is considered to have escaped (or left) the scope of a span, -if that span is ended while the exception is still logically "in flight". -This may be actually "in flight" in some languages (e.g. if the exception -is passed to a Context manager's `__exit__` method in Python) but will -usually be caught at the point of recording the exception in most languages. - -It is usually not possible to determine at the point where an exception is thrown -whether it will escape the scope of a span. -However, it is trivial to know that an exception -will escape, if one checks for an active exception just before ending the span, -as done in the [example for recording span exceptions](#recording-an-exception). - -It follows that an exception may still escape the scope of the span -even if the `exception.escaped` attribute was not set or set to false, -since the event might have been recorded at a time where it was not -clear whether the exception will escape. - */ - EXCEPTION_ESCAPED: 'exception.escaped', - - /** - * The exception message. - */ - EXCEPTION_MESSAGE: 'exception.message', - - /** - * A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. - */ - EXCEPTION_STACKTRACE: 'exception.stacktrace', - - /** - * The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. - */ - EXCEPTION_TYPE: 'exception.type', - - /** - * The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. - */ - HTTP_REQUEST_BODY_SIZE: 'http.request.body.size', - - /** - * HTTP request method. - * - * Note: HTTP request method value SHOULD be "known" to the instrumentation. -By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods) -and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). - -If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`. - -If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override -the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named -OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods -(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults). - -HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. -Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. -Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. - */ - HTTP_REQUEST_METHOD: 'http.request.method', - - /** - * Original HTTP method sent by the client in the request line. - */ - HTTP_REQUEST_METHOD_ORIGINAL: 'http.request.method_original', - - /** - * The ordinal number of request resending attempt (for any reason, including redirects). - * - * Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). - */ - HTTP_REQUEST_RESEND_COUNT: 'http.request.resend_count', - - /** - * The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. - */ - HTTP_RESPONSE_BODY_SIZE: 'http.response.body.size', - - /** - * [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). - */ - HTTP_RESPONSE_STATUS_CODE: 'http.response.status_code', - - /** - * The matched route, that is, the path template in the format used by the respective server framework. - * - * Note: MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. -SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. - */ - HTTP_ROUTE: 'http.route', - - /** - * The number of messages sent, received, or processed in the scope of the batching operation. - * - * Note: Instrumentations SHOULD NOT set `messaging.batch.message_count` on spans that operate with a single message. When a messaging client library supports both batch and single-message API for the same operation, instrumentations SHOULD use `messaging.batch.message_count` for batching APIs and SHOULD NOT use it for single-message APIs. - */ - MESSAGING_BATCH_MESSAGE_COUNT: 'messaging.batch.message_count', - - /** - * A unique identifier for the client that consumes or produces a message. - */ - MESSAGING_CLIENT_ID: 'messaging.client_id', - - /** - * A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name). - */ - MESSAGING_DESTINATION_ANONYMOUS: 'messaging.destination.anonymous', - - /** - * The message destination name. - * - * Note: Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If -the broker doesn't have such notion, the destination name SHOULD uniquely identify the broker. - */ - MESSAGING_DESTINATION_NAME: 'messaging.destination.name', - - /** - * Low cardinality representation of the messaging destination name. - * - * Note: Destination names could be constructed from templates. An example would be a destination name involving a user name or product id. Although the destination name in this case is of high cardinality, the underlying template is of low cardinality and can be effectively used for grouping and aggregation. - */ - MESSAGING_DESTINATION_TEMPLATE: 'messaging.destination.template', - - /** - * A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed. - */ - MESSAGING_DESTINATION_TEMPORARY: 'messaging.destination.temporary', - - /** - * A boolean that is true if the publish message destination is anonymous (could be unnamed or have auto-generated name). - */ - MESSAGING_DESTINATION_PUBLISH_ANONYMOUS: 'messaging.destination_publish.anonymous', - - /** - * The name of the original destination the message was published to. - * - * Note: The name SHOULD uniquely identify a specific queue, topic, or other entity within the broker. If -the broker doesn't have such notion, the original destination name SHOULD uniquely identify the broker. - */ - MESSAGING_DESTINATION_PUBLISH_NAME: 'messaging.destination_publish.name', - - /** - * The ordering key for a given message. If the attribute is not present, the message does not have an ordering key. - */ - MESSAGING_GCP_PUBSUB_MESSAGE_ORDERING_KEY: 'messaging.gcp_pubsub.message.ordering_key', - - /** - * Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers. - */ - MESSAGING_KAFKA_CONSUMER_GROUP: 'messaging.kafka.consumer.group', - - /** - * Partition the message is sent to. - */ - MESSAGING_KAFKA_DESTINATION_PARTITION: 'messaging.kafka.destination.partition', - - /** - * Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message.id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. - * - * Note: If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. - */ - MESSAGING_KAFKA_MESSAGE_KEY: 'messaging.kafka.message.key', - - /** - * The offset of a record in the corresponding Kafka partition. - */ - MESSAGING_KAFKA_MESSAGE_OFFSET: 'messaging.kafka.message.offset', - - /** - * A boolean that is true if the message is a tombstone. - */ - MESSAGING_KAFKA_MESSAGE_TOMBSTONE: 'messaging.kafka.message.tombstone', - - /** - * The size of the message body in bytes. - * - * Note: This can refer to both the compressed or uncompressed body size. If both sizes are known, the uncompressed -body size should be used. - */ - MESSAGING_MESSAGE_BODY_SIZE: 'messaging.message.body.size', - - /** - * The conversation ID identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". - */ - MESSAGING_MESSAGE_CONVERSATION_ID: 'messaging.message.conversation_id', - - /** - * The size of the message body and metadata in bytes. - * - * Note: This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed -size should be used. - */ - MESSAGING_MESSAGE_ENVELOPE_SIZE: 'messaging.message.envelope.size', - - /** - * A value used by the messaging system as an identifier for the message, represented as a string. - */ - MESSAGING_MESSAGE_ID: 'messaging.message.id', - - /** - * A string identifying the kind of messaging operation. - * - * Note: If a custom value is used, it MUST be of low cardinality. - */ - MESSAGING_OPERATION: 'messaging.operation', - - /** - * RabbitMQ message routing key. - */ - MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY: 'messaging.rabbitmq.destination.routing_key', - - /** - * Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. - */ - MESSAGING_ROCKETMQ_CLIENT_GROUP: 'messaging.rocketmq.client_group', - - /** - * Model of message consumption. This only applies to consumer spans. - */ - MESSAGING_ROCKETMQ_CONSUMPTION_MODEL: 'messaging.rocketmq.consumption_model', - - /** - * The delay time level for delay message, which determines the message delay time. - */ - MESSAGING_ROCKETMQ_MESSAGE_DELAY_TIME_LEVEL: 'messaging.rocketmq.message.delay_time_level', - - /** - * The timestamp in milliseconds that the delay message is expected to be delivered to consumer. - */ - MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP: 'messaging.rocketmq.message.delivery_timestamp', - - /** - * It is essential for FIFO message. Messages that belong to the same message group are always processed one by one within the same consumer group. - */ - MESSAGING_ROCKETMQ_MESSAGE_GROUP: 'messaging.rocketmq.message.group', - - /** - * Key(s) of message, another way to mark message besides message id. - */ - MESSAGING_ROCKETMQ_MESSAGE_KEYS: 'messaging.rocketmq.message.keys', - - /** - * The secondary classifier of message besides topic. - */ - MESSAGING_ROCKETMQ_MESSAGE_TAG: 'messaging.rocketmq.message.tag', - - /** - * Type of message. - */ - MESSAGING_ROCKETMQ_MESSAGE_TYPE: 'messaging.rocketmq.message.type', - - /** - * Namespace of RocketMQ resources, resources in different namespaces are individual. - */ - MESSAGING_ROCKETMQ_NAMESPACE: 'messaging.rocketmq.namespace', - - /** - * An identifier for the messaging system being used. See below for a list of well-known identifiers. - */ - MESSAGING_SYSTEM: 'messaging.system', - - /** - * The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. - */ - NETWORK_CARRIER_ICC: 'network.carrier.icc', - - /** - * The mobile carrier country code. - */ - NETWORK_CARRIER_MCC: 'network.carrier.mcc', - - /** - * The mobile carrier network code. - */ - NETWORK_CARRIER_MNC: 'network.carrier.mnc', - - /** - * The name of the mobile carrier. - */ - NETWORK_CARRIER_NAME: 'network.carrier.name', - - /** - * This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. - */ - NETWORK_CONNECTION_SUBTYPE: 'network.connection.subtype', - - /** - * The internet connection type. - */ - NETWORK_CONNECTION_TYPE: 'network.connection.type', - - /** - * The network IO operation direction. - */ - NETWORK_IO_DIRECTION: 'network.io.direction', - - /** - * Local address of the network connection - IP address or Unix domain socket name. - */ - NETWORK_LOCAL_ADDRESS: 'network.local.address', - - /** - * Local port number of the network connection. - */ - NETWORK_LOCAL_PORT: 'network.local.port', - - /** - * Peer address of the network connection - IP address or Unix domain socket name. - */ - NETWORK_PEER_ADDRESS: 'network.peer.address', - - /** - * Peer port number of the network connection. - */ - NETWORK_PEER_PORT: 'network.peer.port', - - /** - * [OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent. - * - * Note: The value SHOULD be normalized to lowercase. - */ - NETWORK_PROTOCOL_NAME: 'network.protocol.name', - - /** - * Version of the protocol specified in `network.protocol.name`. - * - * Note: `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. - */ - NETWORK_PROTOCOL_VERSION: 'network.protocol.version', - - /** - * [OSI transport layer](https://osi-model.com/transport-layer/) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication). - * - * Note: The value SHOULD be normalized to lowercase. - -Consider always setting the transport when setting a port number, since -a port number is ambiguous without knowing the transport. For example -different processes could be listening on TCP port 12345 and UDP port 12345. - */ - NETWORK_TRANSPORT: 'network.transport', - - /** - * [OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent. - * - * Note: The value SHOULD be normalized to lowercase. - */ - NETWORK_TYPE: 'network.type', - - /** - * The [error codes](https://connect.build/docs/protocol/#error-codes) of the Connect request. Error codes are always string values. - */ - RPC_CONNECT_RPC_ERROR_CODE: 'rpc.connect_rpc.error_code', - - /** - * The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request. - */ - RPC_GRPC_STATUS_CODE: 'rpc.grpc.status_code', - - /** - * `error.code` property of response if it is an error response. - */ - RPC_JSONRPC_ERROR_CODE: 'rpc.jsonrpc.error_code', - - /** - * `error.message` property of response if it is an error response. - */ - RPC_JSONRPC_ERROR_MESSAGE: 'rpc.jsonrpc.error_message', - - /** - * `id` property of request or response. Since protocol allows id to be int, string, `null` or missing (for notifications), value is expected to be cast to string for simplicity. Use empty string in case of `null` value. Omit entirely if this is a notification. - */ - RPC_JSONRPC_REQUEST_ID: 'rpc.jsonrpc.request_id', - - /** - * Protocol version as in `jsonrpc` property of request/response. Since JSON-RPC 1.0 doesn't specify this, the value can be omitted. - */ - RPC_JSONRPC_VERSION: 'rpc.jsonrpc.version', - - /** - * The name of the (logical) method being called, must be equal to the $method part in the span name. - * - * Note: This is the logical name of the method from the RPC interface perspective, which can be different from the name of any implementing method/function. The `code.function` attribute may be used to store the latter (e.g., method actually executing the call on the server side, RPC client stub method on the client side). - */ - RPC_METHOD: 'rpc.method', - - /** - * The full (logical) name of the service being called, including its package name, if applicable. - * - * Note: This is the logical name of the service from the RPC interface perspective, which can be different from the name of any implementing class. The `code.namespace` attribute may be used to store the latter (despite the attribute name, it may include a class name; e.g., class with method actually executing the call on the server side, RPC client stub class on the client side). - */ - RPC_SERVICE: 'rpc.service', - - /** - * A string identifying the remoting system. See below for a list of well-known identifiers. - */ - RPC_SYSTEM: 'rpc.system', - - /** - * Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. - * - * Note: When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available. - */ - SERVER_ADDRESS: 'server.address', - - /** - * Server port number. - * - * Note: When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available. - */ - SERVER_PORT: 'server.port', - - /** - * Source address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. - * - * Note: When observed from the destination side, and when communicating through an intermediary, `source.address` SHOULD represent the source address behind any intermediaries, for example proxies, if it's available. - */ - SOURCE_ADDRESS: 'source.address', - - /** - * Source port number. - */ - SOURCE_PORT: 'source.port', - - /** - * Current "managed" thread ID (as opposed to OS thread ID). - */ - THREAD_ID: 'thread.id', - - /** - * Current thread name. - */ - THREAD_NAME: 'thread.name', - - /** - * String indicating the [cipher](https://datatracker.ietf.org/doc/html/rfc5246#appendix-A.5) used during the current connection. - * - * Note: The values allowed for `tls.cipher` MUST be one of the `Descriptions` of the [registered TLS Cipher Suits](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#table-tls-parameters-4). - */ - TLS_CIPHER: 'tls.cipher', - - /** - * PEM-encoded stand-alone certificate offered by the client. This is usually mutually-exclusive of `client.certificate_chain` since this value also exists in that list. - */ - TLS_CLIENT_CERTIFICATE: 'tls.client.certificate', - - /** - * Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain. - */ - TLS_CLIENT_CERTIFICATE_CHAIN: 'tls.client.certificate_chain', - - /** - * Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. - */ - TLS_CLIENT_HASH_MD5: 'tls.client.hash.md5', - - /** - * Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. - */ - TLS_CLIENT_HASH_SHA1: 'tls.client.hash.sha1', - - /** - * Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. - */ - TLS_CLIENT_HASH_SHA256: 'tls.client.hash.sha256', - - /** - * Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. - */ - TLS_CLIENT_ISSUER: 'tls.client.issuer', - - /** - * A hash that identifies clients based on how they perform an SSL/TLS handshake. - */ - TLS_CLIENT_JA3: 'tls.client.ja3', - - /** - * Date/Time indicating when client certificate is no longer considered valid. - */ - TLS_CLIENT_NOT_AFTER: 'tls.client.not_after', - - /** - * Date/Time indicating when client certificate is first considered valid. - */ - TLS_CLIENT_NOT_BEFORE: 'tls.client.not_before', - - /** - * Also called an SNI, this tells the server which hostname to which the client is attempting to connect to. - */ - TLS_CLIENT_SERVER_NAME: 'tls.client.server_name', - - /** - * Distinguished name of subject of the x.509 certificate presented by the client. - */ - TLS_CLIENT_SUBJECT: 'tls.client.subject', - - /** - * Array of ciphers offered by the client during the client hello. - */ - TLS_CLIENT_SUPPORTED_CIPHERS: 'tls.client.supported_ciphers', - - /** - * String indicating the curve used for the given cipher, when applicable. - */ - TLS_CURVE: 'tls.curve', - - /** - * Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel. - */ - TLS_ESTABLISHED: 'tls.established', - - /** - * String indicating the protocol being tunneled. Per the values in the [IANA registry](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), this string should be lower case. - */ - TLS_NEXT_PROTOCOL: 'tls.next_protocol', - - /** - * Normalized lowercase protocol name parsed from original string of the negotiated [SSL/TLS protocol version](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html#RETURN-VALUES). - */ - TLS_PROTOCOL_NAME: 'tls.protocol.name', - - /** - * Numeric part of the version parsed from the original string of the negotiated [SSL/TLS protocol version](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html#RETURN-VALUES). - */ - TLS_PROTOCOL_VERSION: 'tls.protocol.version', - - /** - * Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation. - */ - TLS_RESUMED: 'tls.resumed', - - /** - * PEM-encoded stand-alone certificate offered by the server. This is usually mutually-exclusive of `server.certificate_chain` since this value also exists in that list. - */ - TLS_SERVER_CERTIFICATE: 'tls.server.certificate', - - /** - * Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain. - */ - TLS_SERVER_CERTIFICATE_CHAIN: 'tls.server.certificate_chain', - - /** - * Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. - */ - TLS_SERVER_HASH_MD5: 'tls.server.hash.md5', - - /** - * Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. - */ - TLS_SERVER_HASH_SHA1: 'tls.server.hash.sha1', - - /** - * Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. - */ - TLS_SERVER_HASH_SHA256: 'tls.server.hash.sha256', - - /** - * Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. - */ - TLS_SERVER_ISSUER: 'tls.server.issuer', - - /** - * A hash that identifies servers based on how they perform an SSL/TLS handshake. - */ - TLS_SERVER_JA3S: 'tls.server.ja3s', - - /** - * Date/Time indicating when server certificate is no longer considered valid. - */ - TLS_SERVER_NOT_AFTER: 'tls.server.not_after', - - /** - * Date/Time indicating when server certificate is first considered valid. - */ - TLS_SERVER_NOT_BEFORE: 'tls.server.not_before', - - /** - * Distinguished name of subject of the x.509 certificate presented by the server. - */ - TLS_SERVER_SUBJECT: 'tls.server.subject', - - /** - * The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component. - */ - URL_FRAGMENT: 'url.fragment', - - /** - * Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). - * - * Note: For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment is not transmitted over HTTP, but if it is known, it SHOULD be included nevertheless. -`url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case username and password SHOULD be redacted and attribute's value SHOULD be `https://REDACTED:REDACTED@www.example.com/`. -`url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes. - */ - URL_FULL: 'url.full', - - /** - * The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. - */ - URL_PATH: 'url.path', - - /** - * The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. - * - * Note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. - */ - URL_QUERY: 'url.query', - - /** - * The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. - */ - URL_SCHEME: 'url.scheme', - - /** - * Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. - */ - USER_AGENT_ORIGINAL: 'user_agent.original', - - /** - * A unique id to identify a session. - */ - SESSION_ID: 'session.id', - - /** - * The previous `session.id` for this user, when known. - */ - SESSION_PREVIOUS_ID: 'session.previous_id', - - /** - * The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). - * - * Note: This may be different from `cloud.resource_id` if an alias is involved. - */ - AWS_LAMBDA_INVOKED_ARN: 'aws.lambda.invoked_arn', - - /** - * The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. - */ - CLOUDEVENTS_EVENT_ID: 'cloudevents.event_id', - - /** - * The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. - */ - CLOUDEVENTS_EVENT_SOURCE: 'cloudevents.event_source', - - /** - * The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. - */ - CLOUDEVENTS_EVENT_SPEC_VERSION: 'cloudevents.event_spec_version', - - /** - * The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). - */ - CLOUDEVENTS_EVENT_SUBJECT: 'cloudevents.event_subject', - - /** - * The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. - */ - CLOUDEVENTS_EVENT_TYPE: 'cloudevents.event_type', - - /** - * Parent-child Reference type. - * - * Note: The causal relationship between a child Span and a parent Span. - */ - OPENTRACING_REF_TYPE: 'opentracing.ref_type', - - /** - * Name of the code, either "OK" or "ERROR". MUST NOT be set if the status code is UNSET. - */ - OTEL_STATUS_CODE: 'otel.status_code', - - /** - * Description of the Status if it has a value, otherwise not set. - */ - OTEL_STATUS_DESCRIPTION: 'otel.status_description', - - /** - * The invocation ID of the current function invocation. - */ - FAAS_INVOCATION_ID: 'faas.invocation_id', - - /** - * The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. - */ - FAAS_DOCUMENT_COLLECTION: 'faas.document.collection', - - /** - * The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. - */ - FAAS_DOCUMENT_NAME: 'faas.document.name', - - /** - * Describes the type of the operation that was performed on the data. - */ - FAAS_DOCUMENT_OPERATION: 'faas.document.operation', - - /** - * A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). - */ - FAAS_DOCUMENT_TIME: 'faas.document.time', - - /** - * A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). - */ - FAAS_CRON: 'faas.cron', - - /** - * A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). - */ - FAAS_TIME: 'faas.time', - - /** - * A boolean that is true if the serverless function is executed for the first time (aka cold-start). - */ - FAAS_COLDSTART: 'faas.coldstart', - - /** - * The unique identifier of the feature flag. - */ - FEATURE_FLAG_KEY: 'feature_flag.key', - - /** - * The name of the service provider that performs the flag evaluation. - */ - FEATURE_FLAG_PROVIDER_NAME: 'feature_flag.provider_name', - - /** - * SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of the value can be used. - * - * Note: A semantic identifier, commonly referred to as a variant, provides a means -for referring to a value without including the value itself. This can -provide additional context for understanding the meaning behind a value. -For example, the variant `red` maybe be used for the value `#c05543`. - -A stringified version of the value can be used in situations where a -semantic identifier is unavailable. String representation of the value -should be determined by the implementer. - */ - FEATURE_FLAG_VARIANT: 'feature_flag.variant', - - /** - * The AWS request ID as returned in the response headers `x-amz-request-id` or `x-amz-requestid`. - */ - AWS_REQUEST_ID: 'aws.request_id', - - /** - * The value of the `AttributesToGet` request parameter. - */ - AWS_DYNAMODB_ATTRIBUTES_TO_GET: 'aws.dynamodb.attributes_to_get', - - /** - * The value of the `ConsistentRead` request parameter. - */ - AWS_DYNAMODB_CONSISTENT_READ: 'aws.dynamodb.consistent_read', - - /** - * The JSON-serialized value of each item in the `ConsumedCapacity` response field. - */ - AWS_DYNAMODB_CONSUMED_CAPACITY: 'aws.dynamodb.consumed_capacity', - - /** - * The value of the `IndexName` request parameter. - */ - AWS_DYNAMODB_INDEX_NAME: 'aws.dynamodb.index_name', - - /** - * The JSON-serialized value of the `ItemCollectionMetrics` response field. - */ - AWS_DYNAMODB_ITEM_COLLECTION_METRICS: 'aws.dynamodb.item_collection_metrics', - - /** - * The value of the `Limit` request parameter. - */ - AWS_DYNAMODB_LIMIT: 'aws.dynamodb.limit', - - /** - * The value of the `ProjectionExpression` request parameter. - */ - AWS_DYNAMODB_PROJECTION: 'aws.dynamodb.projection', - - /** - * The value of the `ProvisionedThroughput.ReadCapacityUnits` request parameter. - */ - AWS_DYNAMODB_PROVISIONED_READ_CAPACITY: 'aws.dynamodb.provisioned_read_capacity', - - /** - * The value of the `ProvisionedThroughput.WriteCapacityUnits` request parameter. - */ - AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY: 'aws.dynamodb.provisioned_write_capacity', - - /** - * The value of the `Select` request parameter. - */ - AWS_DYNAMODB_SELECT: 'aws.dynamodb.select', - - /** - * The keys in the `RequestItems` object field. - */ - AWS_DYNAMODB_TABLE_NAMES: 'aws.dynamodb.table_names', - - /** - * The JSON-serialized value of each item of the `GlobalSecondaryIndexes` request field. - */ - AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES: 'aws.dynamodb.global_secondary_indexes', - - /** - * The JSON-serialized value of each item of the `LocalSecondaryIndexes` request field. - */ - AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES: 'aws.dynamodb.local_secondary_indexes', - - /** - * The value of the `ExclusiveStartTableName` request parameter. - */ - AWS_DYNAMODB_EXCLUSIVE_START_TABLE: 'aws.dynamodb.exclusive_start_table', - - /** - * The the number of items in the `TableNames` response parameter. - */ - AWS_DYNAMODB_TABLE_COUNT: 'aws.dynamodb.table_count', - - /** - * The value of the `ScanIndexForward` request parameter. - */ - AWS_DYNAMODB_SCAN_FORWARD: 'aws.dynamodb.scan_forward', - - /** - * The value of the `Count` response parameter. - */ - AWS_DYNAMODB_COUNT: 'aws.dynamodb.count', - - /** - * The value of the `ScannedCount` response parameter. - */ - AWS_DYNAMODB_SCANNED_COUNT: 'aws.dynamodb.scanned_count', - - /** - * The value of the `Segment` request parameter. - */ - AWS_DYNAMODB_SEGMENT: 'aws.dynamodb.segment', - - /** - * The value of the `TotalSegments` request parameter. - */ - AWS_DYNAMODB_TOTAL_SEGMENTS: 'aws.dynamodb.total_segments', - - /** - * The JSON-serialized value of each item in the `AttributeDefinitions` request field. - */ - AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS: 'aws.dynamodb.attribute_definitions', - - /** - * The JSON-serialized value of each item in the the `GlobalSecondaryIndexUpdates` request field. - */ - AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES: 'aws.dynamodb.global_secondary_index_updates', - - /** - * The S3 bucket name the request refers to. Corresponds to the `--bucket` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. - * - * Note: The `bucket` attribute is applicable to all S3 operations that reference a bucket, i.e. that require the bucket name as a mandatory parameter. -This applies to almost all S3 operations except `list-buckets`. - */ - AWS_S3_BUCKET: 'aws.s3.bucket', - - /** - * The source object (in the form `bucket`/`key`) for the copy operation. - * - * Note: The `copy_source` attribute applies to S3 copy operations and corresponds to the `--copy-source` parameter -of the [copy-object operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html). -This applies in particular to the following operations: - -- [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) -- [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). - */ - AWS_S3_COPY_SOURCE: 'aws.s3.copy_source', - - /** - * The delete request container that specifies the objects to be deleted. - * - * Note: The `delete` attribute is only applicable to the [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) operation. -The `delete` attribute corresponds to the `--delete` parameter of the -[delete-objects operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html). - */ - AWS_S3_DELETE: 'aws.s3.delete', - - /** - * The S3 object key the request refers to. Corresponds to the `--key` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. - * - * Note: The `key` attribute is applicable to all object-related S3 operations, i.e. that require the object key as a mandatory parameter. -This applies in particular to the following operations: - -- [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) -- [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) -- [get-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-object.html) -- [head-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/head-object.html) -- [put-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-object.html) -- [restore-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/restore-object.html) -- [select-object-content](https://docs.aws.amazon.com/cli/latest/reference/s3api/select-object-content.html) -- [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) -- [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) -- [create-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-multipart-upload.html) -- [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) -- [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) -- [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). - */ - AWS_S3_KEY: 'aws.s3.key', - - /** - * The part number of the part being uploaded in a multipart-upload operation. This is a positive integer between 1 and 10,000. - * - * Note: The `part_number` attribute is only applicable to the [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) -and [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) operations. -The `part_number` attribute corresponds to the `--part-number` parameter of the -[upload-part operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html). - */ - AWS_S3_PART_NUMBER: 'aws.s3.part_number', - - /** - * Upload ID that identifies the multipart upload. - * - * Note: The `upload_id` attribute applies to S3 multipart-upload operations and corresponds to the `--upload-id` parameter -of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) multipart operations. -This applies in particular to the following operations: - -- [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) -- [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) -- [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) -- [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) -- [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html). - */ - AWS_S3_UPLOAD_ID: 'aws.s3.upload_id', - - /** - * The GraphQL document being executed. - * - * Note: The value may be sanitized to exclude sensitive information. - */ - GRAPHQL_DOCUMENT: 'graphql.document', - - /** - * The name of the operation being executed. - */ - GRAPHQL_OPERATION_NAME: 'graphql.operation.name', - - /** - * The type of the operation being executed. - */ - GRAPHQL_OPERATION_TYPE: 'graphql.operation.type', - - /** - * Compressed size of the message in bytes. - */ - MESSAGE_COMPRESSED_SIZE: 'message.compressed_size', - - /** - * MUST be calculated as two different counters starting from `1` one for sent messages and one for received message. - * - * Note: This way we guarantee that the values will be consistent between different implementations. - */ - MESSAGE_ID: 'message.id', - - /** - * Whether this is a received or sent message. - */ - MESSAGE_TYPE: 'message.type', - - /** - * Uncompressed size of the message in bytes. - */ - MESSAGE_UNCOMPRESSED_SIZE: 'message.uncompressed_size', + USER_AGENT_ORIGINAL: 'user_agent.original', } - - -export const FaasInvokedProviderValues = { - /** Alibaba Cloud. */ - ALIBABA_CLOUD: 'alibaba_cloud', - /** Amazon Web Services. */ - AWS: 'aws', - /** Microsoft Azure. */ - AZURE: 'azure', - /** Google Cloud Platform. */ - GCP: 'gcp', - /** Tencent Cloud. */ - TENCENT_CLOUD: 'tencent_cloud', -} as const -export type FaasInvokedProviderValues = typeof FaasInvokedProviderValues[keyof typeof FaasInvokedProviderValues] - - - - -export const FaasTriggerValues = { - /** A response to some data source operation such as a database or filesystem read/write. */ - DATASOURCE: 'datasource', - /** To provide an answer to an inbound HTTP request. */ - HTTP: 'http', - /** A function is set to be executed when messages are sent to a messaging system. */ - PUBSUB: 'pubsub', - /** A function is scheduled to be executed regularly. */ - TIMER: 'timer', - /** If none of the others apply. */ - OTHER: 'other', -} as const -export type FaasTriggerValues = typeof FaasTriggerValues[keyof typeof FaasTriggerValues] - - - - -export const LogIostreamValues = { - /** Logs from stdout stream. */ - STDOUT: 'stdout', - /** Events from stderr stream. */ - STDERR: 'stderr', -} as const -export type LogIostreamValues = typeof LogIostreamValues[keyof typeof LogIostreamValues] - - - - -export const IosStateValues = { - /** The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. */ - ACTIVE: 'active', - /** The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. */ - INACTIVE: 'inactive', - /** The app is now in the background. This value is associated with UIKit notification `applicationDidEnterBackground`. */ - BACKGROUND: 'background', - /** The app is now in the foreground. This value is associated with UIKit notification `applicationWillEnterForeground`. */ - FOREGROUND: 'foreground', - /** The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. */ - TERMINATE: 'terminate', -} as const -export type IosStateValues = typeof IosStateValues[keyof typeof IosStateValues] - - - - -export const AndroidStateValues = { - /** Any time before Activity.onResume() or, if the app has no Activity, Context.startService() has been called in the app for the first time. */ - CREATED: 'created', - /** Any time after Activity.onPause() or, if the app has no Activity, Context.stopService() has been called when the app was in the foreground state. */ - BACKGROUND: 'background', - /** Any time after Activity.onResume() or, if the app has no Activity, Context.startService() has been called when the app was in either the created or background states. */ - FOREGROUND: 'foreground', -} as const -export type AndroidStateValues = typeof AndroidStateValues[keyof typeof AndroidStateValues] - - - - -export const StateValues = { - /** idle. */ - IDLE: 'idle', - /** used. */ - USED: 'used', -} as const -export type StateValues = typeof StateValues[keyof typeof StateValues] - - - - -export const AspnetcoreRateLimitingResultValues = { - /** Lease was acquired. */ - ACQUIRED: 'acquired', - /** Lease request was rejected by the endpoint limiter. */ - ENDPOINT_LIMITER: 'endpoint_limiter', - /** Lease request was rejected by the global limiter. */ - GLOBAL_LIMITER: 'global_limiter', - /** Lease request was canceled. */ - REQUEST_CANCELED: 'request_canceled', -} as const -export type AspnetcoreRateLimitingResultValues = typeof AspnetcoreRateLimitingResultValues[keyof typeof AspnetcoreRateLimitingResultValues] - - - - -export const AspnetcoreRoutingMatchStatusValues = { - /** Match succeeded. */ - SUCCESS: 'success', - /** Match failed. */ - FAILURE: 'failure', -} as const -export type AspnetcoreRoutingMatchStatusValues = typeof AspnetcoreRoutingMatchStatusValues[keyof typeof AspnetcoreRoutingMatchStatusValues] - - - - -export const AspnetcoreDiagnosticsExceptionResultValues = { - /** Exception was handled by the exception handling middleware. */ - HANDLED: 'handled', - /** Exception was not handled by the exception handling middleware. */ - UNHANDLED: 'unhandled', - /** Exception handling was skipped because the response had started. */ - SKIPPED: 'skipped', - /** Exception handling didn't run because the request was aborted. */ - ABORTED: 'aborted', -} as const -export type AspnetcoreDiagnosticsExceptionResultValues = typeof AspnetcoreDiagnosticsExceptionResultValues[keyof typeof AspnetcoreDiagnosticsExceptionResultValues] - - - - -export const HttpConnectionStateValues = { - /** active state. */ - ACTIVE: 'active', - /** idle state. */ - IDLE: 'idle', -} as const -export type HttpConnectionStateValues = typeof HttpConnectionStateValues[keyof typeof HttpConnectionStateValues] - - - - -export const SignalrConnectionStatusValues = { - /** The connection was closed normally. */ - NORMAL_CLOSURE: 'normal_closure', - /** The connection was closed due to a timeout. */ - TIMEOUT: 'timeout', - /** The connection was closed because the app is shutting down. */ - APP_SHUTDOWN: 'app_shutdown', -} as const -export type SignalrConnectionStatusValues = typeof SignalrConnectionStatusValues[keyof typeof SignalrConnectionStatusValues] - - - - -export const SignalrTransportValues = { - /** ServerSentEvents protocol. */ - SERVER_SENT_EVENTS: 'server_sent_events', - /** LongPolling protocol. */ - LONG_POLLING: 'long_polling', - /** WebSockets protocol. */ - WEB_SOCKETS: 'web_sockets', -} as const -export type SignalrTransportValues = typeof SignalrTransportValues[keyof typeof SignalrTransportValues] - - - - -export const JvmMemoryTypeValues = { - /** Heap memory. */ - HEAP: 'heap', - /** Non-heap memory. */ - NON_HEAP: 'non_heap', -} as const -export type JvmMemoryTypeValues = typeof JvmMemoryTypeValues[keyof typeof JvmMemoryTypeValues] - - - - -export const JvmThreadStateValues = { - /** A thread that has not yet started is in this state. */ - NEW: 'new', - /** A thread executing in the Java virtual machine is in this state. */ - RUNNABLE: 'runnable', - /** A thread that is blocked waiting for a monitor lock is in this state. */ - BLOCKED: 'blocked', - /** A thread that is waiting indefinitely for another thread to perform a particular action is in this state. */ - WAITING: 'waiting', - /** A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state. */ - TIMED_WAITING: 'timed_waiting', - /** A thread that has exited is in this state. */ - TERMINATED: 'terminated', -} as const -export type JvmThreadStateValues = typeof JvmThreadStateValues[keyof typeof JvmThreadStateValues] - - - - -export const SystemCpuStateValues = { - /** user. */ - USER: 'user', - /** system. */ - SYSTEM: 'system', - /** nice. */ - NICE: 'nice', - /** idle. */ - IDLE: 'idle', - /** iowait. */ - IOWAIT: 'iowait', - /** interrupt. */ - INTERRUPT: 'interrupt', - /** steal. */ - STEAL: 'steal', -} as const -export type SystemCpuStateValues = typeof SystemCpuStateValues[keyof typeof SystemCpuStateValues] - - - - -export const SystemMemoryStateValues = { - /** used. */ - USED: 'used', - /** free. */ - FREE: 'free', - /** shared. */ - SHARED: 'shared', - /** buffers. */ - BUFFERS: 'buffers', - /** cached. */ - CACHED: 'cached', -} as const -export type SystemMemoryStateValues = typeof SystemMemoryStateValues[keyof typeof SystemMemoryStateValues] - - - - -export const SystemPagingDirectionValues = { - /** in. */ - IN: 'in', - /** out. */ - OUT: 'out', -} as const -export type SystemPagingDirectionValues = typeof SystemPagingDirectionValues[keyof typeof SystemPagingDirectionValues] - - - - -export const SystemPagingStateValues = { - /** used. */ - USED: 'used', - /** free. */ - FREE: 'free', -} as const -export type SystemPagingStateValues = typeof SystemPagingStateValues[keyof typeof SystemPagingStateValues] - - - - -export const SystemPagingTypeValues = { - /** major. */ - MAJOR: 'major', - /** minor. */ - MINOR: 'minor', -} as const -export type SystemPagingTypeValues = typeof SystemPagingTypeValues[keyof typeof SystemPagingTypeValues] - - - - -export const SystemFilesystemStateValues = { - /** used. */ - USED: 'used', - /** free. */ - FREE: 'free', - /** reserved. */ - RESERVED: 'reserved', -} as const -export type SystemFilesystemStateValues = typeof SystemFilesystemStateValues[keyof typeof SystemFilesystemStateValues] - - - - -export const SystemFilesystemTypeValues = { - /** fat32. */ - FAT32: 'fat32', - /** exfat. */ - EXFAT: 'exfat', - /** ntfs. */ - NTFS: 'ntfs', - /** refs. */ - REFS: 'refs', - /** hfsplus. */ - HFSPLUS: 'hfsplus', - /** ext4. */ - EXT4: 'ext4', -} as const -export type SystemFilesystemTypeValues = typeof SystemFilesystemTypeValues[keyof typeof SystemFilesystemTypeValues] - - - - -export const SystemNetworkStateValues = { - /** close. */ - CLOSE: 'close', - /** close_wait. */ - CLOSE_WAIT: 'close_wait', - /** closing. */ - CLOSING: 'closing', - /** delete. */ - DELETE: 'delete', - /** established. */ - ESTABLISHED: 'established', - /** fin_wait_1. */ - FIN_WAIT_1: 'fin_wait_1', - /** fin_wait_2. */ - FIN_WAIT_2: 'fin_wait_2', - /** last_ack. */ - LAST_ACK: 'last_ack', - /** listen. */ - LISTEN: 'listen', - /** syn_recv. */ - SYN_RECV: 'syn_recv', - /** syn_sent. */ - SYN_SENT: 'syn_sent', - /** time_wait. */ - TIME_WAIT: 'time_wait', -} as const -export type SystemNetworkStateValues = typeof SystemNetworkStateValues[keyof typeof SystemNetworkStateValues] - - - - -export const SystemProcessesStatusValues = { - /** running. */ - RUNNING: 'running', - /** sleeping. */ - SLEEPING: 'sleeping', - /** stopped. */ - STOPPED: 'stopped', - /** defunct. */ - DEFUNCT: 'defunct', -} as const -export type SystemProcessesStatusValues = typeof SystemProcessesStatusValues[keyof typeof SystemProcessesStatusValues] - - - - -export const DbCassandraConsistencyLevelValues = { - /** all. */ - ALL: 'all', - /** each_quorum. */ - EACH_QUORUM: 'each_quorum', - /** quorum. */ - QUORUM: 'quorum', - /** local_quorum. */ - LOCAL_QUORUM: 'local_quorum', - /** one. */ - ONE: 'one', - /** two. */ - TWO: 'two', - /** three. */ - THREE: 'three', - /** local_one. */ - LOCAL_ONE: 'local_one', - /** any. */ - ANY: 'any', - /** serial. */ - SERIAL: 'serial', - /** local_serial. */ - LOCAL_SERIAL: 'local_serial', -} as const -export type DbCassandraConsistencyLevelValues = typeof DbCassandraConsistencyLevelValues[keyof typeof DbCassandraConsistencyLevelValues] - - - - -export const DbCosmosdbConnectionModeValues = { - /** Gateway (HTTP) connections mode. */ - GATEWAY: 'gateway', - /** Direct connection. */ - DIRECT: 'direct', -} as const -export type DbCosmosdbConnectionModeValues = typeof DbCosmosdbConnectionModeValues[keyof typeof DbCosmosdbConnectionModeValues] - - - - -export const DbCosmosdbOperationTypeValues = { - /** invalid. */ - INVALID: 'Invalid', - /** create. */ - CREATE: 'Create', - /** patch. */ - PATCH: 'Patch', - /** read. */ - READ: 'Read', - /** read_feed. */ - READ_FEED: 'ReadFeed', - /** delete. */ - DELETE: 'Delete', - /** replace. */ - REPLACE: 'Replace', - /** execute. */ - EXECUTE: 'Execute', - /** query. */ - QUERY: 'Query', - /** head. */ - HEAD: 'Head', - /** head_feed. */ - HEAD_FEED: 'HeadFeed', - /** upsert. */ - UPSERT: 'Upsert', - /** batch. */ - BATCH: 'Batch', - /** query_plan. */ - QUERY_PLAN: 'QueryPlan', - /** execute_javascript. */ - EXECUTE_JAVASCRIPT: 'ExecuteJavaScript', -} as const -export type DbCosmosdbOperationTypeValues = typeof DbCosmosdbOperationTypeValues[keyof typeof DbCosmosdbOperationTypeValues] - - - - -export const DbSystemValues = { - /** Some other SQL database. Fallback only. See notes. */ - OTHER_SQL: 'other_sql', - /** Microsoft SQL Server. */ - MSSQL: 'mssql', - /** Microsoft SQL Server Compact. */ - MSSQLCOMPACT: 'mssqlcompact', - /** MySQL. */ - MYSQL: 'mysql', - /** Oracle Database. */ - ORACLE: 'oracle', - /** IBM Db2. */ - DB2: 'db2', - /** PostgreSQL. */ - POSTGRESQL: 'postgresql', - /** Amazon Redshift. */ - REDSHIFT: 'redshift', - /** Apache Hive. */ - HIVE: 'hive', - /** Cloudscape. */ - CLOUDSCAPE: 'cloudscape', - /** HyperSQL DataBase. */ - HSQLDB: 'hsqldb', - /** Progress Database. */ - PROGRESS: 'progress', - /** SAP MaxDB. */ - MAXDB: 'maxdb', - /** SAP HANA. */ - HANADB: 'hanadb', - /** Ingres. */ - INGRES: 'ingres', - /** FirstSQL. */ - FIRSTSQL: 'firstsql', - /** EnterpriseDB. */ - EDB: 'edb', - /** InterSystems Caché. */ - CACHE: 'cache', - /** Adabas (Adaptable Database System). */ - ADABAS: 'adabas', - /** Firebird. */ - FIREBIRD: 'firebird', - /** Apache Derby. */ - DERBY: 'derby', - /** FileMaker. */ - FILEMAKER: 'filemaker', - /** Informix. */ - INFORMIX: 'informix', - /** InstantDB. */ - INSTANTDB: 'instantdb', - /** InterBase. */ - INTERBASE: 'interbase', - /** MariaDB. */ - MARIADB: 'mariadb', - /** Netezza. */ - NETEZZA: 'netezza', - /** Pervasive PSQL. */ - PERVASIVE: 'pervasive', - /** PointBase. */ - POINTBASE: 'pointbase', - /** SQLite. */ - SQLITE: 'sqlite', - /** Sybase. */ - SYBASE: 'sybase', - /** Teradata. */ - TERADATA: 'teradata', - /** Vertica. */ - VERTICA: 'vertica', - /** H2. */ - H2: 'h2', - /** ColdFusion IMQ. */ - COLDFUSION: 'coldfusion', - /** Apache Cassandra. */ - CASSANDRA: 'cassandra', - /** Apache HBase. */ - HBASE: 'hbase', - /** MongoDB. */ - MONGODB: 'mongodb', - /** Redis. */ - REDIS: 'redis', - /** Couchbase. */ - COUCHBASE: 'couchbase', - /** CouchDB. */ - COUCHDB: 'couchdb', - /** Microsoft Azure Cosmos DB. */ - COSMOSDB: 'cosmosdb', - /** Amazon DynamoDB. */ - DYNAMODB: 'dynamodb', - /** Neo4j. */ - NEO4J: 'neo4j', - /** Apache Geode. */ - GEODE: 'geode', - /** Elasticsearch. */ - ELASTICSEARCH: 'elasticsearch', - /** Memcached. */ - MEMCACHED: 'memcached', - /** CockroachDB. */ - COCKROACHDB: 'cockroachdb', - /** OpenSearch. */ - OPENSEARCH: 'opensearch', - /** ClickHouse. */ - CLICKHOUSE: 'clickhouse', - /** Cloud Spanner. */ - SPANNER: 'spanner', - /** Trino. */ - TRINO: 'trino', -} as const -export type DbSystemValues = typeof DbSystemValues[keyof typeof DbSystemValues] - - - - -export const HttpFlavorValues = { - /** HTTP/1.0. */ - HTTP_1_0: '1.0', - /** HTTP/1.1. */ - HTTP_1_1: '1.1', - /** HTTP/2. */ - HTTP_2_0: '2.0', - /** HTTP/3. */ - HTTP_3_0: '3.0', - /** SPDY protocol. */ - SPDY: 'SPDY', - /** QUIC protocol. */ - QUIC: 'QUIC', -} as const -export type HttpFlavorValues = typeof HttpFlavorValues[keyof typeof HttpFlavorValues] - - - - -export const NetSockFamilyValues = { - /** IPv4 address. */ - INET: 'inet', - /** IPv6 address. */ - INET6: 'inet6', - /** Unix domain socket path. */ - UNIX: 'unix', -} as const -export type NetSockFamilyValues = typeof NetSockFamilyValues[keyof typeof NetSockFamilyValues] - - - - -export const NetTransportValues = { - /** ip_tcp. */ - IP_TCP: 'ip_tcp', - /** ip_udp. */ - IP_UDP: 'ip_udp', - /** Named or anonymous pipe. */ - PIPE: 'pipe', - /** In-process communication. */ - INPROC: 'inproc', - /** Something else (non IP-based). */ - OTHER: 'other', -} as const -export type NetTransportValues = typeof NetTransportValues[keyof typeof NetTransportValues] - - - - -export const DiskIoDirectionValues = { - /** read. */ - READ: 'read', - /** write. */ - WRITE: 'write', -} as const -export type DiskIoDirectionValues = typeof DiskIoDirectionValues[keyof typeof DiskIoDirectionValues] - - - - -export const ErrorTypeValues = { - /** A fallback error value to be used when the instrumentation doesn't define a custom value. */ - OTHER: '_OTHER', -} as const -export type ErrorTypeValues = typeof ErrorTypeValues[keyof typeof ErrorTypeValues] - - - - -export const HttpRequestMethodValues = { - /** CONNECT method. */ - CONNECT: 'CONNECT', - /** DELETE method. */ - DELETE: 'DELETE', - /** GET method. */ - GET: 'GET', - /** HEAD method. */ - HEAD: 'HEAD', - /** OPTIONS method. */ - OPTIONS: 'OPTIONS', - /** PATCH method. */ - PATCH: 'PATCH', - /** POST method. */ - POST: 'POST', - /** PUT method. */ - PUT: 'PUT', - /** TRACE method. */ - TRACE: 'TRACE', - /** Any HTTP method that the instrumentation has no prior knowledge of. */ - OTHER: '_OTHER', -} as const -export type HttpRequestMethodValues = typeof HttpRequestMethodValues[keyof typeof HttpRequestMethodValues] - - - - -export const MessagingOperationValues = { - /** One or more messages are provided for publishing to an intermediary. If a single message is published, the context of the "Publish" span can be used as the creation context and no "Create" span needs to be created. */ - PUBLISH: 'publish', - /** A message is created. "Create" spans always refer to a single message and are used to provide a unique creation context for messages in batch publishing scenarios. */ - CREATE: 'create', - /** One or more messages are requested by a consumer. This operation refers to pull-based scenarios, where consumers explicitly call methods of messaging SDKs to receive messages. */ - RECEIVE: 'receive', - /** One or more messages are passed to a consumer. This operation refers to push-based scenarios, where consumer register callbacks which get called by messaging SDKs. */ - DELIVER: 'deliver', -} as const -export type MessagingOperationValues = typeof MessagingOperationValues[keyof typeof MessagingOperationValues] - - - - -export const MessagingRocketmqConsumptionModelValues = { - /** Clustering consumption model. */ - CLUSTERING: 'clustering', - /** Broadcasting consumption model. */ - BROADCASTING: 'broadcasting', -} as const -export type MessagingRocketmqConsumptionModelValues = typeof MessagingRocketmqConsumptionModelValues[keyof typeof MessagingRocketmqConsumptionModelValues] - - - - -export const MessagingRocketmqMessageTypeValues = { - /** Normal message. */ - NORMAL: 'normal', - /** FIFO message. */ - FIFO: 'fifo', - /** Delay message. */ - DELAY: 'delay', - /** Transaction message. */ - TRANSACTION: 'transaction', -} as const -export type MessagingRocketmqMessageTypeValues = typeof MessagingRocketmqMessageTypeValues[keyof typeof MessagingRocketmqMessageTypeValues] - - - - -export const MessagingSystemValues = { - /** Apache ActiveMQ. */ - ACTIVEMQ: 'activemq', - /** Amazon Simple Queue Service (SQS). */ - AWS_SQS: 'aws_sqs', - /** Azure Event Grid. */ - AZURE_EVENTGRID: 'azure_eventgrid', - /** Azure Event Hubs. */ - AZURE_EVENTHUBS: 'azure_eventhubs', - /** Azure Service Bus. */ - AZURE_SERVICEBUS: 'azure_servicebus', - /** Google Cloud Pub/Sub. */ - GCP_PUBSUB: 'gcp_pubsub', - /** Java Message Service. */ - JMS: 'jms', - /** Apache Kafka. */ - KAFKA: 'kafka', - /** RabbitMQ. */ - RABBITMQ: 'rabbitmq', - /** Apache RocketMQ. */ - ROCKETMQ: 'rocketmq', -} as const -export type MessagingSystemValues = typeof MessagingSystemValues[keyof typeof MessagingSystemValues] - - - - -export const NetworkConnectionSubtypeValues = { - /** GPRS. */ - GPRS: 'gprs', - /** EDGE. */ - EDGE: 'edge', - /** UMTS. */ - UMTS: 'umts', - /** CDMA. */ - CDMA: 'cdma', - /** EVDO Rel. 0. */ - EVDO_0: 'evdo_0', - /** EVDO Rev. A. */ - EVDO_A: 'evdo_a', - /** CDMA2000 1XRTT. */ - CDMA2000_1XRTT: 'cdma2000_1xrtt', - /** HSDPA. */ - HSDPA: 'hsdpa', - /** HSUPA. */ - HSUPA: 'hsupa', - /** HSPA. */ - HSPA: 'hspa', - /** IDEN. */ - IDEN: 'iden', - /** EVDO Rev. B. */ - EVDO_B: 'evdo_b', - /** LTE. */ - LTE: 'lte', - /** EHRPD. */ - EHRPD: 'ehrpd', - /** HSPAP. */ - HSPAP: 'hspap', - /** GSM. */ - GSM: 'gsm', - /** TD-SCDMA. */ - TD_SCDMA: 'td_scdma', - /** IWLAN. */ - IWLAN: 'iwlan', - /** 5G NR (New Radio). */ - NR: 'nr', - /** 5G NRNSA (New Radio Non-Standalone). */ - NRNSA: 'nrnsa', - /** LTE CA. */ - LTE_CA: 'lte_ca', -} as const -export type NetworkConnectionSubtypeValues = typeof NetworkConnectionSubtypeValues[keyof typeof NetworkConnectionSubtypeValues] - - - - -export const NetworkConnectionTypeValues = { - /** wifi. */ - WIFI: 'wifi', - /** wired. */ - WIRED: 'wired', - /** cell. */ - CELL: 'cell', - /** unavailable. */ - UNAVAILABLE: 'unavailable', - /** unknown. */ - UNKNOWN: 'unknown', -} as const -export type NetworkConnectionTypeValues = typeof NetworkConnectionTypeValues[keyof typeof NetworkConnectionTypeValues] - - - - -export const NetworkIoDirectionValues = { - /** transmit. */ - TRANSMIT: 'transmit', - /** receive. */ - RECEIVE: 'receive', -} as const -export type NetworkIoDirectionValues = typeof NetworkIoDirectionValues[keyof typeof NetworkIoDirectionValues] - - - - -export const NetworkTransportValues = { - /** TCP. */ - TCP: 'tcp', - /** UDP. */ - UDP: 'udp', - /** Named or anonymous pipe. */ - PIPE: 'pipe', - /** Unix domain socket. */ - UNIX: 'unix', -} as const -export type NetworkTransportValues = typeof NetworkTransportValues[keyof typeof NetworkTransportValues] - - - - -export const NetworkTypeValues = { - /** IPv4. */ - IPV4: 'ipv4', - /** IPv6. */ - IPV6: 'ipv6', -} as const -export type NetworkTypeValues = typeof NetworkTypeValues[keyof typeof NetworkTypeValues] - - - - -export const RpcConnectRpcErrorCodeValues = { - /** cancelled. */ - CANCELLED: 'cancelled', - /** unknown. */ - UNKNOWN: 'unknown', - /** invalid_argument. */ - INVALID_ARGUMENT: 'invalid_argument', - /** deadline_exceeded. */ - DEADLINE_EXCEEDED: 'deadline_exceeded', - /** not_found. */ - NOT_FOUND: 'not_found', - /** already_exists. */ - ALREADY_EXISTS: 'already_exists', - /** permission_denied. */ - PERMISSION_DENIED: 'permission_denied', - /** resource_exhausted. */ - RESOURCE_EXHAUSTED: 'resource_exhausted', - /** failed_precondition. */ - FAILED_PRECONDITION: 'failed_precondition', - /** aborted. */ - ABORTED: 'aborted', - /** out_of_range. */ - OUT_OF_RANGE: 'out_of_range', - /** unimplemented. */ - UNIMPLEMENTED: 'unimplemented', - /** internal. */ - INTERNAL: 'internal', - /** unavailable. */ - UNAVAILABLE: 'unavailable', - /** data_loss. */ - DATA_LOSS: 'data_loss', - /** unauthenticated. */ - UNAUTHENTICATED: 'unauthenticated', -} as const -export type RpcConnectRpcErrorCodeValues = typeof RpcConnectRpcErrorCodeValues[keyof typeof RpcConnectRpcErrorCodeValues] - - - - -export const RpcGrpcStatusCodeValues = { - /** OK. */ - OK: 0, - /** CANCELLED. */ - CANCELLED: 1, - /** UNKNOWN. */ - UNKNOWN: 2, - /** INVALID_ARGUMENT. */ - INVALID_ARGUMENT: 3, - /** DEADLINE_EXCEEDED. */ - DEADLINE_EXCEEDED: 4, - /** NOT_FOUND. */ - NOT_FOUND: 5, - /** ALREADY_EXISTS. */ - ALREADY_EXISTS: 6, - /** PERMISSION_DENIED. */ - PERMISSION_DENIED: 7, - /** RESOURCE_EXHAUSTED. */ - RESOURCE_EXHAUSTED: 8, - /** FAILED_PRECONDITION. */ - FAILED_PRECONDITION: 9, - /** ABORTED. */ - ABORTED: 10, - /** OUT_OF_RANGE. */ - OUT_OF_RANGE: 11, - /** UNIMPLEMENTED. */ - UNIMPLEMENTED: 12, - /** INTERNAL. */ - INTERNAL: 13, - /** UNAVAILABLE. */ - UNAVAILABLE: 14, - /** DATA_LOSS. */ - DATA_LOSS: 15, - /** UNAUTHENTICATED. */ - UNAUTHENTICATED: 16, -} as const -export type RpcGrpcStatusCodeValues = typeof RpcGrpcStatusCodeValues[keyof typeof RpcGrpcStatusCodeValues] - - - - -export const RpcSystemValues = { - /** gRPC. */ - GRPC: 'grpc', - /** Java RMI. */ - JAVA_RMI: 'java_rmi', - /** .NET WCF. */ - DOTNET_WCF: 'dotnet_wcf', - /** Apache Dubbo. */ - APACHE_DUBBO: 'apache_dubbo', - /** Connect RPC. */ - CONNECT_RPC: 'connect_rpc', -} as const -export type RpcSystemValues = typeof RpcSystemValues[keyof typeof RpcSystemValues] - - - - -export const TlsProtocolNameValues = { - /** ssl. */ - SSL: 'ssl', - /** tls. */ - TLS: 'tls', -} as const -export type TlsProtocolNameValues = typeof TlsProtocolNameValues[keyof typeof TlsProtocolNameValues] - - - - -export const OpentracingRefTypeValues = { - /** The parent Span depends on the child Span in some capacity. */ - CHILD_OF: 'child_of', - /** The parent Span doesn't depend in any way on the result of the child Span. */ - FOLLOWS_FROM: 'follows_from', -} as const -export type OpentracingRefTypeValues = typeof OpentracingRefTypeValues[keyof typeof OpentracingRefTypeValues] - - - - -export const OtelStatusCodeValues = { - /** The operation has been validated by an Application developer or Operator to have completed successfully. */ - OK: 'OK', - /** The operation contains an error. */ - ERROR: 'ERROR', -} as const -export type OtelStatusCodeValues = typeof OtelStatusCodeValues[keyof typeof OtelStatusCodeValues] - - - - -export const FaasDocumentOperationValues = { - /** When a new object is created. */ - INSERT: 'insert', - /** When an object is modified. */ - EDIT: 'edit', - /** When an object is deleted. */ - DELETE: 'delete', -} as const -export type FaasDocumentOperationValues = typeof FaasDocumentOperationValues[keyof typeof FaasDocumentOperationValues] - - - - -export const GraphqlOperationTypeValues = { - /** GraphQL query. */ - QUERY: 'query', - /** GraphQL mutation. */ - MUTATION: 'mutation', - /** GraphQL subscription. */ - SUBSCRIPTION: 'subscription', -} as const -export type GraphqlOperationTypeValues = typeof GraphqlOperationTypeValues[keyof typeof GraphqlOperationTypeValues] - - - - -export const MessageTypeValues = { - /** sent. */ - SENT: 'SENT', - /** received. */ - RECEIVED: 'RECEIVED', -} as const -export type MessageTypeValues = typeof MessageTypeValues[keyof typeof MessageTypeValues] - diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index 07f470db0af..cc018bae06c 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -178,9 +178,7 @@ export class UndiciInstrumentation extends InstrumentationBase { }; const schemePorts: Record = { https: '443', http: '80' }; - // TODO: check this resolution based on headers - // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes - const serverAddress = reqHeaders.get('host') || requestUrl.hostname; + const serverAddress = requestUrl.hostname; const serverPort = requestUrl.port || schemePorts[urlScheme]; attributes[SemanticAttributes.SERVER_ADDRESS] = serverAddress; @@ -206,7 +204,7 @@ export class UndiciInstrumentation extends InstrumentationBase { } // Check if parent span is required via config and: - // - ff a parent is required but not present, we use a `NoopSpan` to still + // - if a parent is required but not present, we use a `NoopSpan` to still // propagate context without recording it. // - create a span otherwise const activeCtx = context.active(); @@ -247,7 +245,7 @@ export class UndiciInstrumentation extends InstrumentationBase { // This is the 2nd message we recevie for each request. It is fired when connection with // the remote is stablished and about to send the first byte. Here do have info about the - // remote addres an port so we can poupulate some `net.*` attributes into the span + // remote addres an port so we can poupulate some `network.*` attributes into the span private onRequestHeaders({ request, socket }: RequestHeadersMessage): void { const record = this._recordFromReq.get(request as UndiciRequest); @@ -324,9 +322,11 @@ export class UndiciInstrumentation extends InstrumentationBase { } // `content-length` header is a special case - const contentLength = Number(resHeaders.get('content-length')); - if (!isNaN(contentLength)) { - spanAttributes['http.response.header.content-length'] = contentLength; + if (resHeaders.has('content-length')) { + const contentLength = Number(resHeaders.get('content-length')); + if (!isNaN(contentLength)) { + spanAttributes['http.response.header.content-length'] = contentLength; + } } span.setAttributes(spanAttributes); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index 72ee2bc180a..afbdd87b19d 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -122,24 +122,6 @@ export const assertSpan = ( span.attributes['http.response.header.content-length'], contentLength ); - // TODO: check compresssed/uncompressed in semantic conventions - // const contentEncodingHeader = validations.resHeaders.get('content-encoding'); - // if ( - // contentEncodingHeader && - // contentEncodingHeader !== 'identity' - // ) { - // assert.strictEqual( - // span.attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH], - // contentLength - // ); - // } else { - // assert.strictEqual( - // span.attributes[ - // SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED - // ], - // contentLength - // ); - // } } } From 7cd7487d59fd66d6b2fb9a0614d42184d6ce1e0c Mon Sep 17 00:00:00 2001 From: David Luna Date: Tue, 20 Feb 2024 00:45:59 +0100 Subject: [PATCH 26/28] chore(instrumentation-undici): fix lint issues --- .../src/enums/SemanticAttributes.ts | 91 ++++++------ .../src/types.ts | 13 +- .../src/undici.ts | 138 +++++++++++------- .../test/fetch.test.ts | 64 ++++---- .../test/metrics.test.ts | 42 +++--- .../test/undici.test.ts | 117 ++++++++------- .../test/utils/assertSpan.ts | 65 +++++---- .../test/utils/mock-propagation.ts | 2 +- .../test/utils/mock-server.ts | 1 - 9 files changed, 299 insertions(+), 234 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts index b6121b00770..0e65b51aeb6 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/enums/SemanticAttributes.ts @@ -16,10 +16,9 @@ // DO NOT EDIT, this is an Auto-generated file from scripts/semconv/templates//templates/SemanticAttributes.ts.j2 export const SemanticAttributes = { - /** - * State of the HTTP connection in the HTTP connection pool. - */ + * State of the HTTP connection in the HTTP connection pool. + */ HTTP_CONNECTION_STATE: 'http.connection.state', /** @@ -44,8 +43,8 @@ it's RECOMMENDED to: ERROR_TYPE: 'error.type', /** - * The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. - */ + * The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. + */ HTTP_REQUEST_BODY_SIZE: 'http.request.body.size', /** @@ -69,25 +68,25 @@ Tracing instrumentations that do so, MUST also set `http.request.method_original HTTP_REQUEST_METHOD: 'http.request.method', /** - * Original HTTP method sent by the client in the request line. - */ + * Original HTTP method sent by the client in the request line. + */ HTTP_REQUEST_METHOD_ORIGINAL: 'http.request.method_original', /** - * The ordinal number of request resending attempt (for any reason, including redirects). - * - * Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). - */ + * The ordinal number of request resending attempt (for any reason, including redirects). + * + * Note: The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). + */ HTTP_REQUEST_RESEND_COUNT: 'http.request.resend_count', /** - * The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. - */ + * The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. + */ HTTP_RESPONSE_BODY_SIZE: 'http.response.body.size', /** - * [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). - */ + * [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). + */ HTTP_RESPONSE_STATUS_CODE: 'http.response.status_code', /** @@ -99,41 +98,41 @@ SHOULD include the [application root](/docs/http/http-spans.md#http-server-defin HTTP_ROUTE: 'http.route', /** - * Peer address of the network connection - IP address or Unix domain socket name. - */ + * Peer address of the network connection - IP address or Unix domain socket name. + */ NETWORK_PEER_ADDRESS: 'network.peer.address', /** - * Peer port number of the network connection. - */ + * Peer port number of the network connection. + */ NETWORK_PEER_PORT: 'network.peer.port', /** - * [OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent. - * - * Note: The value SHOULD be normalized to lowercase. - */ + * [OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent. + * + * Note: The value SHOULD be normalized to lowercase. + */ NETWORK_PROTOCOL_NAME: 'network.protocol.name', /** - * Version of the protocol specified in `network.protocol.name`. - * - * Note: `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. - */ + * Version of the protocol specified in `network.protocol.name`. + * + * Note: `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + */ NETWORK_PROTOCOL_VERSION: 'network.protocol.version', /** - * Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. - * - * Note: When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available. - */ + * Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + * + * Note: When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available. + */ SERVER_ADDRESS: 'server.address', /** - * Server port number. - * - * Note: When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available. - */ + * Server port number. + * + * Note: When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available. + */ SERVER_PORT: 'server.port', /** @@ -146,24 +145,24 @@ SHOULD include the [application root](/docs/http/http-spans.md#http-server-defin URL_FULL: 'url.full', /** - * The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. - */ + * The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component. + */ URL_PATH: 'url.path', /** - * The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. - * - * Note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. - */ + * The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component. + * + * Note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. + */ URL_QUERY: 'url.query', /** - * The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. - */ + * The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + */ URL_SCHEME: 'url.scheme', /** - * Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. - */ + * Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. + */ USER_AGENT_ORIGINAL: 'user_agent.original', -} +}; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts index 30dd2acc694..26b80801297 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/types.ts @@ -16,18 +16,15 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import type { Attributes, Span } from '@opentelemetry/api'; - // TODO: notes about support // - `fetch` API is added in node v16.15.0 // - `undici` supports node >=18 - // TODO: `Request` class was added in node v16.15.0, make it work with v14 // also we do not get that object from the diagnostics channel message but the // core request from https://github.com/nodejs/undici/blob/main/lib/core/request.js // which is not typed - export interface UndiciRequest { origin: string; method: string; @@ -50,14 +47,18 @@ export interface UnidiciResponse { statusCode: number; } - // This package will instrument HTTP requests made through `undici` or `fetch` global API // so it seems logical to have similar options than the HTTP instrumentation -export interface UndiciInstrumentationConfig extends InstrumentationConfig { +export interface UndiciInstrumentationConfig + extends InstrumentationConfig { /** Not trace all outgoing requests that matched with custom function */ ignoreRequestHook?: (request: RequestType) => boolean; /** Function for adding custom attributes after response is handled */ - applyCustomAttributesOnSpan?: (span: Span, request: RequestType, response: Response) => void; + applyCustomAttributesOnSpan?: ( + span: Span, + request: RequestType, + response: Response + ) => void; /** Function for adding custom attributes before request is handled */ requestHook?: (span: Span, request: RequestType) => void; /** Function for adding custom attributes before a span is started in outgoingRequest */ diff --git a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts index cc018bae06c..bdca38b6bdf 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/src/undici.ts @@ -16,7 +16,10 @@ import * as diagch from 'diagnostics_channel'; import { URL } from 'url'; -import { InstrumentationBase, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; +import { + InstrumentationBase, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; import { Attributes, context, @@ -34,10 +37,19 @@ import { import { VERSION } from './version'; -import { ListenerRecord, RequestHeadersMessage, RequestMessage, ResponseHeadersMessage } from './internal-types'; +import { + ListenerRecord, + RequestHeadersMessage, + RequestMessage, + ResponseHeadersMessage, +} from './internal-types'; import { UndiciInstrumentationConfig, UndiciRequest } from './types'; import { SemanticAttributes } from './enums/SemanticAttributes'; -import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from '@opentelemetry/core'; +import { + hrTime, + hrTimeDuration, + hrTimeToMilliseconds, +} from '@opentelemetry/core'; interface IntrumentationRecord { span: Span; @@ -45,7 +57,6 @@ interface IntrumentationRecord { startTime: HrTime; } - // A combination of https://github.com/elastic/apm-agent-nodejs and // https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-undici/src/index.ts export class UndiciInstrumentation extends InstrumentationBase { @@ -53,7 +64,7 @@ export class UndiciInstrumentation extends InstrumentationBase { // unsubscribing. private _channelSubs!: Array; private _recordFromReq = new WeakMap(); - + private _httpClientDurationHistogram!: Histogram; constructor(config?: UndiciInstrumentationConfig) { super('@opentelemetry/instrumentation-undici', VERSION, config); @@ -64,9 +75,9 @@ export class UndiciInstrumentation extends InstrumentationBase { fetch('').catch(() => {}); } catch (err) { // TODO: nicer message - diag.info(`fetch API not available`); + diag.info('fetch API not available'); } - + this.setConfig(config); } @@ -94,16 +105,25 @@ export class UndiciInstrumentation extends InstrumentationBase { // This method is called by the `InstrumentationAbstract` constructor before // ours is called. So we need to ensure the property is initalized this._channelSubs = this._channelSubs || []; - this.subscribeToChannel('undici:request:create', this.onRequestCreated.bind(this)); - this.subscribeToChannel('undici:client:sendHeaders',this.onRequestHeaders.bind(this)); - this.subscribeToChannel('undici:request:headers',this.onResponseHeaders.bind(this)); + this.subscribeToChannel( + 'undici:request:create', + this.onRequestCreated.bind(this) + ); + this.subscribeToChannel( + 'undici:client:sendHeaders', + this.onRequestHeaders.bind(this) + ); + this.subscribeToChannel( + 'undici:request:headers', + this.onResponseHeaders.bind(this) + ); this.subscribeToChannel('undici:request:trailers', this.onDone.bind(this)); this.subscribeToChannel('undici:request:error', this.onError.bind(this)); } override setConfig(config?: UndiciInstrumentationConfig): void { super.setConfig(config); - + if (config?.enabled) { this.enable(); } else { @@ -121,7 +141,7 @@ export class UndiciInstrumentation extends InstrumentationBase { } ); } - + private _getConfig(): UndiciInstrumentationConfig { return this._config as UndiciInstrumentationConfig; } @@ -149,9 +169,12 @@ export class UndiciInstrumentation extends InstrumentationBase { // - method is 'CONNECT' const config = this._getConfig(); const shouldIgnoreReq = safeExecuteInTheMiddle( - () => !config.enabled || request.method === 'CONNECT' || config.ignoreRequestHook?.(request), - (e) => e && this._diag.error('caught ignoreRequestHook error: ', e), - true, + () => + !config.enabled || + request.method === 'CONNECT' || + config.ignoreRequestHook?.(request), + e => e && this._diag.error('caught ignoreRequestHook error: ', e), + true ); if (shouldIgnoreReq) { @@ -160,12 +183,14 @@ export class UndiciInstrumentation extends InstrumentationBase { const startTime = hrTime(); const rawHeaders = request.headers.split('\r\n'); - const reqHeaders = new Map(rawHeaders.map(h => { - const sepIndex = h.indexOf(':'); - const name = h.substring(0, sepIndex).toLowerCase(); - const val = h.substring(sepIndex + 1).trim(); - return [name, val]; - })); + const reqHeaders = new Map( + rawHeaders.map(h => { + const sepIndex = h.indexOf(':'); + const name = h.substring(0, sepIndex).toLowerCase(); + const val = h.substring(sepIndex + 1).trim(); + return [name, val]; + }) + ); const requestUrl = new URL(request.origin + request.path); const urlScheme = requestUrl.protocol.replace(':', ''); @@ -180,7 +205,7 @@ export class UndiciInstrumentation extends InstrumentationBase { const schemePorts: Record = { https: '443', http: '80' }; const serverAddress = requestUrl.hostname; const serverPort = requestUrl.port || schemePorts[urlScheme]; - + attributes[SemanticAttributes.SERVER_ADDRESS] = serverAddress; if (serverPort && !isNaN(Number(serverPort))) { attributes[SemanticAttributes.SERVER_PORT] = Number(serverPort); @@ -194,8 +219,8 @@ export class UndiciInstrumentation extends InstrumentationBase { // Get attributes from the hook if present const hookAttributes = safeExecuteInTheMiddle( () => config.startSpanHook?.(request), - (e) => e && this._diag.error('caught startSpanHook error: ', e), - true, + e => e && this._diag.error('caught startSpanHook error: ', e), + true ); if (hookAttributes) { Object.entries(hookAttributes).forEach(([key, val]) => { @@ -227,8 +252,8 @@ export class UndiciInstrumentation extends InstrumentationBase { // Execute the request hook if defined safeExecuteInTheMiddle( () => config.requestHook?.(span, request), - (e) => e && this._diag.error('caught requestHook error: ', e), - true, + e => e && this._diag.error('caught requestHook error: ', e), + true ); // Context propagation goes last so no hook can tamper @@ -240,7 +265,7 @@ export class UndiciInstrumentation extends InstrumentationBase { request.headers += Object.entries(addedHeaders) .map(([k, v]) => `${k}: ${v}\r\n`) .join(''); - this._recordFromReq.set(request, {span, attributes, startTime}); + this._recordFromReq.set(request, { span, attributes, startTime }); } // This is the 2nd message we recevie for each request. It is fired when connection with @@ -250,7 +275,7 @@ export class UndiciInstrumentation extends InstrumentationBase { const record = this._recordFromReq.get(request as UndiciRequest); if (!record) { - return + return; } const config = this._getConfig(); @@ -264,18 +289,20 @@ export class UndiciInstrumentation extends InstrumentationBase { // After hooks have been processed (which may modify request headers) // we can collect the headers based on the configuration const rawHeaders = request.headers.split('\r\n'); - const reqHeaders = new Map(rawHeaders.map(h => { - const sepIndex = h.indexOf(':'); - const name = h.substring(0, sepIndex).toLowerCase(); - const val = h.substring(sepIndex + 1).trim(); - return [name, val]; - })); + const reqHeaders = new Map( + rawHeaders.map(h => { + const sepIndex = h.indexOf(':'); + const name = h.substring(0, sepIndex).toLowerCase(); + const val = h.substring(sepIndex + 1).trim(); + return [name, val]; + }) + ); if (config.headersToSpanAttributes?.requestHeaders) { config.headersToSpanAttributes.requestHeaders - .map((name) => name.toLowerCase()) - .filter((name) => reqHeaders.has(name)) - .forEach((name) => { + .map(name => name.toLowerCase()) + .filter(name => reqHeaders.has(name)) + .forEach(name => { spanAttributes[`http.request.header.${name}`] = reqHeaders.get(name); }); } @@ -286,14 +313,17 @@ export class UndiciInstrumentation extends InstrumentationBase { // This is the 3rd message we get for each request and it's fired when the server // headers are received, body may not be accessible yet. // From the response headers we can set the status and content length - private onResponseHeaders({ request, response }: ResponseHeadersMessage): void { + private onResponseHeaders({ + request, + response, + }: ResponseHeadersMessage): void { const record = this._recordFromReq.get(request); if (!record) { return; } - const {span, attributes, startTime} = record; + const { span, attributes, startTime } = record; // We are currently *not* capturing response headers, even though the // intake API does allow it, because none of the other `setHttpContext` // uses currently do @@ -306,7 +336,7 @@ export class UndiciInstrumentation extends InstrumentationBase { for (let idx = 0; idx < response.headers.length; idx = idx + 2) { resHeaders.set( response.headers[idx].toString().toLowerCase(), - response.headers[idx + 1].toString(), + response.headers[idx + 1].toString() ); } @@ -314,9 +344,9 @@ export class UndiciInstrumentation extends InstrumentationBase { const config = this._getConfig(); if (config.headersToSpanAttributes?.responseHeaders) { config.headersToSpanAttributes.responseHeaders - .map((name) => name.toLowerCase()) - .filter((name) => resHeaders.has(name)) - .forEach((name) => { + .map(name => name.toLowerCase()) + .filter(name => resHeaders.has(name)) + .forEach(name => { spanAttributes[`http.response.header.${name}`] = resHeaders.get(name); }); } @@ -331,15 +361,18 @@ export class UndiciInstrumentation extends InstrumentationBase { span.setAttributes(spanAttributes); span.setStatus({ - code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET, + code: + response.statusCode >= 400 + ? SpanStatusCode.ERROR + : SpanStatusCode.UNSET, + }); + this._recordFromReq.set(request, { + span, + startTime, + attributes: Object.assign(attributes, spanAttributes), }); - this._recordFromReq.set( - request, - {span, startTime, attributes: Object.assign(attributes, spanAttributes)} - ); } - // This is the last event we receive if the request went without any errors private onDone({ request }: RequestMessage): void { const record = this._recordFromReq.get(request); @@ -348,8 +381,7 @@ export class UndiciInstrumentation extends InstrumentationBase { return; } - - const {span, attributes, startTime} = record; + const { span, attributes, startTime } = record; // End the span span.end(); this._recordFromReq.delete(request); @@ -371,7 +403,7 @@ export class UndiciInstrumentation extends InstrumentationBase { return; } - const {span, attributes, startTime} = record; + const { span, attributes, startTime } = record; // NOTE: in `undici@6.3.0` when request aborted the error type changes from // a custom error (`RequestAbortedError`) to a built-in `DOMException` carrying @@ -404,7 +436,7 @@ export class UndiciInstrumentation extends InstrumentationBase { SemanticAttributes.URL_SCHEME, SemanticAttributes.ERROR_TYPE, ]; - keysToCopy.forEach((key) => { + keysToCopy.forEach(key => { if (key in attributes) { metricsAttributes[key] = attributes[key]; } diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts index d95a22a97aa..b78e00ea0eb 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/fetch.test.ts @@ -15,7 +15,13 @@ */ import * as assert from 'assert'; -import { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'; +import { + SpanKind, + SpanStatusCode, + context, + propagation, + trace, +} from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { InMemorySpanExporter, @@ -50,7 +56,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { if (typeof globalThis.fetch !== 'function') { this.skip(); } - + propagation.setGlobalPropagator(new MockPropagation()); context.setGlobalContextManager(new AsyncHooksContextManager().enable()); mockServer.start(done); @@ -62,11 +68,11 @@ describe('UndiciInstrumentation `fetch` tests', function () { try { assert.ok( req.headers[MockPropagation.TRACE_CONTEXT_KEY], - `trace propagation for ${MockPropagation.TRACE_CONTEXT_KEY} works`, + `trace propagation for ${MockPropagation.TRACE_CONTEXT_KEY} works` ); assert.ok( req.headers[MockPropagation.SPAN_CONTEXT_KEY], - `trace propagation for ${MockPropagation.SPAN_CONTEXT_KEY} works`, + `trace propagation for ${MockPropagation.SPAN_CONTEXT_KEY} works` ); } catch (assertErr) { // The exception will hang the server and the test so we set a header @@ -83,7 +89,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); }); - after(function(done) { + after(function (done) { context.disable(); propagation.disable(); mockServer.mockListener(undefined); @@ -142,7 +148,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { startSpanHook: () => { throw new Error('startSpanHook error'); }, - }) + }); const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const response = await fetch(fetchUrl); @@ -161,7 +167,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { httpStatusCode: response.status, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', resHeaders: response.headers, }); }); @@ -187,7 +193,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { httpStatusCode: response.status, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', resHeaders: response.headers, }); }); @@ -199,14 +205,14 @@ describe('UndiciInstrumentation `fetch` tests', function () { // Set configuration instrumentation.setConfig({ enabled: true, - ignoreRequestHook: (req) => { + ignoreRequestHook: req => { return req.path.indexOf('/ignore/path') !== -1; }, requestHook: (span, req) => { // TODO: maybe an intermediate request with better API req.headers += 'x-requested-with: undici\r\n'; }, - startSpanHook: (request) => { + startSpanHook: request => { return { 'test.hook.attribute': 'hook-value', }; @@ -214,15 +220,17 @@ describe('UndiciInstrumentation `fetch` tests', function () { headersToSpanAttributes: { requestHeaders: ['foo-client', 'x-requested-with'], responseHeaders: ['foo-server'], - } + }, }); // Do some requests - const ignoreResponse = await fetch(`${protocol}://${hostname}:${mockServer.port}/ignore/path`); + const ignoreResponse = await fetch( + `${protocol}://${hostname}:${mockServer.port}/ignore/path` + ); const reqInit = { headers: new Headers({ 'user-agent': 'custom', - 'foo-client': 'bar' + 'foo-client': 'bar', }), }; assert.ok( @@ -230,7 +238,10 @@ describe('UndiciInstrumentation `fetch` tests', function () { 'propagation is not set for ignored requests' ); - const queryResponse = await fetch(`${protocol}://${hostname}:${mockServer.port}/?query=test`, reqInit); + const queryResponse = await fetch( + `${protocol}://${hostname}:${mockServer.port}/?query=test`, + reqInit + ); assert.ok( queryResponse.headers.get('propagation-error') == null, 'propagation is set for instrumented requests' @@ -245,29 +256,29 @@ describe('UndiciInstrumentation `fetch` tests', function () { httpStatusCode: queryResponse.status, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', reqHeaders: reqInit.headers, resHeaders: queryResponse.headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], 'bar', - 'request headers from fetch options are captured', + 'request headers from fetch options are captured' ); assert.strictEqual( span.attributes['http.request.header.x-requested-with'], 'undici', - 'request headers from requestHook are captured', + 'request headers from requestHook are captured' ); assert.strictEqual( span.attributes['http.response.header.foo-server'], 'bar', - 'response headers from the server are captured', + 'response headers from the server are captured' ); assert.strictEqual( span.attributes['test.hook.attribute'], 'hook-value', - 'startSpanHook is called', + 'startSpanHook is called' ); }); @@ -336,14 +347,13 @@ describe('UndiciInstrumentation `fetch` tests', function () { }); }); - it('should capture errors using fetch API', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); let fetchError; try { - const fetchUrl = `http://unexistent-host-name/path`; + const fetchUrl = 'http://unexistent-host-name/path'; await fetch(fetchUrl); } catch (err) { // Expected error @@ -362,8 +372,8 @@ describe('UndiciInstrumentation `fetch` tests', function () { noNetPeer: true, // do not check network attribs forceStatus: { code: SpanStatusCode.ERROR, - message: 'getaddrinfo ENOTFOUND unexistent-host-name' - } + message: 'getaddrinfo ENOTFOUND unexistent-host-name', + }, }); }); @@ -384,7 +394,7 @@ describe('UndiciInstrumentation `fetch` tests', function () { } // Let the error be published to diagnostics channel - await new Promise((r) => setTimeout(r,5)); + await new Promise(r => setTimeout(r, 5)); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; @@ -394,13 +404,13 @@ describe('UndiciInstrumentation `fetch` tests', function () { hostname: 'localhost', httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', error: fetchError, noNetPeer: true, // do not check network attribs forceStatus: { code: SpanStatusCode.ERROR, - message: 'The operation was aborted.' - } + message: 'The operation was aborted.', + }, }); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts index c6703754abe..97a4989ff7a 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/metrics.test.ts @@ -51,7 +51,6 @@ instrumentation.setTracerProvider(provider); instrumentation.setMeterProvider(meterProvider); describe('UndiciInstrumentation metrics tests', function () { - before(function (done) { // Do not test if the `fetch` global API is not available // This applies to nodejs < v18 or nodejs < v16.15 wihtout the flag @@ -60,7 +59,7 @@ describe('UndiciInstrumentation metrics tests', function () { if (typeof globalThis.fetch !== 'function') { this.skip(); } - + context.setGlobalContextManager(new AsyncHooksContextManager().enable()); mockServer.start(done); mockServer.mockListener((req, res) => { @@ -75,7 +74,7 @@ describe('UndiciInstrumentation metrics tests', function () { instrumentation.enable(); }); - after(function(done) { + after(function (done) { instrumentation.disable(); context.disable(); propagation.disable(); @@ -103,15 +102,18 @@ describe('UndiciInstrumentation metrics tests', function () { it('should report "http.client.request.duration" metric', async () => { const fetchUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; await fetch(fetchUrl); - + await metricReader.collectAndExport(); const resourceMetrics = metricsMemoryExporter.getMetrics(); const scopeMetrics = resourceMetrics[0].scopeMetrics; const metrics = scopeMetrics[0].metrics; - + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); assert.strictEqual(metrics.length, 1, 'metrics count'); - assert.strictEqual(metrics[0].descriptor.name, 'http.client.request.duration'); + assert.strictEqual( + metrics[0].descriptor.name, + 'http.client.request.duration' + ); assert.strictEqual( metrics[0].descriptor.description, 'Measures the duration of outbound HTTP requests.' @@ -127,19 +129,19 @@ describe('UndiciInstrumentation metrics tests', function () { ); assert.strictEqual( metricAttributes[SemanticAttributes.HTTP_REQUEST_METHOD], - 'GET', + 'GET' ); assert.strictEqual( metricAttributes[SemanticAttributes.SERVER_ADDRESS], - 'localhost', + 'localhost' ); assert.strictEqual( metricAttributes[SemanticAttributes.SERVER_PORT], - mockServer.port, + mockServer.port ); assert.strictEqual( metricAttributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE], - 200, + 200 ); }); @@ -151,7 +153,7 @@ describe('UndiciInstrumentation metrics tests', function () { } catch (err) { // Expected error, do nothing } - + await metricReader.collectAndExport(); const resourceMetrics = metricsMemoryExporter.getMetrics(); const scopeMetrics = resourceMetrics[0].scopeMetrics; @@ -159,7 +161,10 @@ describe('UndiciInstrumentation metrics tests', function () { assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); assert.strictEqual(metrics.length, 1, 'metrics count'); - assert.strictEqual(metrics[0].descriptor.name, 'http.client.request.duration'); + assert.strictEqual( + metrics[0].descriptor.name, + 'http.client.request.duration' + ); assert.strictEqual( metrics[0].descriptor.description, 'Measures the duration of outbound HTTP requests.' @@ -175,20 +180,17 @@ describe('UndiciInstrumentation metrics tests', function () { ); assert.strictEqual( metricAttributes[SemanticAttributes.HTTP_REQUEST_METHOD], - 'GET', + 'GET' ); assert.strictEqual( metricAttributes[SemanticAttributes.SERVER_ADDRESS], - 'unknownhost', - ); - assert.strictEqual( - metricAttributes[SemanticAttributes.SERVER_PORT], - 80, + 'unknownhost' ); + assert.strictEqual(metricAttributes[SemanticAttributes.SERVER_PORT], 80); assert.ok( metricAttributes[SemanticAttributes.ERROR_TYPE], - `the metric contains "${SemanticAttributes.ERROR_TYPE}" attribute if request failed`, + `the metric contains "${SemanticAttributes.ERROR_TYPE}" attribute if request failed` ); }); }); -}); \ No newline at end of file +}); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts index 4f0d3252664..3f2332924fd 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts @@ -16,7 +16,13 @@ import * as assert from 'assert'; import { Writable } from 'stream'; -import { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api'; +import { + SpanKind, + SpanStatusCode, + context, + propagation, + trace, +} from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { InMemorySpanExporter, @@ -45,11 +51,10 @@ const provider = new NodeTracerProvider(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); instrumentation.setTracerProvider(provider); - // Undici docs (https://github.com/nodejs/undici#garbage-collection) suggest // that an undici response body should always be consumed. -async function consumeResponseBody(body: Dispatcher.ResponseData["body"]) { - return new Promise((resolve) => { +async function consumeResponseBody(body: Dispatcher.ResponseData['body']) { + return new Promise(resolve => { const devNull = new Writable({ write(_chunk, _encoding, cb) { setImmediate(cb); @@ -73,11 +78,11 @@ describe('UndiciInstrumentation `undici` tests', function () { try { assert.ok( req.headers[MockPropagation.TRACE_CONTEXT_KEY], - `trace propagation for ${MockPropagation.TRACE_CONTEXT_KEY} works`, + `trace propagation for ${MockPropagation.TRACE_CONTEXT_KEY} works` ); assert.ok( req.headers[MockPropagation.SPAN_CONTEXT_KEY], - `trace propagation for ${MockPropagation.SPAN_CONTEXT_KEY} works`, + `trace propagation for ${MockPropagation.SPAN_CONTEXT_KEY} works` ); } catch (assertErr) { // The exception will hang the server and the test so we set a header @@ -94,7 +99,7 @@ describe('UndiciInstrumentation `undici` tests', function () { }); }); - after(function(done) { + after(function (done) { context.disable(); propagation.disable(); mockServer.mockListener(undefined); @@ -133,14 +138,14 @@ describe('UndiciInstrumentation `undici` tests', function () { // Set configuration instrumentation.setConfig({ enabled: true, - ignoreRequestHook: (req) => { + ignoreRequestHook: req => { return req.path.indexOf('/ignore/path') !== -1; }, requestHook: (span, req) => { // TODO: maybe an intermediate request with better API req.headers += 'x-requested-with: undici\r\n'; }, - startSpanHook: (request) => { + startSpanHook: request => { return { 'test.hook.attribute': 'hook-value', }; @@ -148,7 +153,7 @@ describe('UndiciInstrumentation `undici` tests', function () { headersToSpanAttributes: { requestHeaders: ['foo-client', 'x-requested-with'], responseHeaders: ['foo-server'], - } + }, }); }); afterEach(function () { @@ -163,11 +168,13 @@ describe('UndiciInstrumentation `undici` tests', function () { // Do some requests const headers = { 'user-agent': 'custom', - 'foo-client': 'bar' + 'foo-client': 'bar', }; const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; - const ignoreResponse = await undici.request(ignoreRequestUrl, { headers }); + const ignoreResponse = await undici.request(ignoreRequestUrl, { + headers, + }); await consumeResponseBody(ignoreResponse.body); assert.ok( @@ -176,10 +183,7 @@ describe('UndiciInstrumentation `undici` tests', function () { ); spans = memoryExporter.getFinishedSpans(); - assert.ok( - spans.length === 0, - 'ignoreRequestHook is filtering requests' - ); + assert.ok(spans.length === 0, 'ignoreRequestHook is filtering requests'); }); it('should create valid spans for "request" method', async function () { @@ -189,11 +193,13 @@ describe('UndiciInstrumentation `undici` tests', function () { // Do some requests const headers = { 'user-agent': 'custom', - 'foo-client': 'bar' + 'foo-client': 'bar', }; const ignoreRequestUrl = `${protocol}://${hostname}:${mockServer.port}/ignore/path`; - const ignoreResponse = await undici.request(ignoreRequestUrl, { headers }); + const ignoreResponse = await undici.request(ignoreRequestUrl, { + headers, + }); await consumeResponseBody(ignoreResponse.body); assert.ok( @@ -219,29 +225,29 @@ describe('UndiciInstrumentation `undici` tests', function () { httpStatusCode: queryResponse.statusCode, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', reqHeaders: headers, resHeaders: queryResponse.headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], 'bar', - 'request headers from fetch options are captured', + 'request headers from fetch options are captured' ); assert.strictEqual( span.attributes['http.request.header.x-requested-with'], 'undici', - 'request headers from requestHook are captured', + 'request headers from requestHook are captured' ); assert.strictEqual( span.attributes['http.response.header.foo-server'], 'bar', - 'response headers from the server are captured', + 'response headers from the server are captured' ); assert.strictEqual( span.attributes['test.hook.attribute'], 'hook-value', - 'startSpanHook is called', + 'startSpanHook is called' ); }); @@ -252,7 +258,7 @@ describe('UndiciInstrumentation `undici` tests', function () { // Do some requests const headers = { 'user-agent': 'custom', - 'foo-client': 'bar' + 'foo-client': 'bar', }; const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const queryResponse = await undici.fetch(queryRequestUrl, { headers }); @@ -272,29 +278,29 @@ describe('UndiciInstrumentation `undici` tests', function () { httpStatusCode: queryResponse.status, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', reqHeaders: headers, resHeaders: queryResponse.headers as Headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], 'bar', - 'request headers from fetch options are captured', + 'request headers from fetch options are captured' ); assert.strictEqual( span.attributes['http.request.header.x-requested-with'], 'undici', - 'request headers from requestHook are captured', + 'request headers from requestHook are captured' ); assert.strictEqual( span.attributes['http.response.header.foo-server'], 'bar', - 'response headers from the server are captured', + 'response headers from the server are captured' ); assert.strictEqual( span.attributes['test.hook.attribute'], 'hook-value', - 'startSpanHook is called', + 'startSpanHook is called' ); }); @@ -305,7 +311,7 @@ describe('UndiciInstrumentation `undici` tests', function () { // Do some requests const headers = { 'user-agent': 'custom', - 'foo-client': 'bar' + 'foo-client': 'bar', }; // https://undici.nodejs.org/#/docs/api/Dispatcher?id=example-1-basic-get-stream-request const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; @@ -318,10 +324,10 @@ describe('UndiciInstrumentation `undici` tests', function () { queryResponse.statusCode = statusCode; queryResponse.headers = headers; return new Writable({ - write (chunk, encoding, callback) { - (opaque as any).bufs.push(chunk) - callback() - } + write(chunk, encoding, callback) { + (opaque as any).bufs.push(chunk); + callback(); + }, }); } ); @@ -340,29 +346,29 @@ describe('UndiciInstrumentation `undici` tests', function () { httpStatusCode: queryResponse.statusCode, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', reqHeaders: headers, resHeaders: queryResponse.headers as Headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], 'bar', - 'request headers from fetch options are captured', + 'request headers from fetch options are captured' ); assert.strictEqual( span.attributes['http.request.header.x-requested-with'], 'undici', - 'request headers from requestHook are captured', + 'request headers from requestHook are captured' ); assert.strictEqual( span.attributes['http.response.header.foo-server'], 'bar', - 'response headers from the server are captured', + 'response headers from the server are captured' ); assert.strictEqual( span.attributes['test.hook.attribute'], 'hook-value', - 'startSpanHook is called', + 'startSpanHook is called' ); }); @@ -373,7 +379,7 @@ describe('UndiciInstrumentation `undici` tests', function () { // Do some requests const headers = { 'user-agent': 'custom', - 'foo-client': 'bar' + 'foo-client': 'bar', }; const queryRequestUrl = `${protocol}://${hostname}:${mockServer.port}`; @@ -416,29 +422,29 @@ describe('UndiciInstrumentation `undici` tests', function () { httpStatusCode: queryResponse.statusCode, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', reqHeaders: headers, resHeaders: queryResponse.headers as Headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], 'bar', - 'request headers from fetch options are captured', + 'request headers from fetch options are captured' ); assert.strictEqual( span.attributes['http.request.header.x-requested-with'], 'undici', - 'request headers from requestHook are captured', + 'request headers from requestHook are captured' ); assert.strictEqual( span.attributes['http.response.header.foo-server'], 'bar', - 'response headers from the server are captured', + 'response headers from the server are captured' ); assert.strictEqual( span.attributes['test.hook.attribute'], 'hook-value', - 'startSpanHook is called', + 'startSpanHook is called' ); }); @@ -461,7 +467,7 @@ describe('UndiciInstrumentation `undici` tests', function () { startSpanHook: () => { throw new Error('startSpanHook error'); }, - }) + }); const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; const { headers, statusCode, body } = await undici.request(requestUrl); @@ -482,7 +488,7 @@ describe('UndiciInstrumentation `undici` tests', function () { httpStatusCode: statusCode, httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', resHeaders: headers, }); }); @@ -555,7 +561,6 @@ describe('UndiciInstrumentation `undici` tests', function () { }); }); - it('should capture errors while doing request', async function () { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); @@ -581,8 +586,8 @@ describe('UndiciInstrumentation `undici` tests', function () { noNetPeer: true, // do not check network attribs forceStatus: { code: SpanStatusCode.ERROR, - message: 'getaddrinfo ENOTFOUND unexistent-host-name' - } + message: 'getaddrinfo ENOTFOUND unexistent-host-name', + }, }); }); @@ -593,7 +598,9 @@ describe('UndiciInstrumentation `undici` tests', function () { let requestError; const controller = new AbortController(); const requestUrl = `${protocol}://${hostname}:${mockServer.port}/?query=test`; - const requestPromise = undici.request(requestUrl, { signal: controller.signal }); + const requestPromise = undici.request(requestUrl, { + signal: controller.signal, + }); controller.abort(); try { await requestPromise; @@ -603,7 +610,7 @@ describe('UndiciInstrumentation `undici` tests', function () { } // Let the error be published to diagnostics channel - await new Promise((r) => setTimeout(r,5)); + await new Promise(r => setTimeout(r, 5)); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; @@ -613,13 +620,13 @@ describe('UndiciInstrumentation `undici` tests', function () { hostname: 'localhost', httpMethod: 'GET', path: '/', - query:'?query=test', + query: '?query=test', error: requestError, noNetPeer: true, // do not check network attribs forceStatus: { code: SpanStatusCode.ERROR, - message: requestError.message - } + message: requestError.message, + }, }); }); }); diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts index afbdd87b19d..2d7413931e0 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/assertSpan.ts @@ -44,7 +44,11 @@ export const assertSpan = ( assert.strictEqual(span.spanContext().traceId.length, 32); assert.strictEqual(span.spanContext().spanId.length, 16); assert.strictEqual(span.kind, SpanKind.CLIENT, 'span.kind is correct'); - assert.strictEqual(span.name, `HTTP ${validations.httpMethod}`, 'span.name is correct'); + assert.strictEqual( + span.name, + `HTTP ${validations.httpMethod}`, + 'span.name is correct' + ); // TODO: check this // assert.strictEqual( // span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], @@ -54,14 +58,14 @@ export const assertSpan = ( assert.strictEqual( span.attributes[SemanticAttributes.HTTP_REQUEST_METHOD], validations.httpMethod, - `attributes['${SemanticAttributes.HTTP_REQUEST_METHOD}'] is correct`, + `attributes['${SemanticAttributes.HTTP_REQUEST_METHOD}'] is correct` ); if (validations.path) { assert.strictEqual( span.attributes[SemanticAttributes.URL_PATH], validations.path, - `attributes['${SemanticAttributes.URL_PATH}'] is correct`, + `attributes['${SemanticAttributes.URL_PATH}'] is correct` ); } @@ -69,36 +73,43 @@ export const assertSpan = ( assert.strictEqual( span.attributes[SemanticAttributes.URL_QUERY], validations.query, - `attributes['${SemanticAttributes.URL_QUERY}'] is correct`, + `attributes['${SemanticAttributes.URL_QUERY}'] is correct` ); } - + assert.strictEqual( span.attributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE], validations.httpStatusCode, - `attributes['${SemanticAttributes.HTTP_RESPONSE_STATUS_CODE}'] is correct ${span.attributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE]}`, + `attributes['${SemanticAttributes.HTTP_RESPONSE_STATUS_CODE}'] is correct ${ + span.attributes[SemanticAttributes.HTTP_RESPONSE_STATUS_CODE] + }` ); assert.strictEqual(span.links.length, 0, 'there are no links'); if (validations.error) { assert.strictEqual(span.events.length, 1, 'span contains one error event'); - assert.strictEqual(span.events[0].name, 'exception', 'error event name is correct'); + assert.strictEqual( + span.events[0].name, + 'exception', + 'error event name is correct' + ); const eventAttributes = span.events[0].attributes; assert.ok(eventAttributes != null, 'event has attributes'); - assert.deepStrictEqual(Object.keys(eventAttributes), [ - 'exception.type', - 'exception.message', - 'exception.stacktrace', - ], 'the event attribute names are correct'); + assert.deepStrictEqual( + Object.keys(eventAttributes), + ['exception.type', 'exception.message', 'exception.stacktrace'], + 'the event attribute names are correct' + ); } else { assert.strictEqual(span.events.length, 0, 'span contains no events'); } const { httpStatusCode } = validations; - const isStatusUnset = httpStatusCode && httpStatusCode >= 100 && httpStatusCode < 400; - + const isStatusUnset = + httpStatusCode && httpStatusCode >= 100 && httpStatusCode < 400; + assert.deepStrictEqual( span.status, validations.forceStatus || { @@ -108,23 +119,27 @@ export const assertSpan = ( ); assert.ok(span.endTime, 'must be finished'); - assert.ok(hrTimeToNanoseconds(span.duration) > 0, 'must have positive duration'); + assert.ok( + hrTimeToNanoseconds(span.duration) > 0, + 'must have positive duration' + ); if (validations.resHeaders) { - const contentLengthHeader = validations.resHeaders instanceof Headers ? - validations.resHeaders.get('content-length') : - validations.resHeaders['content-length']; - + const contentLengthHeader = + validations.resHeaders instanceof Headers + ? validations.resHeaders.get('content-length') + : validations.resHeaders['content-length']; + if (contentLengthHeader) { const contentLength = Number(contentLengthHeader); - + assert.strictEqual( span.attributes['http.response.header.content-length'], contentLength ); } } - + assert.strictEqual( span.attributes[SemanticAttributes.SERVER_ADDRESS], validations.hostname, @@ -147,11 +162,11 @@ export const assertSpan = ( `${SemanticAttributes.URL_FULL} & ${SemanticAttributes.SERVER_ADDRESS} must be consistent` ); - if (validations.reqHeaders) { - const userAgent = validations.reqHeaders instanceof Headers ? - validations.reqHeaders.get('user-agent') : - validations.reqHeaders['user-agent']; + const userAgent = + validations.reqHeaders instanceof Headers + ? validations.reqHeaders.get('user-agent') + : validations.reqHeaders['user-agent']; if (userAgent) { assert.strictEqual( span.attributes[SemanticAttributes.USER_AGENT_ORIGINAL], diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts index e9cbe9d80b2..5c49e661d62 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-propagation.ts @@ -37,7 +37,7 @@ export class MockPropagation implements TextMapPropagator { } inject(context: Context, carrier: Record): void { const spanContext = trace.getSpanContext(context); - + if (spanContext) { carrier[MockPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId; carrier[MockPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts index db47cea2904..5fa464df033 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/utils/mock-server.ts @@ -15,7 +15,6 @@ */ import * as http from 'http'; - export class MockServer { private _port: number | undefined; private _httpServer: http.Server | undefined; From 7d69f4a4fa47787481ba651272cef7a1e8ec735c Mon Sep 17 00:00:00 2001 From: David Luna Date: Tue, 20 Feb 2024 01:10:37 +0100 Subject: [PATCH 27/28] chore(instrumentation-undici): fix compile error --- .../test/undici.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts index 3f2332924fd..14404ab7405 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-undici/test/undici.test.ts @@ -280,7 +280,7 @@ describe('UndiciInstrumentation `undici` tests', function () { path: '/', query: '?query=test', reqHeaders: headers, - resHeaders: queryResponse.headers as Headers, + resHeaders: queryResponse.headers as unknown as Headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], @@ -348,7 +348,7 @@ describe('UndiciInstrumentation `undici` tests', function () { path: '/', query: '?query=test', reqHeaders: headers, - resHeaders: queryResponse.headers as Headers, + resHeaders: queryResponse.headers as unknown as Headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], @@ -424,7 +424,7 @@ describe('UndiciInstrumentation `undici` tests', function () { path: '/', query: '?query=test', reqHeaders: headers, - resHeaders: queryResponse.headers as Headers, + resHeaders: queryResponse.headers as unknown as Headers, }); assert.strictEqual( span.attributes['http.request.header.foo-client'], From db97899902f79c7728e9869898d98e2fcbf6ca1c Mon Sep 17 00:00:00 2001 From: David Luna Date: Tue, 20 Feb 2024 11:44:14 +0100 Subject: [PATCH 28/28] chore(instrumentation-undici): add example --- examples/undici/README.md | 81 +++++++++++++++++++ examples/undici/client.js | 24 ++++++ examples/undici/docker-compose.yml | 21 +++++ examples/undici/package.json | 48 +++++++++++ examples/undici/server.js | 45 +++++++++++ examples/undici/tracer.js | 42 ++++++++++ .../README.md | 4 +- 7 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 examples/undici/README.md create mode 100644 examples/undici/client.js create mode 100644 examples/undici/docker-compose.yml create mode 100644 examples/undici/package.json create mode 100644 examples/undici/server.js create mode 100644 examples/undici/tracer.js diff --git a/examples/undici/README.md b/examples/undici/README.md new file mode 100644 index 00000000000..7b256f4c1b7 --- /dev/null +++ b/examples/undici/README.md @@ -0,0 +1,81 @@ +# Overview + +OpenTelemetry Undici Instrumentation allows the user to automatically collect trace data and export them to the backend of choice (we can use Zipkin or Jaeger for this example), to give observability to distributed systems. + +This is a simple example that demonstrates tracing HTTP request from client to server. The example +shows key aspects of tracing such as + +- Root Span (on Client) +- Child Span (on Client) +- Child Span from a Remote Parent (on Server) +- SpanContext Propagation (from Client to Server) +- Span Events +- Span Attributes + +## Installation + +```sh +# from this directory +npm install +``` + +Setup [Zipkin Tracing](https://zipkin.io/pages/quickstart.html) +or +Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one) + +## Run the Application + +### Zipkin + +- Run the server + + ```sh + # from this directory + npm run zipkin:server + ``` + +- Run the client + + ```sh + # from this directory + npm run zipkin:client + ``` + +#### Zipkin UI + +`zipkin:server` script should output the `traceid` in the terminal (e.g `traceid: 4815c3d576d930189725f1f1d1bdfcc6`). +Go to Zipkin with your browser (e.g ) + +

+ +### Jaeger + +- Run the server + + ```sh + # from this directory + npm run jaeger:server + ``` + +- Run the client + + ```sh + # from this directory + npm run jaeger:client + ``` + +#### Jaeger UI + +`jaeger:server` script should output the `traceid` in the terminal (e.g `traceid: 4815c3d576d930189725f1f1d1bdfcc6`). +Go to Jaeger with your browser (e.g ) + +

+ +## Useful links + +- For more information on OpenTelemetry, visit: +- For more information on OpenTelemetry for Node.js, visit: + +## LICENSE + +Apache License 2.0 diff --git a/examples/undici/client.js b/examples/undici/client.js new file mode 100644 index 00000000000..ba079031588 --- /dev/null +++ b/examples/undici/client.js @@ -0,0 +1,24 @@ +'use strict'; + +const undici = require('undici'); +const tracer = require('./tracer')('example-undici-client'); + +/** A function which makes requests and handles response. */ +async function makeRequests(type) { + tracer.startActiveSpan('makeRequests with global fetch', async (span) => { + const fetchResponse = await fetch('localhost:8080/helloworld'); + console.log('response with global fetch: ' + await fetchResponse.text()); + + const undiciResponse = await undici.fetch('localhost:8080/helloworld'); + console.log('response with undici fetch: ' + await undiciResponse.text()); + span.end(); + }); + + // The process must live for at least the interval past any traces that + // must be exported, or some risk being lost if they are recorded after the + // last export. + console.log('Sleeping 5 seconds before shutdown to ensure all records are flushed.'); + setTimeout(() => { console.log('Completed.'); }, 5000); +} + +makeRequests(); diff --git a/examples/undici/docker-compose.yml b/examples/undici/docker-compose.yml new file mode 100644 index 00000000000..87f10a0dec6 --- /dev/null +++ b/examples/undici/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.7' + +services: + jaeger: + image: jaegertracing/all-in-one + ports: + - "16686:16686" + - "4318:4318" + environment: + - LOG_LEVEL=debug + networks: + - undici-example + zipkin: + image: openzipkin/zipkin + container_name: zipkin + ports: + # Port used for the Zipkin UI and HTTP Api + - 9411:9411 + +networks: + undici-example: \ No newline at end of file diff --git a/examples/undici/package.json b/examples/undici/package.json new file mode 100644 index 00000000000..21d1c8f7984 --- /dev/null +++ b/examples/undici/package.json @@ -0,0 +1,48 @@ +{ + "name": "undici-example", + "private": true, + "version": "0.46.0", + "description": "Example of Undici integration with OpenTelemetry", + "main": "index.js", + "scripts": { + "docker:start": "docker compose -f .docker-compose.yml up -d", + "docker:stop": "docker compose -f ./docker-compose.yml down", + "zipkin:server": "cross-env EXPORTER=zipkin node ./server.js", + "zipkin:client": "cross-env EXPORTER=zipkin node ./client.js", + "jaeger:server": "cross-env EXPORTER=jaeger node ./server.js", + "jaeger:client": "cross-env EXPORTER=jaeger node ./client.js" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" + }, + "keywords": [ + "opentelemetry", + "undici", + "fetch", + "tracing" + ], + "engines": { + "node": ">=14" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "dependencies": { + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/exporter-zipkin": "1.19.0", + "@opentelemetry/instrumentation": "0.46.0", + "@opentelemetry/instrumentation-undici": "0.46.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.46.0", + "@opentelemetry/resources": "1.19.0", + "@opentelemetry/sdk-trace-base": "1.19.0", + "@opentelemetry/sdk-trace-node": "1.19.0", + "@opentelemetry/semantic-conventions": "1.19.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/undici", + "devDependencies": { + "cross-env": "^6.0.0" + } +} diff --git a/examples/undici/server.js b/examples/undici/server.js new file mode 100644 index 00000000000..62b117e7112 --- /dev/null +++ b/examples/undici/server.js @@ -0,0 +1,45 @@ +'use strict'; + +const api = require('@opentelemetry/api'); +const tracer = require('./tracer')('example-undici-server'); +const http = require('http'); + +/** Starts a HTTP server that receives requests on sample server port. */ +function startServer(port) { + // Creates a server + const server = http.createServer(handleRequest); + // Starts the server + server.listen(port, (err) => { + if (err) { + throw err; + } + console.log(`Node HTTP listening on ${port}`); + }); +} + +/** A function which handles requests and send response. */ +function handleRequest(request, response) { + const currentSpan = api.trace.getActiveSpan(); + // display traceid in the terminal + const traceId = currentSpan.spanContext().traceId; + console.log(`traceId: ${traceId}`); + const span = tracer.startSpan('handleRequest', { + kind: 1, // server + attributes: { key: 'value' }, + }); + // Annotate our span to capture metadata about the operation + span.addEvent('invoking handleRequest'); + + const body = []; + request.on('error', (err) => console.log(err)); + request.on('data', (chunk) => body.push(chunk)); + request.on('end', () => { + // deliberately sleeping to mock some action. + setTimeout(() => { + span.end(); + response.end('Hello World!'); + }, 2000); + }); +} + +startServer(8080); diff --git a/examples/undici/tracer.js b/examples/undici/tracer.js new file mode 100644 index 00000000000..7def3bed83a --- /dev/null +++ b/examples/undici/tracer.js @@ -0,0 +1,42 @@ +'use strict'; + +const opentelemetry = require('@opentelemetry/api'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { Resource } = require('@opentelemetry/resources'); +const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); +const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); +const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-proto'); +const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); +const { UndiciInstrumentation } = require('@opentelemetry/instrumentation-undici'); + +const EXPORTER = process.env.EXPORTER || ''; + +module.exports = (serviceName) => { + const provider = new NodeTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: serviceName, + }), + }); + + let exporter; + if (EXPORTER.toLowerCase().startsWith('z')) { + exporter = new ZipkinExporter(); + } else { + exporter = new OTLPTraceExporter(); + } + + provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + + // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings + provider.register(); + + registerInstrumentations({ + // // when boostraping with lerna for testing purposes + instrumentations: [ + new UndiciInstrumentation(), + ], + }); + + return opentelemetry.trace.getTracer('undici-example'); +}; diff --git a/experimental/packages/opentelemetry-instrumentation-undici/README.md b/experimental/packages/opentelemetry-instrumentation-undici/README.md index f3fb8a8d203..f507fc11577 100644 --- a/experimental/packages/opentelemetry-instrumentation-undici/README.md +++ b/experimental/packages/opentelemetry-instrumentation-undici/README.md @@ -15,7 +15,7 @@ npm install --save @opentelemetry/instrumentation-undici ## Usage -OpenTelemetry HTTP Instrumentation allows the user to automatically collect trace data and export them to their backend of choice, to give observability to distributed systems. +OpenTelemetry Undici/fetch Instrumentation allows the user to automatically collect trace data and export them to their backend of choice, to give observability to distributed systems. To load a specific instrumentation (Undici in this case), specify it in the Node Tracer's configuration. @@ -42,7 +42,7 @@ registerInstrumentations({ See [examples/http](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/fetch) for a short example. -### Fetch instrumentation Options +### Undici/Fetch instrumentation Options