mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-20 22:25:30 +00:00
feat(telemetry): link daemon HTTP request spans to inbound W3C traceparent (#9391)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
npm cache producer / Save npm cache (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
* feat(telemetry): link daemon HTTP request spans to inbound W3C traceparent The daemon HTTP surface records a request span per request, but every span starts a new trace: a caller forwarding the standard W3C traceparent header (OTel-instrumented clients, proxies, gateways) gets no linkage back to its own trace. Extract traceparent/tracestate from inbound request headers in the daemon telemetry middleware and parent the request span to that remote context. Extraction reuses the same path as the existing JSON-RPC _meta extraction (global propagator first, strict manual fallback so behavior is identical without a registered SDK) and fails closed: requests without a valid header keep the exact current span shape. * fix(telemetry): guard inbound traceparent sampling and align W3C fallback - Force TraceFlags.SAMPLED on inbound HTTP parents via the existing shouldForceSampled() matrix: an unsampled remote parent under the default parentbased_always_on sampler silently dropped the request span, the whole next() subtree, and the session-subprocess spans forwarded via _meta (review C1). - Replace the hand-rolled manual fallback parser with a direct W3CTraceContextPropagator instance so acceptance rules (future versions, tracestate, all-zero ids, version-00 extension field) match the registered path with or without an initialized SDK. - Gate middleware extraction behind isTelemetrySdkInitialized() to skip the hot-path parse when telemetry is off, and emit a debug daemon log when a present-but-invalid traceparent header is rejected. - Re-export DaemonRequestSpanOptions from the core barrel and add a type-level guard so the parentContext field cannot silently disappear (vitest alone cannot catch its removal). * chore(vscode): regenerate companion NOTICES.txt for @opentelemetry/core * fix(telemetry): lazy-load OTel core fallback propagator behind SDK init Address review feedback on the inbound traceparent linkage: - Keep @opentelemetry/core out of the static graph. The module-level W3CTraceContextPropagator in daemon-tracing.ts pulled the CJS barrel (bot-measured +65,046 bytes) into every closure loading that module, including telemetry-off deployments. daemon-tracing.ts now keeps only a holder + setter (setDaemonFallbackPropagator, typed against @opentelemetry/api — type imports stay free at runtime); the lazy sdk-impl.ts chunk, whose closure already contains @opentelemetry/core via sdk-node/resources, constructs and injects the W3C instance on the successful SDK assembly path. Until injection, extraction returns no parent context: the HTTP edge is already gated on isTelemetrySdkInitialized (nothing changes when telemetry is off), and the _meta edge's consumers (withDaemonSpan / withInteractionSpan) short-circuit on the same flag, so an unresolved pre-init parent never had an observable effect. - Add the mutation-verified fail-closed test for the header-extraction try/catch in daemonTelemetryMiddleware: a throwing extractor leaves the request settling normally (recordDaemonHttpRequest still fires once) with no parentContext on the span options. - Record the rejected traceparent value (truncated to 128 chars) as http.request.header.traceparent on the invalid-header breadcrumb — traceparent only carries trace-id/span-id/flags, so this is privacy-safe and makes broken cross-service joins diagnosable. Also document why the _meta extraction path deliberately skips shouldForceSampled (trusted in-process bridge vs external HTTP input). * feat(telemetry): carry inbound trace id into daemon access log with telemetry off Telemetry off (the default) left daemon logs without any trace id: with no request span, the log trace prefix never fires, so a caller forwarding W3C traceparent could not be joined to its daemon log lines. The middleware now parses the header with a plain regex (extractInboundTraceId — same shape/all-zero/ff rejections as the W3C propagator, no OTel machinery) and stores the trace id on the per-response telemetry context. The access log emits it as the camelCase traceId field of "request completed", keeping the log-based join alive with no telemetry config and no trace backend. With telemetry on nothing changes: the request span already carries the caller's trace id into the log prefix. * fix(telemetry): unify _meta/HTTP sampling and repair build export - Export extractInboundTraceId from the core barrel: the previous commit exported it from daemon-tracing.ts only, so downstream package builds failed with TS2305. - extractDaemonTraceContext now applies the same shouldForceSampled() matrix as the HTTP edge: the _meta path is also reachable from direct ACP clients (acpAgent newSession/loadSession/unstable_resumeSession and Session.prompt pass caller-controlled _meta), so an external sampled=0 parent no longer silences daemon spans there either. The in-process bridge is unaffected (its injected values are already SAMPLED). - The rejected-header breadcrumb now goes through sanitizeLogText so a crafted traceparent cannot forge log line structure with control characters. - Add the sdk-impl wiring test: after initializeTelemetry the injected W3C fallback propagator resolves inbound HTTP parents. * fix(telemetry): align log-path traceparent parsing and emit traceId in both modes - extractInboundTraceId now mirrors the vendored W3C propagator's acceptance exactly: single optional leading/trailing whitespace and trailing extension fields above version 00 (version 00 must stay four fields). Previously the strict four-field anchor made the two paths disagree on the same forward-compatible header, silently dropping the access-log traceId for exactly the callers the propagator path supports. - The camelCase traceId access-log field is now captured whenever a valid header parses, regardless of telemetry mode, so one saved log query / alert shape works for every deployment; with telemetry on the snake_case span prefix carries the same id redundantly. * fix(telemetry): move inbound trace id getter out of the middleware module 52d572c0f2 made the access log statically import the telemetry middleware module to read the captured inbound trace id. The access log sits inside the serve fast-path pre-listen closure (run-qwen-serve imports it directly), so the middleware's core-barrel import graph came along for the ride and check-serve-fast-path-bundle started failing: the 5.6MB core chunk (shell tool, glob, chokidar, @iarna/toml, fzf) became statically reachable from run-qwen-serve. Move the response-context symbol, its type, and the getDaemonTelemetryInboundTraceId getter into a new import-light telemetry-context.ts; the middleware imports the symbol from there and re-exports the getter, so the access log no longer links against the telemetry module at all. * fix(telemetry): capture inbound trace id pre-auth under a dedicated symbol * test(telemetry): pin the trace id seam through the context module getter --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
This commit is contained in:
parent
4b77a6e472
commit
d96f264de7
17 changed files with 1638 additions and 464 deletions
142
docs/design/2026-08-18-daemon-http-inbound-trace-context.md
Normal file
142
docs/design/2026-08-18-daemon-http-inbound-trace-context.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Daemon HTTP inbound trace context
|
||||
|
||||
## Motivation
|
||||
|
||||
The daemon already _propagates_ trace context outbound: prompt requests carry a
|
||||
`traceparent` inside JSON-RPC `_meta`, and the daemon extracts it to parent its
|
||||
bridge spans (`extractDaemonTraceContext`). The HTTP surface, however, only
|
||||
_records_ request spans — every `qwen-code.daemon.request` span starts a new
|
||||
trace. An HTTP caller that forwards the standard W3C `traceparent` header
|
||||
(corporate proxies, OTel-instrumented clients, ACP gateways) gets no linkage:
|
||||
the server-side span cannot be joined back to the caller's trace, so
|
||||
cross-service debugging falls back to timestamps.
|
||||
|
||||
W3C Trace Context extraction at the HTTP server edge is standard
|
||||
`SpanKind.SERVER`-adjacent behavior per the OTel HTTP semantic conventions, and
|
||||
the plumbing already exists: `withDaemonSpan` accepts an explicit
|
||||
`parentContext`.
|
||||
|
||||
## Design
|
||||
|
||||
1. **Core** (`daemon-tracing.ts`): the existing `_meta` extraction logic
|
||||
(global `propagation.extract` first, then a direct
|
||||
`W3CTraceContextPropagator` instance as fallback, so acceptance rules —
|
||||
future traceparent versions, `tracestate`, all-zero ids — are identical
|
||||
with and without a registered global propagator) moves into a shared
|
||||
`contextFromTraceparentValues` helper. A new
|
||||
`extractDaemonHttpTraceContext(headers)` reads `traceparent`/`tracestate`
|
||||
from a Node-style (lowercased) header object and reuses that helper.
|
||||
`DaemonRequestSpanOptions` gains an optional `parentContext` passed straight
|
||||
through to `withDaemonSpan`.
|
||||
2. **Serve middleware**: `daemonTelemetryMiddleware` extracts from
|
||||
`req.headers` per request (fail-closed to `undefined`, telemetry never
|
||||
affects handling) and passes the context only when extraction succeeded —
|
||||
requests without a valid header keep the exact current span shape.
|
||||
Span-context extraction is gated on `isTelemetrySdkInitialized()`, so
|
||||
telemetry-off deployments pay no OTel machinery on the hot path — only the
|
||||
single-regex trace-id capture described under Log correlation — and a
|
||||
present-but-invalid header emits a debug daemon log
|
||||
(`qwen-code.daemon.traceparent.invalid`) so a rejected header is
|
||||
diagnosable from daemon logs alone.
|
||||
|
||||
Note the whole subtree relocates with the request span, not just
|
||||
`daemon.request` itself: session-subprocess spans reached via `_meta`
|
||||
(prompt / model / tool) also join the caller's trace, so anything
|
||||
aggregating or alerting by `traceId` sees session-side spans change
|
||||
ownership too.
|
||||
|
||||
## Log correlation
|
||||
|
||||
Daemon log lines already carry a `[trace_id=… span_id=…]` prefix when written
|
||||
inside an active recording span (`getActiveTraceContext`, #9084). Telemetry
|
||||
on, this PR closes the loop end to end: the request span parents to the
|
||||
caller's trace, so every daemon log line for that request — including the
|
||||
access log's `request completed` — is prefixed with the caller's trace id.
|
||||
(The access log's `finish` listener reads the active span through the same
|
||||
AsyncLocalStorage propagation as the logger, so ordering of the two `finish`
|
||||
listeners is irrelevant for the prefix.)
|
||||
|
||||
Telemetry off — the default — there is no span, so the prefix never fires.
|
||||
A separate lightweight path keeps the log-based join alive with no telemetry
|
||||
config and no trace backend: the middleware parses the `traceparent` header
|
||||
with a plain regex (`extractInboundTraceId`, acceptance mirroring the vendored
|
||||
W3C propagator — same shape/all-zero/`ff` rejections, single optional
|
||||
leading/trailing whitespace, trailing extension fields above version `00` —
|
||||
so a header either joins on both paths or neither; `tracestate` stays in the
|
||||
propagator path since a log line only needs a plausible trace id) and stores
|
||||
the trace id on the per-response telemetry context (the same
|
||||
symbol the workspace hash uses). The access log's `finish` callback reads it
|
||||
back and emits it as the camelCase `traceId` field of `request completed` —
|
||||
distinct from the logger's reserved snake_case `trace_id` prefix keys, so a
|
||||
caller cannot spoof the span-derived prefix. The camelCase field is captured
|
||||
in both telemetry modes whenever a valid header parses, so one log query
|
||||
shape works for every deployment; with telemetry on the snake_case span
|
||||
prefix carries the same id redundantly. The field is omitted, not
|
||||
empty, when no valid header is present, and every step stays fail-closed
|
||||
(telemetry must not affect request handling).
|
||||
|
||||
## Sampling policy
|
||||
|
||||
The caller's `sampled` bit is not adopted verbatim on the HTTP path. Under
|
||||
the default `parentbased_always_on` sampler (the daemon SDK configures no
|
||||
sampler), a remote unsampled parent delegates to `AlwaysOffSampler` and would
|
||||
silently delete the request span, everything under `next()`, and — via the
|
||||
`_meta` forwarding — the session-subprocess spans. `sampled=0` is simply the
|
||||
caller's head-based ratio sampling, so inbound HTTP parents force
|
||||
`TraceFlags.SAMPLED` through the same `shouldForceSampled()` decision matrix
|
||||
as the synthetic session root: `parentbased_*` defaults and `always_on` force
|
||||
sampling; `parentbased_always_off` honors the operator's opt-out;
|
||||
non-parentbased samplers (e.g. `traceidratio`) keep the caller's flags and
|
||||
decide per span. The `_meta` path applies the same forcing: for the
|
||||
in-process bridge (daemon → subprocess) it is a no-op — that parent is our
|
||||
own span, already SAMPLED under this policy — while direct ACP clients can
|
||||
also attach `_meta` with a caller-controlled `sampled=0`, which is external
|
||||
input exactly like the HTTP header and gets the same protection.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No new span kinds or attributes: existing `qwen-code.daemon.request` spans
|
||||
stay `SpanKind.INTERNAL` with the same attributes; only the parent link
|
||||
changes when a valid header is present.
|
||||
- No `traceparent` _response_ injection and no W3C `tracingresponse` support.
|
||||
- No new sampling configuration surface: the inbound policy reuses the
|
||||
existing `shouldForceSampled()` matrix (see above); the SDK's own sampler
|
||||
remains the only sampler authority.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- Changing the request span to `SpanKind.SERVER` per HTTP semconv: **known
|
||||
gap** — `qwen-code.daemon.request` is the daemon's only SERVER-adjacent
|
||||
span (`HttpInstrumentation` never patches the server side here because the
|
||||
SDK loads lazily), so backends deriving service topology / RED metrics
|
||||
from SERVER spans (Tempo service-graph, ARMS) will not recognize the
|
||||
daemon as a service inside the caller's trace. Switching would mutate the
|
||||
shape of every existing daemon.request span and can shift backend
|
||||
grouping; deferred as a follow-up.
|
||||
- Extracting inside the core `withDaemonRequestSpan` from a raw header bag:
|
||||
rejected because core request-span options are transport-primitive today;
|
||||
the middleware is the only place that knows the carrier is HTTP headers.
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit: header extraction (valid / absent / malformed / all-zero ids / array
|
||||
value / version `ff` / version `00` with extension field / future version
|
||||
`01` / inbound `tracestate`), request-span parenting through
|
||||
`withDaemonRequestSpan`, the sampled-flag decision matrix (default forced,
|
||||
`parentbased_always_off` and `traceidratio` verbatim, `_meta` path forced
|
||||
under the default sampler and verbatim on opt-out), middleware pass-through
|
||||
(present vs omitted key, telemetry-off
|
||||
skip, rejected-header debug log), and a type-level guard keeping
|
||||
`parentContext` on `DaemonRequestSpanOptions`.
|
||||
- Unit (telemetry-off log join): `extractInboundTraceId` (valid / absent /
|
||||
malformed / all-zero ids / version `ff` / array value / future version,
|
||||
plus no-propagator-needed in the fresh-module state), middleware capture
|
||||
(telemetry off with and without a header, telemetry on emitting the
|
||||
camelCase field alongside the span prefix),
|
||||
handler-resolved context initialization not clobbering the stored id), and
|
||||
the access log emitting / omitting the `traceId` field.
|
||||
- Dry run: `serve` with `QWEN_TELEMETRY_OUTFILE`, one curl with a fixed
|
||||
`traceparent` — exported span must share the header's traceId and parent to
|
||||
its spanId; a control request without the header must stay on its own trace.
|
||||
With telemetry disabled, the same curl must still log
|
||||
`request completed` with `traceId` matching the header.
|
||||
|
|
@ -403,6 +403,45 @@ established the principle: "telemetry's scope of work doesn't include
|
|||
sending identifiers to LLM providers"; correlation-header work moves to
|
||||
its own design discussion rather than landing under telemetry.
|
||||
|
||||
## Inbound correlation (daemon HTTP API)
|
||||
|
||||
The daemon HTTP API accepts the standard W3C `traceparent` header on every
|
||||
request. Two consumers read it independently:
|
||||
|
||||
- **Request span re-parenting (telemetry enabled).** When the telemetry SDK
|
||||
is initialized, a valid header is extracted as the request span's remote
|
||||
parent, so daemon spans attach under the caller's trace instead of
|
||||
starting a new one. The `_meta` forwarding path reads the same parent
|
||||
chain, so session subprocess spans forwarded through a daemon request
|
||||
inherit it too.
|
||||
- **Access-log `traceId` field (both modes).** A dedicated pre-auth capture
|
||||
middleware parses the header on every request — including ones
|
||||
short-circuited at auth (401), the rate limiter (429), the JSON body
|
||||
parser (400), or never matched by any route (404) — and the access log
|
||||
emits the caller trace id as a camelCase `traceId` field. With telemetry
|
||||
disabled this field is the only join between a daemon log line and the
|
||||
caller's logs (or trace backend), so one saved query works for both modes
|
||||
with no telemetry configuration.
|
||||
|
||||
An invalid-but-present header is rejected (the span stays parentless) and
|
||||
leaves a rate-limited DEBUG breadcrumb
|
||||
(`qwen-code.daemon.traceparent.invalid`) recording the rejected value, so a
|
||||
broken cross-service join is diagnosable from daemon logs alone.
|
||||
|
||||
### Forced sampling under inbound parents
|
||||
|
||||
Under the default `parentbased_always_on` sampler (and other parentbased
|
||||
defaults), a remote parent's `sampled=0` flag is a head-based decision on
|
||||
the caller's side, not a request to drop daemon telemetry, so extraction
|
||||
forces the SAMPLED flag on inbound parents. The only opt-out is
|
||||
`OTEL_TRACES_SAMPLER=parentbased_always_off`, which honors the caller's
|
||||
flags — note it also disables root-span sampling for the whole daemon, not
|
||||
just inbound-linked requests.
|
||||
|
||||
**Warning:** a constant `traceparent` (e.g. hardcoded in a load-test
|
||||
client) re-parents every daemon request into one single trace; generate a
|
||||
fresh header per request.
|
||||
|
||||
## Aliyun Telemetry
|
||||
|
||||
### Manual OTLP Export
|
||||
|
|
|
|||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -28696,6 +28696,7 @@
|
|||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^2.0.1",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.203.0",
|
||||
|
|
|
|||
|
|
@ -181,7 +181,10 @@ import {
|
|||
parseClientIdHeader,
|
||||
safeBody,
|
||||
} from './server/request-helpers.js';
|
||||
import { daemonTelemetryMiddleware } from './server/telemetry.js';
|
||||
import {
|
||||
daemonInboundTraceIdCaptureMiddleware,
|
||||
daemonTelemetryMiddleware,
|
||||
} from './server/telemetry.js';
|
||||
import { installAccessLogMiddleware } from './server/access-log.js';
|
||||
import { setupDeviceFlowRegistry } from './server/device-flow-registry.js';
|
||||
import {
|
||||
|
|
@ -1722,6 +1725,12 @@ export function createServeApp(
|
|||
|
||||
installAccessLogMiddleware(app, daemonLog);
|
||||
|
||||
// Capture the caller trace id BEFORE authenticate / rate limiter / body
|
||||
// parser: those layers short-circuit (401/429/400) before the telemetry
|
||||
// middleware ever runs, and the access log still needs the captured id
|
||||
// to join their log lines (and 404s) with the caller's trace.
|
||||
app.use(daemonInboundTraceIdCaptureMiddleware);
|
||||
|
||||
// Serve the Web Shell static assets (/ and /assets) BEFORE bearerAuth. The
|
||||
// static shell carries no secrets and a browser cannot attach an
|
||||
// Authorization header to a `<script src>` subresource or an address-bar
|
||||
|
|
|
|||
|
|
@ -9,10 +9,23 @@ import { context, ROOT_CONTEXT } from '@opentelemetry/api';
|
|||
import type { Application, RequestHandler } from 'express';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { DaemonLogContext, DaemonLogger } from '../daemon-logger.js';
|
||||
|
||||
const telemetryMocks = vi.hoisted(() => ({
|
||||
getDaemonTelemetryInboundTraceId: vi.fn((): string | undefined => undefined),
|
||||
}));
|
||||
|
||||
// The access log reads the caller trace id through this seam; mocking it
|
||||
// keeps the suite off the real telemetry module (and its core import graph).
|
||||
vi.mock('./telemetry-context.js', () => ({
|
||||
getDaemonTelemetryInboundTraceId:
|
||||
telemetryMocks.getDaemonTelemetryInboundTraceId,
|
||||
}));
|
||||
|
||||
import { installAccessLogMiddleware } from './access-log.js';
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
telemetryMocks.getDaemonTelemetryInboundTraceId.mockReset();
|
||||
});
|
||||
|
||||
function fakeLogger(): DaemonLogger {
|
||||
|
|
@ -115,6 +128,30 @@ describe('installAccessLogMiddleware', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('joins the request log line to the caller trace when telemetry is off', () => {
|
||||
telemetryMocks.getDaemonTelemetryInboundTraceId.mockReturnValueOnce(
|
||||
'3'.repeat(32),
|
||||
);
|
||||
const h = harness();
|
||||
h.begin({ path: '/traced' }).response.emit('finish');
|
||||
|
||||
expect(h.logger.info).toHaveBeenCalledWith(
|
||||
'request completed',
|
||||
expect.objectContaining({ traceId: '3'.repeat(32) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('omits the traceId field when no inbound trace id was captured', () => {
|
||||
const h = harness();
|
||||
h.begin({ path: '/untraced' }).response.emit('finish');
|
||||
|
||||
const context = vi.mocked(h.logger.info).mock.calls[0]?.[1] as
|
||||
| DaemonLogContext
|
||||
| undefined;
|
||||
expect(context).toBeDefined();
|
||||
expect('traceId' in (context ?? {})).toBe(false);
|
||||
});
|
||||
|
||||
it('caps UTF-8 fields, uses the first raw client header, and tolerates clock retreat', () => {
|
||||
const h = harness();
|
||||
const sessionId = '你'.repeat(100);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { performance } from 'node:perf_hooks';
|
|||
import { context, ROOT_CONTEXT } from '@opentelemetry/api';
|
||||
import type { Application } from 'express';
|
||||
import type { DaemonLogContext, DaemonLogger } from '../daemon-logger.js';
|
||||
import { getDaemonTelemetryInboundTraceId } from './telemetry-context.js';
|
||||
|
||||
const SESSION_ID_RE = /\/session\/([^/]+)/;
|
||||
const ACCESS_LOG_BURST = 60;
|
||||
|
|
@ -158,6 +159,11 @@ export function installAccessLogMiddleware(
|
|||
const clientId = rawClientId
|
||||
? truncateUtf8(rawClientId, CLIENT_ID_MAX_BYTES)
|
||||
: undefined;
|
||||
// With telemetry on, the daemon request span stamps the trace prefix
|
||||
// on this line already; this field covers telemetry-off deployments,
|
||||
// where it is the only traceId link between a daemon log line and the
|
||||
// caller that sent the traceparent header.
|
||||
const inboundTraceId = getDaemonTelemetryInboundTraceId(res);
|
||||
const ctx = {
|
||||
route: route.value,
|
||||
...(route.originalBytes
|
||||
|
|
@ -179,6 +185,7 @@ export function installAccessLogMiddleware(
|
|||
: {}),
|
||||
}
|
||||
: {}),
|
||||
...(inboundTraceId ? { traceId: inboundTraceId } : {}),
|
||||
status,
|
||||
durationMs: Math.max(0, Math.round(monotonicNow() - startMs)),
|
||||
};
|
||||
|
|
|
|||
51
packages/cli/src/serve/server/telemetry-context.ts
Normal file
51
packages/cli/src/serve/server/telemetry-context.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2026 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Response } from 'express';
|
||||
|
||||
// This module must stay import-light: the access log (inside the serve
|
||||
// fast-path's pre-listen static closure) reads the captured trace id from
|
||||
// here, so it cannot reach the telemetry middleware's core import graph.
|
||||
export interface DaemonTelemetryResponseContext {
|
||||
workspaceCwd?: string;
|
||||
}
|
||||
|
||||
export const daemonTelemetryResponseContext = Symbol(
|
||||
'daemonTelemetryResponseContext',
|
||||
);
|
||||
|
||||
export type TelemetryResponse = Response & {
|
||||
[daemonTelemetryResponseContext]?: DaemonTelemetryResponseContext;
|
||||
};
|
||||
|
||||
// The captured caller trace id lives under its own symbol: the presence of
|
||||
// the telemetry response context doubles as the opt-in gate for
|
||||
// handler-resolved workspace attribution (see setDaemonTelemetryWorkspace),
|
||||
// so capturing a trace id must never create it — otherwise a caller merely
|
||||
// sending a traceparent header would silently change span attribution.
|
||||
export const daemonInboundTraceIdContext = Symbol(
|
||||
'daemonInboundTraceIdContext',
|
||||
);
|
||||
|
||||
export type InboundTraceIdResponse = Response & {
|
||||
[daemonInboundTraceIdContext]?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The caller trace id captured from a valid inbound `traceparent` header,
|
||||
* in both telemetry modes. The access log reads it so a request's log line
|
||||
* still joins with the caller's logs (or trace backend) with no daemon-side
|
||||
* telemetry at all.
|
||||
*/
|
||||
export function getDaemonTelemetryInboundTraceId(
|
||||
res: Response,
|
||||
): string | undefined {
|
||||
try {
|
||||
return (res as InboundTraceIdResponse)[daemonInboundTraceIdContext];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,11 @@ import { EventEmitter } from 'node:events';
|
|||
import type { NextFunction, Request, Response } from 'express';
|
||||
|
||||
const coreMocks = vi.hoisted(() => ({
|
||||
emitDaemonLog: vi.fn(),
|
||||
extractDaemonHttpTraceContext: vi.fn((): unknown => undefined),
|
||||
extractInboundTraceId: vi.fn((): string | undefined => undefined),
|
||||
hashDaemonWorkspace: vi.fn((workspace: string) => `hash:${workspace}`),
|
||||
isTelemetrySdkInitialized: vi.fn(() => true),
|
||||
recordDaemonError: vi.fn(),
|
||||
recordDaemonHttpRequest: vi.fn(),
|
||||
recordDaemonHttpResponse: vi.fn(),
|
||||
|
|
@ -20,27 +24,44 @@ const coreMocks = vi.hoisted(() => ({
|
|||
),
|
||||
}));
|
||||
|
||||
// The middleware only touches these five core helpers; stub them so the test is
|
||||
// a pure unit on the `recordRequest` seam. `withDaemonRequestSpan` just runs the
|
||||
// wrapped fn (which registers the res listeners and calls next()).
|
||||
// The middleware only touches the core helpers stubbed below (the import
|
||||
// list of telemetry.ts); keep this mock surface in sync with that list so
|
||||
// the test stays a pure unit on the `recordRequest` seam.
|
||||
// `withDaemonRequestSpan` just runs the wrapped fn (which registers the res
|
||||
// listeners and calls next()).
|
||||
vi.mock('@qwen-code/qwen-code-core', () => ({
|
||||
...coreMocks,
|
||||
}));
|
||||
|
||||
import {
|
||||
daemonInboundTraceIdCaptureMiddleware,
|
||||
daemonTelemetryMiddleware,
|
||||
legacySessionTelemetryRoutes,
|
||||
resolveDaemonTelemetryRoute,
|
||||
setDaemonTelemetryWorkspace,
|
||||
} from './telemetry.js';
|
||||
// Deliberately imported from the context module (not telemetry.ts's
|
||||
// re-export): the middleware must write and this getter must read the SAME
|
||||
// symbol — if either side ever declares its own, the readback tests below
|
||||
// fail (the access log reads through this exact seam).
|
||||
import { getDaemonTelemetryInboundTraceId } from './telemetry-context.js';
|
||||
import {
|
||||
getDeferredRuntimeRequestTiming,
|
||||
MAX_CLIENT_ID_LENGTH,
|
||||
setDeferredRuntimeRequestTiming,
|
||||
} from './request-helpers.js';
|
||||
|
||||
function mockReq(method: string, path: string): Request {
|
||||
return { method, path, get: () => undefined } as unknown as Request;
|
||||
function mockReq(
|
||||
method: string,
|
||||
path: string,
|
||||
headers?: Record<string, unknown>,
|
||||
): Request {
|
||||
return {
|
||||
method,
|
||||
path,
|
||||
headers,
|
||||
get: () => undefined,
|
||||
} as unknown as Request;
|
||||
}
|
||||
|
||||
function mockRes(statusCode: number): Response & EventEmitter {
|
||||
|
|
@ -127,6 +148,227 @@ describe('daemonTelemetryMiddleware — recordRequest seam', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('links the request span to an inbound traceparent header when extraction succeeds', () => {
|
||||
const parentContext = { __remoteParent: true };
|
||||
coreMocks.extractDaemonHttpTraceContext.mockReturnValueOnce(parentContext);
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
res.emit('finish');
|
||||
|
||||
expect(coreMocks.extractDaemonHttpTraceContext).toHaveBeenCalledWith({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
});
|
||||
expect(coreMocks.withDaemonRequestSpan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ parentContext }),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('omits the parent context when no valid traceparent header is present', () => {
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {}),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
res.emit('finish');
|
||||
|
||||
expect(coreMocks.extractDaemonHttpTraceContext).toHaveBeenCalledWith({});
|
||||
const options = coreMocks.withDaemonRequestSpan.mock
|
||||
.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect('parentContext' in options).toBe(false);
|
||||
expect(coreMocks.emitDaemonLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips span-context extraction when the telemetry SDK is not initialized', () => {
|
||||
coreMocks.isTelemetrySdkInitialized.mockReturnValueOnce(false);
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
res.emit('finish');
|
||||
|
||||
// Telemetry off: no span parent and no breadcrumb. The access-log
|
||||
// trace id capture lives in daemonInboundTraceIdCaptureMiddleware, not
|
||||
// here.
|
||||
expect(coreMocks.extractDaemonHttpTraceContext).not.toHaveBeenCalled();
|
||||
expect(coreMocks.extractInboundTraceId).not.toHaveBeenCalled();
|
||||
const options = coreMocks.withDaemonRequestSpan.mock
|
||||
.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect('parentContext' in options).toBe(false);
|
||||
expect(coreMocks.emitDaemonLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs at debug severity when a present traceparent header is rejected', () => {
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', { traceparent: 'junk-header' }),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
res.emit('finish');
|
||||
|
||||
expect(coreMocks.extractDaemonHttpTraceContext).toHaveBeenCalledWith({
|
||||
traceparent: 'junk-header',
|
||||
});
|
||||
expect(coreMocks.emitDaemonLog).toHaveBeenCalledWith(
|
||||
'Rejected invalid inbound traceparent header.',
|
||||
{
|
||||
'http.route': 'GET /daemon/status',
|
||||
'http.request.header.traceparent': 'junk-header',
|
||||
},
|
||||
{
|
||||
eventName: 'qwen-code.daemon.traceparent.invalid',
|
||||
severityNumber: 5,
|
||||
},
|
||||
);
|
||||
const options = coreMocks.withDaemonRequestSpan.mock
|
||||
.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect('parentContext' in options).toBe(false);
|
||||
});
|
||||
|
||||
it('truncates the rejected traceparent header value in the breadcrumb', () => {
|
||||
const longHeader = 'x'.repeat(300);
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', { traceparent: longHeader }),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
res.emit('finish');
|
||||
|
||||
expect(coreMocks.emitDaemonLog).toHaveBeenCalledWith(
|
||||
'Rejected invalid inbound traceparent header.',
|
||||
expect.objectContaining({
|
||||
'http.request.header.traceparent': 'x'.repeat(128),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
eventName: 'qwen-code.daemon.traceparent.invalid',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('neutralizes control characters in the rejected traceparent breadcrumb', () => {
|
||||
const forgedHeader = 'junk\u0000\u001bheader\nvalue';
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', { traceparent: forgedHeader }),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
res.emit('finish');
|
||||
|
||||
// NUL and ESC collapse to spaces and the newline renders visibly, so a
|
||||
// crafted header cannot forge log line structure or inject ANSI codes.
|
||||
expect(coreMocks.emitDaemonLog).toHaveBeenCalledWith(
|
||||
'Rejected invalid inbound traceparent header.',
|
||||
expect.objectContaining({
|
||||
'http.request.header.traceparent': 'junk header\\nvalue',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
eventName: 'qwen-code.daemon.traceparent.invalid',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rate-limits the rejected traceparent breadcrumb', () => {
|
||||
const mw = daemonTelemetryMiddleware(() => '/ws');
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
const res = mockRes(200);
|
||||
mw(
|
||||
mockReq('GET', '/daemon/status', { traceparent: 'junk-header' }),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
res.emit('finish');
|
||||
}
|
||||
// The per-instance burst budget (60, +2/s refill) caps a flood of
|
||||
// invalid headers; the loop runs well under a second, so refill is
|
||||
// negligible.
|
||||
expect(coreMocks.emitDaemonLog.mock.calls.length).toBeGreaterThan(0);
|
||||
expect(coreMocks.emitDaemonLog.mock.calls.length).toBeLessThan(200);
|
||||
});
|
||||
|
||||
it('fails closed and settles the request when header extraction throws', () => {
|
||||
coreMocks.extractDaemonHttpTraceContext.mockImplementationOnce(() => {
|
||||
throw new Error('extract failed');
|
||||
});
|
||||
const res = mockRes(200);
|
||||
const next = vi.fn() as unknown as NextFunction;
|
||||
|
||||
expect(() =>
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
res,
|
||||
next,
|
||||
),
|
||||
).not.toThrow();
|
||||
res.emit('finish');
|
||||
|
||||
// The request still settles normally through the telemetry pipeline.
|
||||
expect(coreMocks.recordDaemonHttpRequest).toHaveBeenCalledTimes(1);
|
||||
const options = coreMocks.withDaemonRequestSpan.mock
|
||||
.calls[0]?.[0] as Record<string, unknown>;
|
||||
expect('parentContext' in options).toBe(false);
|
||||
// Fail-closed extraction counts as rejected, so the breadcrumb still
|
||||
// fires for the present-but-unparsed header.
|
||||
expect(coreMocks.emitDaemonLog).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not log when extraction succeeds, no header, or an array header is sent', () => {
|
||||
const parentContext = { __remoteParent: true };
|
||||
coreMocks.extractDaemonHttpTraceContext.mockReturnValueOnce(parentContext);
|
||||
const resA = mockRes(200);
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
resA,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
resA.emit('finish');
|
||||
|
||||
const resB = mockRes(200);
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {}),
|
||||
resB,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
resB.emit('finish');
|
||||
|
||||
// Array header values stay fail-closed (rejected) but are not the
|
||||
// "present-but-invalid string" breadcrumb case.
|
||||
const resC = mockRes(200);
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {
|
||||
traceparent: [`00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`],
|
||||
}),
|
||||
resC,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
resC.emit('finish');
|
||||
|
||||
expect(coreMocks.emitDaemonLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires exactly once even if both finish and close emit', () => {
|
||||
const recordRequest = vi.fn();
|
||||
const mw = daemonTelemetryMiddleware(() => '/ws', recordRequest);
|
||||
|
|
@ -937,3 +1179,116 @@ describe('legacy session telemetry route catalog', () => {
|
|||
expect(resolveDaemonTelemetryRoute(mockReq(method, path))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('daemonInboundTraceIdCaptureMiddleware', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('captures the caller trace id from a valid traceparent header', () => {
|
||||
coreMocks.extractInboundTraceId.mockReturnValueOnce('3'.repeat(32));
|
||||
const res = mockRes(200);
|
||||
const next = vi.fn() as unknown as NextFunction;
|
||||
|
||||
daemonInboundTraceIdCaptureMiddleware(
|
||||
mockReq('GET', '/session/abc/prompt', {
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
res,
|
||||
next,
|
||||
);
|
||||
|
||||
expect(coreMocks.extractInboundTraceId).toHaveBeenCalledWith({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
});
|
||||
expect(getDaemonTelemetryInboundTraceId(res)).toBe('3'.repeat(32));
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('leaves the response untouched when no valid header parses', () => {
|
||||
const res = mockRes(200);
|
||||
const next = vi.fn() as unknown as NextFunction;
|
||||
|
||||
daemonInboundTraceIdCaptureMiddleware(
|
||||
mockReq('GET', '/session/abc/prompt', { traceparent: 'junk-header' }),
|
||||
res,
|
||||
next,
|
||||
);
|
||||
|
||||
expect(getDaemonTelemetryInboundTraceId(res)).toBeUndefined();
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('still calls next when extraction throws', () => {
|
||||
coreMocks.extractInboundTraceId.mockImplementationOnce(() => {
|
||||
throw new Error('extract failed');
|
||||
});
|
||||
const res = mockRes(200);
|
||||
const next = vi.fn() as unknown as NextFunction;
|
||||
|
||||
expect(() =>
|
||||
daemonInboundTraceIdCaptureMiddleware(
|
||||
mockReq('GET', '/anything', {}),
|
||||
res,
|
||||
next,
|
||||
),
|
||||
).not.toThrow();
|
||||
|
||||
expect(getDaemonTelemetryInboundTraceId(res)).toBeUndefined();
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('capturing the trace id does not open the workspace attribution gate', () => {
|
||||
// Regression: capture used to create the telemetry response context,
|
||||
// whose mere presence is the opt-in for handler-resolved workspace
|
||||
// attribution — so a caller merely sending a traceparent header changed
|
||||
// the span's workspace.hash. Capture now stores the id under its own
|
||||
// symbol, and setDaemonTelemetryWorkspace stays a no-op here.
|
||||
coreMocks.extractInboundTraceId.mockReturnValueOnce('3'.repeat(32));
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonInboundTraceIdCaptureMiddleware(
|
||||
mockReq('GET', '/daemon/status', {
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('GET', '/daemon/status', {}),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
setDaemonTelemetryWorkspace(res, '/ws');
|
||||
res.emit('finish');
|
||||
|
||||
expect(coreMocks.spanSetAttribute).not.toHaveBeenCalledWith(
|
||||
'qwen-code.workspace.hash',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the captured trace id when the telemetry middleware initializes the workspace context', () => {
|
||||
coreMocks.extractInboundTraceId.mockReturnValueOnce('3'.repeat(32));
|
||||
const res = mockRes(200);
|
||||
|
||||
daemonInboundTraceIdCaptureMiddleware(
|
||||
mockReq('POST', '/session', {
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
daemonTelemetryMiddleware(() => '/ws')(
|
||||
mockReq('POST', '/session', {}),
|
||||
res,
|
||||
vi.fn() as unknown as NextFunction,
|
||||
);
|
||||
|
||||
// The handler_resolved branch initializes the workspace context; the
|
||||
// captured trace id lives under its own symbol and must survive.
|
||||
expect(getDaemonTelemetryInboundTraceId(res)).toBe('3'.repeat(32));
|
||||
|
||||
res.emit('finish');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,13 +4,19 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import {
|
||||
emitDaemonLog,
|
||||
extractDaemonHttpTraceContext,
|
||||
extractInboundTraceId,
|
||||
hashDaemonWorkspace,
|
||||
isTelemetrySdkInitialized,
|
||||
recordDaemonError,
|
||||
recordDaemonHttpRequest,
|
||||
recordDaemonHttpResponse,
|
||||
withDaemonRequestSpan,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { sanitizeLogText } from '@qwen-code/channel-base';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import {
|
||||
CLIENT_ID_HEADER,
|
||||
|
|
@ -18,6 +24,14 @@ import {
|
|||
getDeferredRuntimeRequestTiming,
|
||||
MAX_CLIENT_ID_LENGTH,
|
||||
} from './request-helpers.js';
|
||||
import {
|
||||
daemonInboundTraceIdContext,
|
||||
daemonTelemetryResponseContext,
|
||||
type InboundTraceIdResponse,
|
||||
type TelemetryResponse,
|
||||
} from './telemetry-context.js';
|
||||
|
||||
export { getDaemonTelemetryInboundTraceId } from './telemetry-context.js';
|
||||
|
||||
type LegacySessionTelemetryAttribution = 'handler_resolved' | 'pre_resolved';
|
||||
|
||||
|
|
@ -392,16 +406,6 @@ interface ResolvedDaemonTelemetryRoute {
|
|||
attribution?: LegacySessionTelemetryAttribution;
|
||||
}
|
||||
|
||||
interface DaemonTelemetryResponseContext {
|
||||
workspaceCwd?: string;
|
||||
}
|
||||
|
||||
const daemonTelemetryResponseContext = Symbol('daemonTelemetryResponseContext');
|
||||
|
||||
type TelemetryResponse = Response & {
|
||||
[daemonTelemetryResponseContext]?: DaemonTelemetryResponseContext;
|
||||
};
|
||||
|
||||
function decodePathSegment(value: string): string {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
|
|
@ -707,6 +711,39 @@ export function resolveDaemonTelemetryRoute(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
// The rejected-traceparent breadcrumb is rate-limited like the access
|
||||
// log: an attacker (or a broken client) can send invalid traceparent headers
|
||||
// on every request, and an unbounded DEBUG emit per request would let
|
||||
// crafted traffic flood the daemon log.
|
||||
const TRACEPARENT_BREADCRUMB_BURST = 60;
|
||||
const TRACEPARENT_BREADCRUMB_REFILL_PER_SECOND = 2;
|
||||
|
||||
/**
|
||||
* Capture the caller trace id from a valid inbound `traceparent` header in
|
||||
* both telemetry modes. Mounted before auth, rate limiting, and body
|
||||
* parsing so the access log line of a request short-circuited at those
|
||||
* layers (401/429/400) — or one matching no route (404) — still joins the
|
||||
* caller's trace. The id is stored under its own symbol (see
|
||||
* telemetry-context.ts): creating the telemetry response context would
|
||||
* flip the handler-resolved workspace attribution gate.
|
||||
*/
|
||||
export function daemonInboundTraceIdCaptureMiddleware(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): void {
|
||||
try {
|
||||
const inboundTraceId = extractInboundTraceId(req.headers);
|
||||
if (inboundTraceId !== undefined) {
|
||||
(res as InboundTraceIdResponse)[daemonInboundTraceIdContext] =
|
||||
inboundTraceId;
|
||||
}
|
||||
} catch {
|
||||
// Telemetry must not affect request handling.
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export function daemonTelemetryMiddleware(
|
||||
resolveWorkspaceCwd: (req: Request) => string | undefined,
|
||||
// Optional in-process sink for the Daemon Status dashboard's time-series
|
||||
|
|
@ -718,6 +755,25 @@ export function daemonTelemetryMiddleware(
|
|||
recordRequest?: (durationMs: number, statusCode: number) => void,
|
||||
): (req: Request, res: Response, next: NextFunction) => void {
|
||||
const workspaceHashByCwd = new Map<string, string>();
|
||||
// The rejected-traceparent breadcrumb is rate-limited per middleware
|
||||
// instance like the access log: an attacker (or a broken client) can send
|
||||
// invalid traceparent headers on every request, and an unbounded DEBUG
|
||||
// emit per request would let crafted traffic flood the daemon log.
|
||||
let breadcrumbTokens = TRACEPARENT_BREADCRUMB_BURST;
|
||||
let breadcrumbRefillBaseline = performance.now();
|
||||
const acquireBreadcrumbToken = (): boolean => {
|
||||
const now = Math.max(performance.now(), breadcrumbRefillBaseline);
|
||||
breadcrumbTokens = Math.min(
|
||||
TRACEPARENT_BREADCRUMB_BURST,
|
||||
breadcrumbTokens +
|
||||
((now - breadcrumbRefillBaseline) / 1_000) *
|
||||
TRACEPARENT_BREADCRUMB_REFILL_PER_SECOND,
|
||||
);
|
||||
breadcrumbRefillBaseline = now;
|
||||
if (breadcrumbTokens < 1) return false;
|
||||
breadcrumbTokens -= 1;
|
||||
return true;
|
||||
};
|
||||
const resolveWorkspaceHash = (workspaceCwd: string): string => {
|
||||
const existing = workspaceHashByCwd.get(workspaceCwd);
|
||||
if (existing !== undefined) return existing;
|
||||
|
|
@ -754,10 +810,60 @@ export function daemonTelemetryMiddleware(
|
|||
: undefined;
|
||||
const deferredRuntime = getDeferredRuntimeRequestTiming(req);
|
||||
const startMs = deferredRuntime?.startedAt.getTime() ?? Date.now();
|
||||
// With telemetry on, extract the full W3C context (span parent + forced
|
||||
// sampling). The camelCase `traceId` access-log field is captured by
|
||||
// daemonInboundTraceIdCaptureMiddleware (mounted pre-auth) in both
|
||||
// modes, so one log query shape works for every deployment; with
|
||||
// telemetry on the span-derived snake_case prefix carries the same id
|
||||
// redundantly, while telemetry-off logs have it as their only carrier —
|
||||
// no telemetry config, no trace backend needed.
|
||||
let parentContext: ReturnType<typeof extractDaemonHttpTraceContext>;
|
||||
if (isTelemetrySdkInitialized()) {
|
||||
try {
|
||||
parentContext = extractDaemonHttpTraceContext(req.headers);
|
||||
} catch {
|
||||
// Telemetry must not affect request handling.
|
||||
parentContext = undefined;
|
||||
}
|
||||
try {
|
||||
const inboundTraceparent = req.headers?.['traceparent'];
|
||||
if (
|
||||
!parentContext &&
|
||||
typeof inboundTraceparent === 'string' &&
|
||||
inboundTraceparent.length > 0 &&
|
||||
acquireBreadcrumbToken()
|
||||
) {
|
||||
// Leave a breadcrumb when a present-but-invalid header is rejected,
|
||||
// so a broken cross-service join is diagnosable from daemon logs
|
||||
// alone instead of requiring a request replay. Traceparent carries
|
||||
// only trace-id/span-id/flags — no user content — so recording the
|
||||
// rejected value is privacy-safe and shows *why* the join failed.
|
||||
// sanitizeLogText truncates and also neutralizes control characters
|
||||
// so a crafted header cannot forge log structure.
|
||||
emitDaemonLog(
|
||||
'Rejected invalid inbound traceparent header.',
|
||||
{
|
||||
'http.route': route.route,
|
||||
'http.request.header.traceparent': sanitizeLogText(
|
||||
inboundTraceparent,
|
||||
128,
|
||||
),
|
||||
},
|
||||
{
|
||||
eventName: 'qwen-code.daemon.traceparent.invalid',
|
||||
severityNumber: 5, // SeverityNumber.DEBUG
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Telemetry must not affect request handling (covers the log emit
|
||||
// AND the header sanitization above it).
|
||||
}
|
||||
}
|
||||
const telemetryRes = res as TelemetryResponse;
|
||||
if (route.attribution === 'handler_resolved') {
|
||||
try {
|
||||
telemetryRes[daemonTelemetryResponseContext] = {};
|
||||
telemetryRes[daemonTelemetryResponseContext] ??= {};
|
||||
} catch {
|
||||
// Telemetry must not affect request handling.
|
||||
}
|
||||
|
|
@ -772,6 +878,7 @@ export function daemonTelemetryMiddleware(
|
|||
? { permissionRequestId: route.permissionRequestId }
|
||||
: {}),
|
||||
...(clientId ? { clientId } : {}),
|
||||
...(parentContext ? { parentContext } : {}),
|
||||
...(deferredRuntime?.waitMs !== undefined
|
||||
? {
|
||||
startTime: deferredRuntime.startedAt,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "^2.0.1",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.203.0",
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ import {
|
|||
ROOT_CONTEXT,
|
||||
SpanStatusCode,
|
||||
trace,
|
||||
TraceFlags,
|
||||
type Context,
|
||||
type Span,
|
||||
type Tracer,
|
||||
} from '@opentelemetry/api';
|
||||
import { W3CTraceContextPropagator } from '@opentelemetry/core';
|
||||
|
||||
vi.mock('./sdk.js', () => ({
|
||||
isTelemetrySdkInitialized: () => true,
|
||||
|
|
@ -25,18 +28,36 @@ import {
|
|||
addDaemonRequestAttribute,
|
||||
captureDaemonTelemetryContext,
|
||||
createDaemonBridgeTelemetry,
|
||||
extractDaemonHttpTraceContext,
|
||||
extractDaemonTraceContext,
|
||||
extractInboundTraceId,
|
||||
hashDaemonWorkspace,
|
||||
injectDaemonTraceContext,
|
||||
runWithDaemonTelemetryContext,
|
||||
setDaemonFallbackPropagator,
|
||||
withDaemonSpan,
|
||||
withDaemonRequestSpan,
|
||||
type DaemonRequestSpanOptions,
|
||||
} from './daemon-tracing.js';
|
||||
import { getSessionIdFromContext } from './session-context.js';
|
||||
|
||||
// Mirror the post-init state: `sdk-impl.ts` injects the W3C fallback
|
||||
// propagator once the lazy SDK chunk assembles successfully (this suite
|
||||
// mocks `isTelemetrySdkInitialized` as true above, so the holder must be
|
||||
// populated the same way the real SDK would).
|
||||
setDaemonFallbackPropagator(new W3CTraceContextPropagator());
|
||||
|
||||
// vitest transpiles without type-checking: this compile-time assertion keeps
|
||||
// the optional parentContext field from silently disappearing (only `tsc`
|
||||
// would notice), while the runtime suite guards the behavior it enables.
|
||||
type DaemonRequestSpanOptionsExposesParentContext =
|
||||
DaemonRequestSpanOptions extends { parentContext?: Context } ? true : false;
|
||||
const daemonRequestSpanOptionsExposesParentContext: DaemonRequestSpanOptionsExposesParentContext = true;
|
||||
|
||||
describe('daemon-tracing', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('injects traceparent from the active span without the global propagator', () => {
|
||||
|
|
@ -156,6 +177,226 @@ describe('daemon-tracing', () => {
|
|||
expect(extracted).toBeDefined();
|
||||
expect(trace.getSpanContext(extracted!)?.traceId).toBe(traceId);
|
||||
expect(trace.getSpanContext(extracted!)?.spanId).toBe(spanId);
|
||||
expect(trace.getSpanContext(extracted!)?.traceState?.get('vendor')).toBe(
|
||||
'value',
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts trace context from inbound HTTP traceparent headers', () => {
|
||||
const traceId = '3'.repeat(32);
|
||||
const spanId = '4'.repeat(16);
|
||||
const extracted = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${traceId}-${spanId}-01`,
|
||||
});
|
||||
|
||||
expect(extracted).toBeDefined();
|
||||
expect(trace.getSpanContext(extracted!)?.traceId).toBe(traceId);
|
||||
expect(trace.getSpanContext(extracted!)?.spanId).toBe(spanId);
|
||||
expect(trace.getSpanContext(extracted!)?.isRemote).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid inbound HTTP traceparent headers', () => {
|
||||
expect(extractDaemonHttpTraceContext(undefined)).toBeUndefined();
|
||||
expect(extractDaemonHttpTraceContext({})).toBeUndefined();
|
||||
expect(
|
||||
extractDaemonHttpTraceContext({ traceparent: 'not-a-traceparent' }),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'0'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
extractDaemonHttpTraceContext({
|
||||
traceparent: [`00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects traceparent headers the W3C propagator rejects', () => {
|
||||
// version ff is reserved for future use and always invalid
|
||||
expect(
|
||||
extractDaemonHttpTraceContext({
|
||||
traceparent: `ff-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
// version 00 must not carry the optional future-extension field
|
||||
expect(
|
||||
extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01-extra`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('extracts only the trace id for the telemetry-off log join', () => {
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBe('3'.repeat(32));
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: `01-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBe('3'.repeat(32));
|
||||
// Acceptance mirrors the vendored W3C propagator: a single optional
|
||||
// whitespace on either edge, and trailing extension fields above
|
||||
// version 00 — so a header joins on both paths or neither.
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: ` 00-${'3'.repeat(32)}-${'4'.repeat(16)}-01 `,
|
||||
}),
|
||||
).toBe('3'.repeat(32));
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: `01-${'3'.repeat(32)}-${'4'.repeat(16)}-01-future-field`,
|
||||
}),
|
||||
).toBe('3'.repeat(32));
|
||||
});
|
||||
|
||||
it('rejects invalid headers on the telemetry-off trace id path', () => {
|
||||
expect(extractInboundTraceId(undefined)).toBeUndefined();
|
||||
expect(extractInboundTraceId({})).toBeUndefined();
|
||||
expect(
|
||||
extractInboundTraceId({ traceparent: 'not-a-traceparent' }),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: `00-${'0'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'0'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: `ff-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
// version 00 must not carry extension fields — same as the propagator
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01-extra`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
extractInboundTraceId({
|
||||
traceparent: [`00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('yields no parent context until the SDK chunk injects the fallback propagator', async () => {
|
||||
// Fresh module registry: daemon-tracing without the sdk-impl injection.
|
||||
// The global propagator stays a no-op in tests (nothing registers one),
|
||||
// so a valid traceparent resolves to nothing while the fallback holder
|
||||
// is empty — the telemetry-off / SDK-chunk-not-loaded state.
|
||||
vi.resetModules();
|
||||
const fresh = await import('./daemon-tracing.js');
|
||||
|
||||
expect(
|
||||
fresh.extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
// The telemetry-off trace id path needs no propagator — that is the
|
||||
// whole point of the plain regex parse.
|
||||
expect(
|
||||
fresh.extractInboundTraceId({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
}),
|
||||
).toBe('3'.repeat(32));
|
||||
expect(
|
||||
fresh.extractDaemonTraceContext({
|
||||
_meta: {
|
||||
[DAEMON_TRACEPARENT_META_KEY]: `00-${'1'.repeat(32)}-${'2'.repeat(16)}-01`,
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts future traceparent versions like the registered W3C propagator', () => {
|
||||
const traceId = '3'.repeat(32);
|
||||
const spanId = '4'.repeat(16);
|
||||
const extracted = extractDaemonHttpTraceContext({
|
||||
traceparent: `01-${traceId}-${spanId}-01`,
|
||||
});
|
||||
|
||||
expect(trace.getSpanContext(extracted!)?.traceId).toBe(traceId);
|
||||
expect(trace.getSpanContext(extracted!)?.spanId).toBe(spanId);
|
||||
expect(trace.getSpanContext(extracted!)?.isRemote).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves inbound tracestate on the extracted HTTP context', () => {
|
||||
const extracted = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
tracestate: 'vendor=value',
|
||||
});
|
||||
|
||||
expect(trace.getSpanContext(extracted!)?.traceState?.get('vendor')).toBe(
|
||||
'value',
|
||||
);
|
||||
});
|
||||
|
||||
it('forces the sampled flag on inbound HTTP parents under the default sampler', () => {
|
||||
vi.stubEnv('OTEL_TRACES_SAMPLER', '');
|
||||
const forced = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-00`,
|
||||
});
|
||||
const forcedContext = trace.getSpanContext(forced!);
|
||||
expect(forcedContext).toBeDefined();
|
||||
expect((forcedContext?.traceFlags ?? 0) & TraceFlags.SAMPLED).toBe(
|
||||
TraceFlags.SAMPLED,
|
||||
);
|
||||
expect(forcedContext?.isRemote).toBe(true);
|
||||
// already-sampled parents keep their flags
|
||||
const sampled = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'5'.repeat(32)}-${'6'.repeat(16)}-01`,
|
||||
});
|
||||
const sampledFlags = trace.getSpanContext(sampled!)?.traceFlags ?? 0;
|
||||
expect(sampledFlags & TraceFlags.SAMPLED).toBe(TraceFlags.SAMPLED);
|
||||
});
|
||||
|
||||
it('keeps the caller flags when the sampler config opts out of forcing', () => {
|
||||
vi.stubEnv('OTEL_TRACES_SAMPLER', 'parentbased_always_off');
|
||||
const alwaysOff = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-00`,
|
||||
});
|
||||
expect(trace.getSpanContext(alwaysOff!)?.traceFlags).toBe(0);
|
||||
|
||||
vi.stubEnv('OTEL_TRACES_SAMPLER', 'traceidratio');
|
||||
const ratio = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-00`,
|
||||
});
|
||||
expect(trace.getSpanContext(ratio!)?.traceFlags).toBe(0);
|
||||
});
|
||||
|
||||
it('forces the sampled flag on _meta parents under the default sampler', () => {
|
||||
vi.stubEnv('OTEL_TRACES_SAMPLER', '');
|
||||
const extracted = extractDaemonTraceContext({
|
||||
_meta: {
|
||||
[DAEMON_TRACEPARENT_META_KEY]: `00-${'1'.repeat(32)}-${'2'.repeat(16)}-00`,
|
||||
},
|
||||
});
|
||||
expect(
|
||||
(trace.getSpanContext(extracted!)?.traceFlags ?? 0) & TraceFlags.SAMPLED,
|
||||
).toBe(TraceFlags.SAMPLED);
|
||||
});
|
||||
|
||||
it('keeps the caller flags on the _meta path when the sampler opts out', () => {
|
||||
vi.stubEnv('OTEL_TRACES_SAMPLER', 'parentbased_always_off');
|
||||
const extracted = extractDaemonTraceContext({
|
||||
_meta: {
|
||||
[DAEMON_TRACEPARENT_META_KEY]: `00-${'1'.repeat(32)}-${'2'.repeat(16)}-00`,
|
||||
},
|
||||
});
|
||||
expect(trace.getSpanContext(extracted!)?.traceFlags).toBe(0);
|
||||
});
|
||||
|
||||
it('keeps parentContext on DaemonRequestSpanOptions (type-level guard)', () => {
|
||||
expect(daemonRequestSpanOptionsExposesParentContext).toBe(true);
|
||||
});
|
||||
|
||||
it('starts a daemon span under an explicit remote parent context', async () => {
|
||||
|
|
@ -198,6 +439,53 @@ describe('daemon-tracing', () => {
|
|||
expect(span.end).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('starts a daemon request span under an extracted HTTP parent context', async () => {
|
||||
const parentContext = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'5'.repeat(32)}-${'6'.repeat(16)}-01`,
|
||||
});
|
||||
const span = {
|
||||
setStatus: vi.fn(),
|
||||
end: vi.fn(),
|
||||
setAttribute: vi.fn(),
|
||||
setAttributes: vi.fn(),
|
||||
recordException: vi.fn(),
|
||||
} as unknown as Span;
|
||||
const startActiveSpan = vi.fn(
|
||||
async (
|
||||
_name: string,
|
||||
_options: unknown,
|
||||
_parent: unknown,
|
||||
fn: (span: Span) => Promise<string>,
|
||||
) => await fn(span),
|
||||
);
|
||||
vi.spyOn(trace, 'getTracer').mockReturnValue({
|
||||
startActiveSpan,
|
||||
} as unknown as Tracer);
|
||||
|
||||
await expect(
|
||||
withDaemonRequestSpan(
|
||||
{
|
||||
method: 'GET',
|
||||
route: 'GET /daemon/status',
|
||||
parentContext,
|
||||
},
|
||||
async () => 'ok',
|
||||
),
|
||||
).resolves.toBe('ok');
|
||||
|
||||
expect(startActiveSpan).toHaveBeenCalledWith(
|
||||
'qwen-code.daemon.request',
|
||||
expect.objectContaining({
|
||||
attributes: expect.objectContaining({
|
||||
'http.request.method': 'GET',
|
||||
}),
|
||||
}),
|
||||
parentContext!,
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(span.end).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('binds an explicit daemon session to the callback context', async () => {
|
||||
const span = {
|
||||
setStatus: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -7,17 +7,21 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
context as otelContext,
|
||||
defaultTextMapGetter,
|
||||
propagation,
|
||||
ROOT_CONTEXT,
|
||||
SpanKind,
|
||||
SpanStatusCode,
|
||||
trace,
|
||||
TraceFlags,
|
||||
type Context,
|
||||
type Span,
|
||||
type TextMapPropagator,
|
||||
} from '@opentelemetry/api';
|
||||
import { logs, type LogAttributes } from '@opentelemetry/api-logs';
|
||||
import { SERVICE_NAME } from './constants.js';
|
||||
import { isTelemetrySdkInitialized } from './sdk.js';
|
||||
import { shouldForceSampled } from './tracer.js';
|
||||
import { truncateSpanError } from './session-tracing.js';
|
||||
import {
|
||||
formatTraceparent,
|
||||
|
|
@ -48,6 +52,7 @@ export interface DaemonRequestSpanOptions {
|
|||
sessionId?: string;
|
||||
clientId?: string;
|
||||
permissionRequestId?: string;
|
||||
parentContext?: Context;
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
|
|
@ -60,9 +65,6 @@ function errorType(error: unknown): string {
|
|||
return typeof error;
|
||||
}
|
||||
|
||||
const INVALID_TRACE_ID = '0'.repeat(32);
|
||||
const INVALID_SPAN_ID = '0'.repeat(16);
|
||||
|
||||
function stripReservedTraceMeta(meta: unknown): Record<string, unknown> {
|
||||
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return {};
|
||||
const record = meta as Record<string, unknown>;
|
||||
|
|
@ -164,7 +166,11 @@ export async function withDaemonRequestSpan<T>(
|
|||
: {}),
|
||||
},
|
||||
fn,
|
||||
{ autoOkOnSuccess: false, startTime: options.startTime },
|
||||
{
|
||||
autoOkOnSuccess: false,
|
||||
startTime: options.startTime,
|
||||
parentContext: options.parentContext,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -291,6 +297,74 @@ export function injectDaemonTraceContext<T extends object>(request: T): T {
|
|||
};
|
||||
}
|
||||
|
||||
// Fallback propagator for `contextFromTraceparentValues` below. The global
|
||||
// propagator stays a no-op unless the daemon SDK registered one (opt-in
|
||||
// outbound propagation), so extraction needs a direct W3C instance to apply
|
||||
// the same acceptance rules — future traceparent versions, tracestate,
|
||||
// all-zero ids — as the registered path. The instance is injected by the
|
||||
// lazy SDK chunk (`sdk-impl.ts`) instead of being constructed here: this
|
||||
// module sits on every CLI launch's static startup graph, and
|
||||
// @opentelemetry/core is a CJS barrel that tree-shaking cannot slim down
|
||||
// (~65 KB per launch even with telemetry off). Until the SDK initializes,
|
||||
// the holder stays empty and extraction returns no parent context — the
|
||||
// telemetry-off state, with no OTel side effects.
|
||||
let daemonFallbackPropagator: TextMapPropagator | undefined;
|
||||
|
||||
/**
|
||||
* Install the W3C fallback propagator used by inbound traceparent
|
||||
* extraction. Called by the dynamically imported SDK chunk (`sdk-impl.ts`)
|
||||
* once telemetry is actually enabled, so @opentelemetry/core never enters
|
||||
* the static startup graph (the `TextMapPropagator` type import above costs
|
||||
* nothing at runtime).
|
||||
*/
|
||||
export function setDaemonFallbackPropagator(
|
||||
propagator: TextMapPropagator,
|
||||
): void {
|
||||
daemonFallbackPropagator = propagator;
|
||||
}
|
||||
|
||||
function contextFromTraceparentValues(
|
||||
traceparent: string,
|
||||
tracestate: unknown,
|
||||
): Context | undefined {
|
||||
const carrier: Record<string, string> = { traceparent };
|
||||
if (typeof tracestate === 'string' && tracestate.length > 0) {
|
||||
carrier['tracestate'] = tracestate;
|
||||
}
|
||||
const extracted = propagation.extract(ROOT_CONTEXT, carrier);
|
||||
if (trace.getSpanContext(extracted)) return extracted;
|
||||
if (!daemonFallbackPropagator) return undefined;
|
||||
const fallback = daemonFallbackPropagator.extract(
|
||||
ROOT_CONTEXT,
|
||||
carrier,
|
||||
defaultTextMapGetter,
|
||||
);
|
||||
return trace.getSpanContext(fallback) ? fallback : undefined;
|
||||
}
|
||||
|
||||
// A remote caller's `sampled=0` is head-based ratio sampling on their
|
||||
// side, not a request to drop daemon telemetry. Under the default
|
||||
// parentbased_always_on sampler a remote unsampled parent delegates to
|
||||
// AlwaysOff, silently deleting the request span, everything under it, and —
|
||||
// via _meta forwarding — the session subprocess spans. Reuse the
|
||||
// session-root decision matrix: parentbased defaults and always_on force
|
||||
// SAMPLED; parentbased_always_off honors the operator's opt-out;
|
||||
// non-parentbased samplers decide per span.
|
||||
function forceSampledUnderSampler(
|
||||
extracted: Context | undefined,
|
||||
): Context | undefined {
|
||||
if (!extracted || !shouldForceSampled()) return extracted;
|
||||
const spanContext = trace.getSpanContext(extracted);
|
||||
if (!spanContext) return extracted;
|
||||
return trace.setSpan(
|
||||
extracted,
|
||||
trace.wrapSpanContext({
|
||||
...spanContext,
|
||||
traceFlags: spanContext.traceFlags | TraceFlags.SAMPLED,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function extractDaemonTraceContext(
|
||||
source: unknown,
|
||||
): Context | undefined {
|
||||
|
|
@ -303,37 +377,67 @@ export function extractDaemonTraceContext(
|
|||
if (typeof traceparent !== 'string' || traceparent.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const carrier: Record<string, string> = { traceparent };
|
||||
const tracestate = record[DAEMON_TRACESTATE_META_KEY];
|
||||
if (typeof tracestate === 'string' && tracestate.length > 0) {
|
||||
carrier['tracestate'] = tracestate;
|
||||
}
|
||||
const extracted = propagation.extract(ROOT_CONTEXT, carrier);
|
||||
if (trace.getSpanContext(extracted)) return extracted;
|
||||
// The _meta path is reachable from two kinds of callers: the in-process
|
||||
// bridge (injectDaemonTraceContext, values already SAMPLED so forcing is a
|
||||
// no-op) and direct ACP clients whose request _meta is external input just
|
||||
// like the HTTP header — so both edges get the same sampled protection.
|
||||
return forceSampledUnderSampler(
|
||||
contextFromTraceparentValues(
|
||||
traceparent,
|
||||
record[DAEMON_TRACESTATE_META_KEY],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const parts = traceparent.split('-');
|
||||
const traceId = parts[1];
|
||||
const spanId = parts[2];
|
||||
const flags = parts[3];
|
||||
if (
|
||||
parts[0] !== '00' ||
|
||||
!traceId?.match(/^[0-9a-f]{32}$/) ||
|
||||
!spanId?.match(/^[0-9a-f]{16}$/) ||
|
||||
!flags?.match(/^[0-9a-f]{2}$/) ||
|
||||
traceId === INVALID_TRACE_ID ||
|
||||
spanId === INVALID_SPAN_ID
|
||||
) {
|
||||
export function extractDaemonHttpTraceContext(
|
||||
headers: Record<string, unknown> | undefined,
|
||||
): Context | undefined {
|
||||
const traceparent = headers?.['traceparent'];
|
||||
if (typeof traceparent !== 'string' || traceparent.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return trace.setSpan(
|
||||
ROOT_CONTEXT,
|
||||
trace.wrapSpanContext({
|
||||
traceId,
|
||||
spanId,
|
||||
traceFlags: Number.parseInt(flags, 16),
|
||||
isRemote: true,
|
||||
}),
|
||||
const extracted = contextFromTraceparentValues(
|
||||
traceparent,
|
||||
headers?.['tracestate'],
|
||||
);
|
||||
return forceSampledUnderSampler(extracted);
|
||||
}
|
||||
|
||||
const TRACEPARENT_RE =
|
||||
/^\s?([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?\s?$/;
|
||||
const ALL_ZERO_TRACE_ID = '0'.repeat(32);
|
||||
const ALL_ZERO_SPAN_ID = '0'.repeat(16);
|
||||
|
||||
/**
|
||||
* Extract the caller's trace id from an inbound `traceparent` header without
|
||||
* any OpenTelemetry machinery. Unlike {@link extractDaemonHttpTraceContext}
|
||||
* (which builds a span parent and needs the W3C propagator — only installed
|
||||
* once the telemetry SDK starts), this is a plain format check so the daemon
|
||||
* log can carry the caller's trace id even with telemetry disabled: the
|
||||
* log-based join then works with no trace backend at all. The acceptance
|
||||
* rules mirror the vendored W3C propagator exactly (single optional leading/
|
||||
* trailing whitespace, trailing fields allowed above version `00`, `ff` and
|
||||
* all-zero ids rejected), so a header either joins on both paths or neither.
|
||||
*/
|
||||
export function extractInboundTraceId(
|
||||
headers: Record<string, unknown> | undefined,
|
||||
): string | undefined {
|
||||
const traceparent = headers?.['traceparent'];
|
||||
if (typeof traceparent !== 'string' || traceparent.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const match = TRACEPARENT_RE.exec(traceparent);
|
||||
if (!match) return undefined;
|
||||
// match: [full, version, traceId, spanId, flags, trailingFields]
|
||||
const [, version, traceId, spanId, , trailing] = match;
|
||||
// Version 00 must be exactly four fields; higher versions may carry
|
||||
// trailing extension fields the parser ignores — same as the propagator.
|
||||
if (version === '00' && trailing !== undefined) return undefined;
|
||||
if (version === 'ff') return undefined;
|
||||
if (traceId === ALL_ZERO_TRACE_ID || spanId === ALL_ZERO_SPAN_ID) {
|
||||
return undefined;
|
||||
}
|
||||
return traceId;
|
||||
}
|
||||
|
||||
export interface DaemonBridgeTelemetryMetrics {
|
||||
|
|
|
|||
|
|
@ -203,7 +203,9 @@ export {
|
|||
captureDaemonTelemetryContext,
|
||||
createDaemonBridgeTelemetry,
|
||||
emitDaemonLog,
|
||||
extractDaemonHttpTraceContext,
|
||||
extractDaemonTraceContext,
|
||||
extractInboundTraceId,
|
||||
hashDaemonWorkspace,
|
||||
injectDaemonTraceContext,
|
||||
recordDaemonError,
|
||||
|
|
@ -213,6 +215,7 @@ export {
|
|||
withDaemonRequestSpan,
|
||||
withDaemonSpan,
|
||||
type DaemonBridgeTelemetryMetrics,
|
||||
type DaemonRequestSpanOptions,
|
||||
} from './daemon-tracing.js';
|
||||
export {
|
||||
initializeDaemonMetrics,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
import { diag } from '@opentelemetry/api';
|
||||
import type { Context, TextMapPropagator } from '@opentelemetry/api';
|
||||
import { W3CTraceContextPropagator } from '@opentelemetry/core';
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
|
||||
import { resourceFromAttributes } from '@opentelemetry/resources';
|
||||
|
|
@ -41,6 +42,7 @@ import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|||
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
|
||||
import type { TelemetryRuntimeConfig } from './runtime-config.js';
|
||||
import { SERVICE_NAME } from './constants.js';
|
||||
import { setDaemonFallbackPropagator } from './daemon-tracing.js';
|
||||
import {
|
||||
FileLogExporter,
|
||||
FileMetricExporter,
|
||||
|
|
@ -510,5 +512,15 @@ export async function startTelemetrySdk(
|
|||
],
|
||||
});
|
||||
|
||||
// Construct the daemon fallback W3C propagator here rather than in
|
||||
// daemon-tracing.ts to keep @opentelemetry/core out of the static startup
|
||||
// graph: this module is only loaded via dynamic import when telemetry is
|
||||
// enabled, and its closure already contains @opentelemetry/core (via
|
||||
// sdk-node/resources), so the fallback adds no bytes to the CLI launch
|
||||
// path. The setter is a no-op-safe seam — until it runs, inbound
|
||||
// traceparent extraction yields no parent context, matching the
|
||||
// telemetry-off state.
|
||||
setDaemonFallbackPropagator(new W3CTraceContextPropagator());
|
||||
|
||||
return { sdk, metricReader };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { diag, ROOT_CONTEXT } from '@opentelemetry/api';
|
||||
import { diag, ROOT_CONTEXT, trace } from '@opentelemetry/api';
|
||||
import type { Config } from '../config/config.js';
|
||||
import {
|
||||
initializeTelemetry,
|
||||
|
|
@ -69,6 +69,7 @@ vi.mock('./session-tracing.js', () => ({
|
|||
}));
|
||||
vi.mock('./tracer.js', () => ({
|
||||
createSessionRootContext: vi.fn((id: string) => ({ __sessionId: id })),
|
||||
shouldForceSampled: vi.fn((): boolean => true),
|
||||
}));
|
||||
|
||||
import { LogToSpanProcessor } from './log-to-span-processor.js';
|
||||
|
|
@ -80,6 +81,7 @@ import {
|
|||
import { setShellTracePropagation } from './trace-context.js';
|
||||
import { createSessionRootContext } from './tracer.js';
|
||||
import { emitSessionEnd, emitSessionStart } from './session-events.js';
|
||||
import { extractDaemonHttpTraceContext } from './daemon-tracing.js';
|
||||
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
||||
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';
|
||||
import { sessionIdContext } from '../utils/sessionIdContext.js';
|
||||
|
|
@ -282,6 +284,22 @@ describe('Telemetry SDK', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('installs the daemon fallback propagator when the SDK initializes', async () => {
|
||||
// The pre-init state (fresh registry, empty fallback holder → no
|
||||
// parent context) is covered by daemon-tracing.test.ts; this test
|
||||
// proves the other half of the wiring: after initializeTelemetry the
|
||||
// sdk-impl chunk has injected the W3C fallback, so inbound HTTP
|
||||
// extraction resolves a remote parent even though the global
|
||||
// propagator stays a no-op (NodeSDK is mocked, nothing registers one).
|
||||
await initializeTelemetry(mockConfig);
|
||||
|
||||
const extracted = extractDaemonHttpTraceContext({
|
||||
traceparent: `00-${'3'.repeat(32)}-${'4'.repeat(16)}-01`,
|
||||
});
|
||||
expect(trace.getSpanContext(extracted!)?.traceId).toBe('3'.repeat(32));
|
||||
expect(trace.getSpanContext(extracted!)?.isRemote).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores external exporter selectors while starting explicit exporters', async () => {
|
||||
const exporterEnv = {
|
||||
OTEL_TRACES_EXPORTER: 'console',
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ export function startSpanWithContext(
|
|||
* let the sampler decide. `always_on` is the exception — it ignores parent
|
||||
* flags, so SAMPLED is harmless and keeps the decision matrix explicit.
|
||||
*/
|
||||
function shouldForceSampled(): boolean {
|
||||
export function shouldForceSampled(): boolean {
|
||||
const sampler =
|
||||
process.env['OTEL_TRACES_SAMPLER']?.trim().toLowerCase() ?? '';
|
||||
if (!sampler || sampler.startsWith('parentbased_')) {
|
||||
|
|
|
|||
|
|
@ -5789,6 +5789,420 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|||
limitations under the License.
|
||||
|
||||
|
||||
============================================================
|
||||
@opentelemetry/core@2.0.1
|
||||
(No repository found)
|
||||
|
||||
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.
|
||||
|
||||
|
||||
============================================================
|
||||
@opentelemetry/semantic-conventions@1.36.0
|
||||
(No repository found)
|
||||
|
||||
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.
|
||||
|
||||
|
||||
============================================================
|
||||
@opentelemetry/exporter-logs-otlp-grpc@0.203.0
|
||||
(No repository found)
|
||||
|
|
@ -6787,420 +7201,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
|
||||
|
||||
============================================================
|
||||
@opentelemetry/core@2.0.1
|
||||
(No repository found)
|
||||
|
||||
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.
|
||||
|
||||
|
||||
============================================================
|
||||
@opentelemetry/semantic-conventions@1.36.0
|
||||
(No repository found)
|
||||
|
||||
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.
|
||||
|
||||
|
||||
============================================================
|
||||
@opentelemetry/otlp-exporter-base@0.203.0
|
||||
(No repository found)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue