Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
"@grpc/grpc-js": "^1.14.7",
"@opentelemetry/api": "^1.10.0",
"@opentelemetry/api-logs": "^0.223.0",
"@opentelemetry/context-async-hooks": "^3.0.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.300.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.300.0",
"@opentelemetry/resources": "^2.12.0",
"@opentelemetry/sdk-logs": "^0.223.0",
"@opentelemetry/sdk-trace-base": "^2.12.0",
"@opentelemetry/sdk-trace-node": "^2.12.0",
"@opentelemetry/resources": "^3.0.0",
"@opentelemetry/sdk-logs": "^3.0.0",
"@opentelemetry/sdk-trace": "^3.0.0",
"@opentelemetry/semantic-conventions": "^1.44.0",
"@remote-sql-agent/protocol": "workspace:*",
"argon2": "^0.45.1",
Expand Down
28 changes: 21 additions & 7 deletions packages/server/src/tracing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { trace, type Tracer } from '@opentelemetry/api';
import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
import { TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';
import { context, trace, type Tracer } from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import {
BatchSpanProcessor,
TraceIdRatioBasedSampler,
TracerProvider,
} from '@opentelemetry/sdk-trace';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
Expand Down Expand Up @@ -32,7 +36,8 @@ import type { ServerConfig } from './config.js';
const SERVICE_VERSION = '0.2.0';
const TRACER_NAME = 'rsagent.server';

let provider: NodeTracerProvider | null = null;
let provider: TracerProvider | null = null;
let contextManager: AsyncLocalStorageContextManager | null = null;

export function tracingEnabled(config: ServerConfig): boolean {
return config.trace.otlpEnabled && Boolean(config.trace.otlpEndpoint);
Expand All @@ -58,12 +63,12 @@ export function startTracing(config: ServerConfig, logger: Logger): void {
headers: config.trace.otlpHeaders,
});

provider = new NodeTracerProvider({
provider = new TracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: config.trace.serviceName,
[ATTR_SERVICE_VERSION]: SERVICE_VERSION,
}),
spanProcessors: [new BatchSpanProcessor(exporter)],
spanProcessors: [new BatchSpanProcessor({ exporter })],
// Below 1 lets a large estate cap trace volume without touching the
// collector; the default of 1 (every request and command traced) is right
// for the traffic this product actually sees — a handful of operators,
Expand All @@ -73,7 +78,12 @@ export function startTracing(config: ServerConfig, logger: Logger): void {
? new TraceIdRatioBasedSampler(config.trace.sampleRatio)
: undefined,
});
provider.register();
// sdk-trace 3.x dropped NodeTracerProvider.register(): the provider no longer
// wires itself into the API, so the global provider and the async-local
// context manager (which keeps the active span across awaits) are set here.
contextManager = new AsyncLocalStorageContextManager().enable();
context.setGlobalContextManager(contextManager);
trace.setGlobalTracerProvider(provider);

logger.info(
{ endpoint: config.trace.otlpEndpoint, sampleRatio: config.trace.sampleRatio },
Expand All @@ -85,6 +95,10 @@ export async function stopTracing(): Promise<void> {
await provider?.forceFlush().catch(() => undefined);
await provider?.shutdown().catch(() => undefined);
provider = null;
contextManager?.disable();
contextManager = null;
trace.disable();
context.disable();
}

export function getTracer(): Tracer {
Expand Down
12 changes: 6 additions & 6 deletions packages/server/test/command-tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { eq } from 'drizzle-orm';
import { generateCommandSigningKeyPair } from '@remote-sql-agent/protocol';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import {
NodeTracerProvider,
TracerProvider,
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-node';
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
} from '@opentelemetry/sdk-trace';
import type { ReadableSpan } from '@opentelemetry/sdk-trace';
import type { Database } from '../src/db/client.js';
import { loadConfig } from '../src/config.js';
import { CommandService } from '../src/domain/commands.js';
Expand Down Expand Up @@ -40,10 +40,10 @@ const signingKeyPem = generateCommandSigningKeyPair().privateKeyPem;

beforeAll(async () => {
({ db, close } = await setupTestDatabase('command_tracing'));
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)],
const provider = new TracerProvider({
spanProcessors: [new SimpleSpanProcessor({ exporter })],
});
provider.register();
trace.setGlobalTracerProvider(provider);
});

afterAll(async () => {
Expand Down
10 changes: 5 additions & 5 deletions packages/server/test/request-tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { pino } from 'pino';
import { generateKeyPairSync } from 'node:crypto';
import { trace, SpanStatusCode } from '@opentelemetry/api';
import {
NodeTracerProvider,
TracerProvider,
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-node';
} from '@opentelemetry/sdk-trace';
import type { Database } from '../src/db/client.js';
import { loadConfig } from '../src/config.js';
import { createApp } from '../src/api/app.js';
Expand Down Expand Up @@ -34,10 +34,10 @@ const signingKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toStri

beforeAll(async () => {
({ db, close } = await setupTestDatabase('request_tracing'));
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(exporter)],
const provider = new TracerProvider({
spanProcessors: [new SimpleSpanProcessor({ exporter })],
});
provider.register();
trace.setGlobalTracerProvider(provider);
});

afterAll(async () => {
Expand Down
70 changes: 70 additions & 0 deletions packages/server/test/tracing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { afterEach, describe, expect, it } from 'vitest';
import { pino } from 'pino';
import { context, trace } from '@opentelemetry/api';
import type { ServerConfig } from '../src/config.js';
import { getTracer, startTracing, stopTracing, tracingEnabled } from '../src/tracing.js';

/**
* startTracing wires the OpenTelemetry 3.x SDK into the API by hand (the SDK's
* own `register()` is gone), so this checks the wiring itself: a real global
* provider, and a context manager that keeps the active span across awaits.
* No database or collector is needed; nothing is exported (the endpoint is
* never contacted before shutdown, which swallows the flush failure).
*/
function config(trace: Partial<ServerConfig['trace']> = {}): ServerConfig {
return {
trace: {
otlpEnabled: true,
otlpEndpoint: 'http://127.0.0.1:9/v1/traces',
otlpHeaders: {},
serviceName: 'rsagent-test',
sampleRatio: 1,
...trace,
},
} as ServerConfig;
}

const logger = pino({ level: 'silent' });

afterEach(async () => {
await stopTracing();
});

describe('tracing', () => {
it('is disabled without an endpoint', () => {
expect(tracingEnabled(config({ otlpEndpoint: undefined }))).toBe(false);
expect(tracingEnabled(config({ otlpEnabled: false }))).toBe(false);
expect(tracingEnabled(config())).toBe(true);
});

it('leaves the API no-op when disabled', () => {
startTracing(config({ otlpEnabled: false }), logger);
expect(getTracer().startSpan('x').isRecording()).toBe(false);
});

it('records spans and keeps the active span across awaits once started', async () => {
startTracing(config(), logger);
const tracer = getTracer();

await tracer.startActiveSpan('outer', async (outer) => {
expect(outer.isRecording()).toBe(true);
await new Promise((resolve) => setTimeout(resolve, 5));
expect(trace.getSpan(context.active())).toBe(outer);
const child = tracer.startSpan('inner');
expect(child.spanContext().traceId).toBe(outer.spanContext().traceId);
child.end();
outer.end();
});
});

it('honours a sample ratio of 0', () => {
startTracing(config({ sampleRatio: 0 }), logger);
expect(getTracer().startSpan('dropped').isRecording()).toBe(false);
});

it('is a no-op again after stopTracing', async () => {
startTracing(config(), logger);
await stopTracing();
expect(getTracer().startSpan('after').isRecording()).toBe(false);
});
});
109 changes: 15 additions & 94 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading