Skip to content

Support for OpenTelemetry's db.query.text field#360

Draft
JoshMock wants to merge 7 commits intomainfrom
otel-db.query.text
Draft

Support for OpenTelemetry's db.query.text field#360
JoshMock wants to merge 7 commits intomainfrom
otel-db.query.text

Conversation

@JoshMock
Copy link
Copy Markdown
Member

@JoshMock JoshMock commented Mar 13, 2026

Records a db.query.text field to OpenTelemetry spans for search-like queries sent to Elasticsearch, adding support for an optional semantic convention noted in the OTel docs. Best-effort sanitization is performed, with JSON DSL queries having all primitive values substituted with ?. Parameterized string queries have all parameters dropped. Any other string-based queries are NOT recorded, as sanitization is less straightforward.

This also refactors OpenTelemetry into a middleware module, which is an ongoing effort we are applying to the transport library. See elastic/elasticsearch-js#2902

NOTE: This is a spec-driven development experiment, using Spec Kit. All the specification artifacts in .specify/ and specs/ will be committed for now, at least until the experiment ends.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds opt-in recording of OpenTelemetry’s db.query.text attribute for Elasticsearch “search-like” requests (with best-effort sanitization and truncation), and refactors OpenTelemetry instrumentation into a first-class transport middleware. It also commits Spec Kit artifacts for the spec-driven development experiment.

Changes:

  • Introduce OpenTelemetryMiddleware (plus middleware-engine wrap support) and wire it into Transport.request().
  • Add sanitization helpers (sanitizeJsonBody, sanitizeStringQuery, sanitizeNdjsonBody) and extensive unit tests for sanitization + OTel behavior.
  • Update packaging/exports and commit Spec Kit specification artifacts under specs/ and .specify/.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/unit/transport.test.ts Removes OTel tests from transport unit suite (moved to dedicated middleware test).
test/unit/security.test.ts Adds unit tests for new sanitization helpers.
test/unit/middleware/opentelemetry.test.ts Adds comprehensive OTel middleware tests, including db.query.text behavior, truncation, and env-var controls.
src/symbols.ts Removes OTel-related internal symbols in favor of middleware approach.
src/security.ts Adds sanitization helpers used for db.query.text capture.
src/middleware/types.ts Adds OPEN_TELEMETRY middleware name/priority and introduces wrap middleware hook.
src/middleware/index.ts Exposes the new OpenTelemetry middleware and related constants/types.
src/middleware/OpenTelemetry.ts Implements OTel span creation, attribute setting, and db.query.text capture/sanitization/truncation.
src/middleware/MiddlewareEngine.ts Adds executeWrap() to compose/execute wrap-style middleware.
src/Transport.ts Refactors request flow to run through middleware wrapping; wires OTel middleware; exports OpenTelemetryOptions from middleware module.
specs/002-capture-search-query-default-on/tasks.md Spec Kit task breakdown for the feature.
specs/002-capture-search-query-default-on/spec.md Spec for env-var disable, truncation, and per-request overrides.
specs/002-capture-search-query-default-on/research.md Research notes and decisions for sanitization strategy and precedence rules.
specs/002-capture-search-query-default-on/plan.md Implementation plan for the combined 001+002 feature scope.
specs/002-capture-search-query-default-on/data-model.md Data model for captureSearchQuery and endpoint categorization.
specs/002-capture-search-query-default-on/contracts/otel-api.md Public contract for OpenTelemetryOptions and security module exports.
specs/002-capture-search-query-default-on/checklists/requirements.md Requirements checklist for the 002 spec.
specs/001-otel-search-query-capture/spec.md Initial 001 feature spec for db.query.text capture.
specs/001-otel-search-query-capture/checklists/requirements.md Requirements checklist for the 001 spec.
package.json Updates exports mappings (subpath import/require resolution).
.specify/templates/tasks-template.md Adds tasks template for Spec Kit workflow.
.specify/templates/spec-template.md Adds spec template for Spec Kit workflow.
.specify/templates/plan-template.md Adds plan template for Spec Kit workflow.
.specify/templates/constitution-template.md Adds constitution template for Spec Kit workflow.
.specify/templates/checklist-template.md Adds checklist template for Spec Kit workflow.
.specify/templates/agent-file-template.md Adds agent context template for Spec Kit workflow.
.specify/scripts/bash/update-agent-context.sh Adds script to update agent context files based on plans.
.specify/scripts/bash/setup-plan.sh Adds script to initialize plan files from templates.
.specify/scripts/bash/create-new-feature.sh Adds script to create new Spec Kit feature directories/branches.
.specify/scripts/bash/common.sh Adds shared helper functions for Spec Kit scripts.
.specify/scripts/bash/check-prerequisites.sh Adds consolidated prerequisite checker for Spec Kit workflow.
.specify/memory/constitution.md Adds/ratifies the project constitution for spec-driven workflow.
.npmignore Excludes Spec Kit artifacts and additional repo files from npm package.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +159 to +170
if ((otelOptions.captureSearchQuery ?? false) && params.meta?.name != null && SEARCH_LIKE_ENDPOINTS.has(params.meta.name)) {
const rawBody = NDJSON_ENDPOINTS.has(params.meta.name) ? params.bulkBody : params.body
if (rawBody != null && rawBody !== '' && !isStream(rawBody)) {
const bodyStr = typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody)
let sanitized: string | null
if (NDJSON_ENDPOINTS.has(params.meta.name)) {
sanitized = sanitizeNdjsonBody(bodyStr)
} else if (STRING_QUERY_ENDPOINTS.has(params.meta.name)) {
sanitized = sanitizeStringQuery(bodyStr)
} else {
sanitized = sanitizeJsonBody(bodyStr)
}
Comment on lines +160 to +174
const rawBody = NDJSON_ENDPOINTS.has(params.meta.name) ? params.bulkBody : params.body
if (rawBody != null && rawBody !== '' && !isStream(rawBody)) {
const bodyStr = typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody)
let sanitized: string | null
if (NDJSON_ENDPOINTS.has(params.meta.name)) {
sanitized = sanitizeNdjsonBody(bodyStr)
} else if (STRING_QUERY_ENDPOINTS.has(params.meta.name)) {
sanitized = sanitizeStringQuery(bodyStr)
} else {
sanitized = sanitizeJsonBody(bodyStr)
}
if (sanitized !== null) {
attributes['db.query.text'] = sanitized.slice(0, SEARCH_QUERY_MAX_LENGTH)
}
}
Comment thread src/middleware/OpenTelemetry.ts Outdated
Comment on lines +72 to +83
wrap = async (params: TransportRequestParams, options: TransportRequestOptions, next: () => Promise<any>): Promise<any> => {
const otelOptions: OpenTelemetryOptions = Object.assign({}, this.transportOptions, options.openTelemetry ?? {})

if (!(otelOptions.enabled ?? true) || params.meta?.name == null) {
return await next()
}

let context = opentelemetry.context.active()
if (otelOptions.suppressInternalInstrumentation ?? false) {
context = suppressTracing(context)
}

Comment thread src/middleware/MiddlewareEngine.ts Outdated
Comment on lines +44 to +50
try {
if (mw.wrap == null) return await next()
return await mw.wrap(params, options, next)
} catch (error) {
if (error instanceof ElasticsearchClientError) throw error
throw new MiddlewareException(`Middleware ${mw.name} failed in wrap`, { cause: error })
}
Comment thread src/Transport.ts Outdated
Comment on lines +717 to +719
params, options, async () => await this._request(params, { ...options, meta: true })
) as TransportResult
return (options as TransportRequestOptionsWithMeta).meta ? result : result.body
Comment thread package.json
Comment on lines 15 to 24
"./lib/*": {
"import": "./esm/*.js",
"require": "./lib/*.js",
"types": "./lib/*.d.ts"
},
"./*.js": {
"import": "./esm/*.js",
"import": "./esm/*",
"require": "./lib/*.js",
"types": "./lib/*.d.ts"
},
"./*": {
"import": "./esm/*.js",
"require": "./lib/*.js",
"types": "./lib/*.d.ts"
"import": "./esm/*",
"require": "./*",
"types": "./*.d.ts"
}
Comment thread src/middleware/OpenTelemetry.ts Outdated
Comment on lines +86 to +98
return await this.tracer.startActiveSpan(params.meta.name, { attributes, kind: SpanKind.CLIENT }, context, async (span) => {
try {
const result = await next()
this.setResponseAttributes(span, result)
return result
} catch (err: any) {
span.recordException(err as Exception)
span.setStatus({ code: SpanStatusCode.ERROR })
span.setAttribute('error.type', err.name ?? 'Error')
throw err
} finally {
span.end()
}
@JoshMock JoshMock force-pushed the otel-db.query.text branch from 9ee3ec1 to 90660aa Compare March 13, 2026 20:11
@JoshMock JoshMock force-pushed the otel-db.query.text branch from 90660aa to 372d539 Compare March 13, 2026 20:23
@elasticmachine
Copy link
Copy Markdown
Collaborator

💚 Build Succeeded

Performance benchmark comparison

Benchmark details (Ubuntu)

Performance Benchmarks

Transport#constructor - UndiciConnection

WeightedConnectionPool

Stat Base PR Change
p75 753031.000 737310.000 ↓ -2.088%
p99 842646.000 864227.000 ↑ +2.561%
avg 674875.409 704267.409 ↑ +4.355%

ClusterConnectionPool

Stat Base PR Change
p75 562866.000 645388.000 ↑ +14.661%
p99 591666.000 850373.000 ↑ +43.725%
avg 552587.043 636774.619 ↑ +15.235%

CloudConnectionPool

Stat Base PR Change
p75 524296.000 615626.000 ↑ +17.420%
p99 550066.000 657780.000 ↑ +19.582%
avg 507772.800 584043.095 ↑ +15.021%

Transport#constructor - HttpConnection

WeightedConnectionPool

Stat Base PR Change
p75 422947.000 482507.000 ↑ +14.082%
p99 447468.000 560959.000 ↑ +25.363%
avg 404436.440 472760.957 ↑ +16.894%

ClusterConnectionPool

Stat Base PR Change
p75 45299.861 52144.734 ↑ +15.110%
p99 47230.911 52768.657 ↑ +11.725%
avg 42939.657 48984.844 ↑ +14.078%

CloudConnectionPool

Stat Base PR Change
p75 37891.207 45432.145 ↑ +19.902%
p99 43697.287 50781.199 ↑ +16.211%
avg 36862.441 41915.917 ↑ +13.709%

GC Benchmarks

Search request: defaults

Metric Base PR Change
opsPerSec 44112.000 45785.000 ↑ +3.793%
avgLatencyMs 0.023 0.022 ↓ -3.654%
durationMs 113.346 109.204 ↓ -3.654%
Garbage collection
totalEvents 0 0 n/a
Memory usage (after)
heapUsed 11850384.000 bytes 11949632.000 bytes ↑ +0.838%
heapTotal 30298112.000 bytes 30822400.000 bytes ↑ +1.730%
external 3560738.000 bytes 3560738.000 bytes +0.000%
arrayBuffers 19827.000 19827.000 +0.000%
Memory usage (delta)
heapUsed delta 171400.000 bytes 188144.000 bytes ↑ +9.769%
heapTotal delta 524288.000 bytes 524288.000 bytes +0.000%
external delta 0.000 bytes 0.000 bytes n/a

Search request: defaults + compression

Metric Base PR Change
opsPerSec 6185.000 7023.000 ↑ +13.549%
avgLatencyMs 0.162 0.142 ↓ -11.927%
durationMs 808.284 711.878 ↓ -11.927%
Garbage collection
totalEvents 14.000 12.000 ↓ -14.286%
totalDuration 26.898 23.415 ↓ -12.952%
avgDuration 1.921 1.951 ↑ +1.556%
maxDuration 6.113 6.593 ↑ +7.854%
Memory usage (after)
heapUsed 12371832.000 bytes 12473848.000 bytes ↑ +0.825%
heapTotal 30822400.000 bytes 31608832.000 bytes ↑ +2.551%
external 8863530.000 bytes 7994754.000 bytes ↓ -9.802%
arrayBuffers 44411.000 44411.000 +0.000%
Memory usage (delta)
heapUsed delta 348280.000 bytes 371560.000 bytes ↑ +6.684%
heapTotal delta 262144.000 bytes 524288.000 bytes ↑ +100.000%
external delta 3655400.000 bytes 2786624.000 bytes ↓ -23.767%
---

This comment was automatically generated by the comparative benchmark workflow.

History

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants