Skip to content

Docs on how to disable OpenTelemetry #2934

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/reference/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

```typescript
import { Transport } from '@elastic/transport'

class MyTransport extends Transport {
async request(params, options = {}): Promise<any> {
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 = 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:

```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:
Expand Down
6 changes: 3 additions & 3 deletions docs/reference/transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -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)
}
}
```
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
161 changes: 136 additions & 25 deletions test/unit/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('-')) {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -301,7 +303,7 @@ test('Close the client', async t => {
t.plan(1)

class MyConnectionPool extends BaseConnectionPool {
async empty (): Promise<void> {
async empty(): Promise<void> {
t.pass('called')
}
}
Expand Down Expand Up @@ -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'
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -456,39 +458,39 @@ 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}`,
// Connection: UndiciConnection is the default
})

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}`,
Connection: HttpConnection,
})

await client.transport.request({ method: 'GET', path: '/' })
server.stop()
})

test('caFingerprint', t => {
Expand All @@ -503,19 +505,19 @@ 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()
})

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()
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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,
Expand All @@ -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<any> {
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<any> {
// @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()
})
Loading