From 14d9bcb5cb614371ed8af058e571766c4a5f775b Mon Sep 17 00:00:00 2001 From: Josh Mock Date: Mon, 28 Jul 2025 14:19:53 -0500 Subject: [PATCH 1/4] Add unit tests to verify OTel disable option --- package.json | 1 + test/unit/client.test.ts | 161 +++++++++++++++++++++++++++++++++------ 2 files changed, 137 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 434986d95..55d8cdc8f 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ }, "devDependencies": { "@elastic/request-converter": "9.1.2", + "@opentelemetry/sdk-trace-base": "1.30.1", "@sinonjs/fake-timers": "14.0.0", "@types/debug": "4.1.12", "@types/ms": "2.1.0", diff --git a/test/unit/client.test.ts b/test/unit/client.test.ts index feffc373e..ac8203f55 100644 --- a/test/unit/client.test.ts +++ b/test/unit/client.test.ts @@ -8,10 +8,12 @@ import { URL } from 'node:url' import { setTimeout } from 'node:timers/promises' import { test } from 'tap' import FakeTimers from '@sinonjs/fake-timers' +import { Transport } from '@elastic/transport' import { buildServer, connection } from '../utils' import { Client, errors, SniffingTransport } from '../..' import * as symbols from '@elastic/transport/lib/symbols' import { BaseConnectionPool, CloudConnectionPool, WeightedConnectionPool, HttpConnection } from '@elastic/transport' +import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base' let clientVersion: string = require('../../package.json').version // eslint-disable-line if (clientVersion.includes('-')) { @@ -124,7 +126,7 @@ test('Basic auth', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { authorization: 'Basic aGVsbG86d29ybGQ=' }) return { statusCode: 200, @@ -149,7 +151,7 @@ test('Basic auth via url', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { authorization: 'Basic aGVsbG86d29ybGQ=' }) return { statusCode: 200, @@ -170,7 +172,7 @@ test('ApiKey as string', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { authorization: 'ApiKey foobar' }) return { statusCode: 200, @@ -194,7 +196,7 @@ test('ApiKey as object', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { authorization: 'ApiKey Zm9vOmJhcg==' }) return { statusCode: 200, @@ -221,7 +223,7 @@ test('Bearer auth', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { authorization: 'Bearer token' }) return { statusCode: 200, @@ -245,7 +247,7 @@ test('Override authentication per request', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { authorization: 'Basic foobar' }) return { statusCode: 200, @@ -273,7 +275,7 @@ test('Custom headers per request', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { foo: 'bar', faz: 'bar' @@ -301,7 +303,7 @@ test('Close the client', async t => { t.plan(1) class MyConnectionPool extends BaseConnectionPool { - async empty (): Promise { + async empty(): Promise { t.pass('called') } } @@ -336,10 +338,10 @@ test('Elastic Cloud config', t => { t.test('Invalid Cloud ID will throw ConfigurationError', t => { t.throws(() => new Client({ - cloud : { - id : 'invalidCloudIdThatIsNotBase64' + cloud: { + id: 'invalidCloudIdThatIsNotBase64' }, - auth : { + auth: { username: 'elastic', password: 'changeme' } @@ -414,7 +416,7 @@ test('Meta header enabled by default', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.match(opts.headers, { 'x-elastic-client-meta': `es=${clientVersion},js=${nodeVersion},t=${transportVersion},hc=${nodeVersion}` }) return { statusCode: 200, @@ -435,7 +437,7 @@ test('Meta header disabled', async t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.notOk(opts.headers?.['x-elastic-client-meta']) return { statusCode: 200, @@ -456,12 +458,13 @@ test('Meta header disabled', async t => { test('Meta header indicates when UndiciConnection is used', async t => { t.plan(1) - function handler (req: http.IncomingMessage, res: http.ServerResponse) { + function handler(req: http.IncomingMessage, res: http.ServerResponse) { t.equal(req.headers['x-elastic-client-meta'], `es=${clientVersion},js=${nodeVersion},t=${transportVersion},un=${nodeVersion}`) res.end('ok') } const [{ port }, server] = await buildServer(handler) + t.after(() => server.stop()) const client = new Client({ node: `http://localhost:${port}`, @@ -469,18 +472,18 @@ test('Meta header indicates when UndiciConnection is used', async t => { }) await client.transport.request({ method: 'GET', path: '/' }) - server.stop() }) test('Meta header indicates when HttpConnection is used', async t => { t.plan(1) - function handler (req: http.IncomingMessage, res: http.ServerResponse) { + function handler(req: http.IncomingMessage, res: http.ServerResponse) { t.equal(req.headers['x-elastic-client-meta'], `es=${clientVersion},js=${nodeVersion},t=${transportVersion},hc=${nodeVersion}`) res.end('ok') } const [{ port }, server] = await buildServer(handler) + t.after(() => server.stop()) const client = new Client({ node: `http://localhost:${port}`, @@ -488,7 +491,6 @@ test('Meta header indicates when HttpConnection is used', async t => { }) await client.transport.request({ method: 'GET', path: '/' }) - server.stop() }) test('caFingerprint', t => { @@ -503,9 +505,9 @@ test('caFingerprint', t => { test('caFingerprint can\'t be configured over http / 1', t => { t.throws(() => new Client({ - node: 'http://localhost:9200', - caFingerprint: 'FO:OB:AR' - }), + node: 'http://localhost:9200', + caFingerprint: 'FO:OB:AR' + }), errors.ConfigurationError ) t.end() @@ -513,9 +515,9 @@ test('caFingerprint can\'t be configured over http / 1', t => { test('caFingerprint can\'t be configured over http / 2', t => { t.throws(() => new Client({ - nodes: ['http://localhost:9200'], - caFingerprint: 'FO:OB:AR' - }), + nodes: ['http://localhost:9200'], + caFingerprint: 'FO:OB:AR' + }), errors.ConfigurationError ) t.end() @@ -551,7 +553,7 @@ test('Ensure new client does not time out if requestTimeout is not set', async t const clock = FakeTimers.install({ toFake: ['setTimeout'] }) t.teardown(() => clock.uninstall()) - function handler (_req: http.IncomingMessage, res: http.ServerResponse) { + function handler(_req: http.IncomingMessage, res: http.ServerResponse) { setTimeout(1000 * 60 * 60).then(() => { t.ok('timeout ended') res.setHeader('content-type', 'application/json') @@ -660,7 +662,7 @@ test('serverless defaults', t => { t.plan(1) const Connection = connection.buildMockConnection({ - onRequest (opts) { + onRequest(opts) { t.equal(opts.headers?.['elastic-api-version'], '2023-10-31') return { statusCode: 200, @@ -686,3 +688,112 @@ test('serverless defaults', t => { t.end() }) + +test('custom transport: class', async t => { + t.plan(3) + + class MyTransport extends Transport { + async request(params, options): Promise { + t.ok(true, 'custom Transport request function should be called') + return super.request(params, options) + } + } + + function handler(_req: http.IncomingMessage, res: http.ServerResponse) { + t.ok(true, 'handler should be called') + res.end('ok') + } + + const [{ port }, server] = await buildServer(handler) + t.after(() => server.stop()) + + const client = new Client({ + node: `http://localhost:${port}`, + Transport: MyTransport + }) + + t.ok(client.transport instanceof MyTransport, 'Custom transport should be used') + + client.transport.request({ method: 'GET', path: '/' }) +}) + +test('custom transport: disable otel via options', async t => { + const exporter = new InMemorySpanExporter() + const processor = new SimpleSpanProcessor(exporter) + const provider = new BasicTracerProvider({ + spanProcessors: [processor] + }) + provider.register() + + t.after(async () => { + await provider.forceFlush() + exporter.reset() + await provider.shutdown() + }) + + class MyTransport extends Transport { + async request(params, options = {}): Promise { + // @ts-expect-error + options.openTelemetry = { enabled: false } + return super.request(params, options) + } + } + + function handler(_req: http.IncomingMessage, res: http.ServerResponse) { + res.end('ok') + } + + const [{ port }, server] = await buildServer(handler) + t.after(() => server.stop()) + + const client = new Client({ + node: `http://localhost:${port}`, + Transport: MyTransport + }) + + await client.transport.request({ + path: '/hello', + method: 'GET', + meta: { name: 'hello' }, + }) + + t.equal(exporter.getFinishedSpans().length, 0) + t.end() +}) + +test('custom transport: disable otel via env var', async t => { + const exporter = new InMemorySpanExporter() + const processor = new SimpleSpanProcessor(exporter) + const provider = new BasicTracerProvider({ + spanProcessors: [processor] + }) + provider.register() + + t.after(async () => { + await provider.forceFlush() + exporter.reset() + await provider.shutdown() + }) + + function handler(_req: http.IncomingMessage, res: http.ServerResponse) { + res.end('ok') + } + + const [{ port }, server] = await buildServer(handler) + t.after(() => server.stop()) + + const client = new Client({ + node: `http://localhost:${port}`, + }) + + process.env.OTEL_ELASTICSEARCH_ENABLED = 'false' + + await client.transport.request({ + path: '/hello', + method: 'GET', + meta: { name: 'hello' }, + }) + + t.equal(exporter.getFinishedSpans().length, 0) + t.end() +}) From 31b17ff953907cb96ef78da1381780da63246cbc Mon Sep 17 00:00:00 2001 From: Josh Mock Date: Mon, 28 Jul 2025 14:25:08 -0500 Subject: [PATCH 2/4] Add documentation for disabling OpenTelemetry --- docs/reference/observability.md | 35 +++++++++++++++++++++++++++++++++ docs/reference/transport.md | 6 +++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/reference/observability.md b/docs/reference/observability.md index d3b5f59f3..e03a1523b 100644 --- a/docs/reference/observability.md +++ b/docs/reference/observability.md @@ -35,6 +35,41 @@ To start sending Elasticsearch trace data to your OpenTelemetry endpoint, follow node --require '@opentelemetry/auto-instrumentations-node/register' index.js ``` +### Disabling OpenTelemetry collection [disable-otel] + +As of `@elastic/transport` version 9.1.0—or 8.10.0 when using `@elastic/elasticsearch` 8.x—OpenTelemetry tracing can be disabled in multiple ways. + +To entirely disable OpenTelemetry collection, you can provide a custom `Transport` at client instantiation time that sets `openTelemetry.enabled` to `false`: + +```typscript +import { Transport } from '@elastic/transport' + +class MyTransport extends Transport { + async request(params, options = {}): Promise { + options.openTelemetry = { enabled: false } + return super.request(params, options) + } +} + +const client = new Client({ + node: '...', + auth: { ... }, + Transport: MyTransport +}) +``` + +Alternatively, you can also export an environment variable `OTEL_ELASTICSEARCH_ENABLED=false` to achieve the same effect. + +If you would not like OpenTelemetry to be disabled entirely, but would like the client to suppress tracing, you can use the option `openTelemetry.suppressInternalInstrumentation = false` instead. + +If you would like to keep either option enabled by default, but want to disable them for a single API call, you can pass `Transport` options as a second argument to any API function call: + +```typescript +const response = await client.search({ ... }, { + openTelemetry: { enabled: false } +}) +``` + ## Events [_events] The client is an event emitter. This means that you can listen for its events to add additional logic to your code, without needing to change the client’s internals or how you use the client. You can find the events' names by accessing the `events` key of the client: diff --git a/docs/reference/transport.md b/docs/reference/transport.md index 382574bb6..4977a2890 100644 --- a/docs/reference/transport.md +++ b/docs/reference/transport.md @@ -12,7 +12,7 @@ const { Client } = require('@elastic/elasticsearch') const { Transport } = require('@elastic/transport') class MyTransport extends Transport { - request (params, options, callback) { + request (params, options) { // your code } } @@ -26,9 +26,9 @@ Sometimes you need to inject a small snippet of your code and then continue to u ```js class MyTransport extends Transport { - request (params, options, callback) { + request (params, options) { // your code - return super.request(params, options, callback) + return super.request(params, options) } } ``` From 1c07ac9b89ffefe975a2121b3df1a7308720c3a9 Mon Sep 17 00:00:00 2001 From: Josh Mock Date: Mon, 28 Jul 2025 14:30:03 -0500 Subject: [PATCH 3/4] Fix markdown typo --- docs/reference/observability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/observability.md b/docs/reference/observability.md index e03a1523b..970b86c3f 100644 --- a/docs/reference/observability.md +++ b/docs/reference/observability.md @@ -41,7 +41,7 @@ As of `@elastic/transport` version 9.1.0—or 8.10.0 when using `@elastic/el To entirely disable OpenTelemetry collection, you can provide a custom `Transport` at client instantiation time that sets `openTelemetry.enabled` to `false`: -```typscript +```typescript import { Transport } from '@elastic/transport' class MyTransport extends Transport { From 6ce5ba153ae77b6a52a2e8d6b3925e03dd821b82 Mon Sep 17 00:00:00 2001 From: Josh Mock Date: Mon, 28 Jul 2025 14:32:56 -0500 Subject: [PATCH 4/4] Use correct value in docs for OTel suppression --- docs/reference/observability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/observability.md b/docs/reference/observability.md index 970b86c3f..b307c5571 100644 --- a/docs/reference/observability.md +++ b/docs/reference/observability.md @@ -60,7 +60,7 @@ const client = new Client({ Alternatively, you can also export an environment variable `OTEL_ELASTICSEARCH_ENABLED=false` to achieve the same effect. -If you would not like OpenTelemetry to be disabled entirely, but would like the client to suppress tracing, you can use the option `openTelemetry.suppressInternalInstrumentation = false` instead. +If you would not like OpenTelemetry to be disabled entirely, but would like the client to suppress tracing, you can use the option `openTelemetry.suppressInternalInstrumentation = true` instead. If you would like to keep either option enabled by default, but want to disable them for a single API call, you can pass `Transport` options as a second argument to any API function call: