Skip to content

Commit a815633

Browse files
authored
Add external log exporters and fix missing external trace exporters in deployed tasks (#2038)
* Add external log exporters and fix missing external trace exporters in deployed tasks * Generate the external traceID correctly and exporter 3rd party logs with the external traceID as well
1 parent 84a0336 commit a815633

File tree

10 files changed

+143
-7
lines changed

10 files changed

+143
-7
lines changed

.changeset/cuddly-boats-press.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Add external log exporters and fix missing external trace exporters in deployed tasks

packages/cli-v3/src/entryPoints/dev-run-worker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ async function bootstrap() {
164164
url: env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318",
165165
instrumentations: config.telemetry?.instrumentations ?? config.instrumentations ?? [],
166166
exporters: config.telemetry?.exporters ?? [],
167+
logExporters: config.telemetry?.logExporters ?? [],
167168
diagLogLevel: (env.OTEL_LOG_LEVEL as TracingDiagnosticLogLevel) ?? "none",
168169
forceFlushTimeoutMillis: 30_000,
169170
});
@@ -174,7 +175,8 @@ async function bootstrap() {
174175
const tracer = new TriggerTracer({ tracer: otelTracer, logger: otelLogger });
175176
const consoleInterceptor = new ConsoleInterceptor(
176177
otelLogger,
177-
typeof config.enableConsoleLogging === "boolean" ? config.enableConsoleLogging : true
178+
typeof config.enableConsoleLogging === "boolean" ? config.enableConsoleLogging : true,
179+
typeof config.disableConsoleInterceptor === "boolean" ? config.disableConsoleInterceptor : false
178180
);
179181

180182
const configLogLevel = triggerLogLevel ?? config.logLevel ?? "info";

packages/cli-v3/src/entryPoints/managed-run-worker.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ async function bootstrap() {
163163
instrumentations: config.instrumentations ?? [],
164164
diagLogLevel: (env.OTEL_LOG_LEVEL as TracingDiagnosticLogLevel) ?? "none",
165165
forceFlushTimeoutMillis: 30_000,
166+
exporters: config.telemetry?.exporters ?? [],
167+
logExporters: config.telemetry?.logExporters ?? [],
166168
});
167169

168170
const otelTracer: Tracer = tracingSDK.getTracer("trigger-dev-worker", VERSION);
@@ -171,7 +173,8 @@ async function bootstrap() {
171173
const tracer = new TriggerTracer({ tracer: otelTracer, logger: otelLogger });
172174
const consoleInterceptor = new ConsoleInterceptor(
173175
otelLogger,
174-
typeof config.enableConsoleLogging === "boolean" ? config.enableConsoleLogging : true
176+
typeof config.enableConsoleLogging === "boolean" ? config.enableConsoleLogging : true,
177+
typeof config.disableConsoleInterceptor === "boolean" ? config.disableConsoleInterceptor : false
175178
);
176179

177180
const configLogLevel = triggerLogLevel ?? config.logLevel ?? "info";

packages/core/src/v3/config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
} from "./index.js";
1212
import type { LogLevel } from "./logger/taskLogger.js";
1313
import type { MachinePresetName } from "./schemas/common.js";
14+
import { LogRecordExporter } from "@opentelemetry/sdk-logs";
1415

1516
export type CompatibilityFlag = "run_engine_v2";
1617

@@ -80,6 +81,13 @@ export type TriggerConfig = {
8081
* @see https://trigger.dev/docs/config/config-file#exporters
8182
*/
8283
exporters?: Array<SpanExporter>;
84+
85+
/**
86+
* Log exporters to use for OpenTelemetry. This is useful if you want to add custom log exporters to your tasks.
87+
*
88+
* @see https://trigger.dev/docs/config/config-file#exporters
89+
*/
90+
logExporters?: Array<LogRecordExporter>;
8391
};
8492

8593
/**
@@ -131,6 +139,11 @@ export type TriggerConfig = {
131139
*/
132140
enableConsoleLogging?: boolean;
133141

142+
/**
143+
* Disable the console interceptor. This will prevent logs from being sent to the trigger.dev backend.
144+
*/
145+
disableConsoleInterceptor?: boolean;
146+
134147
build?: {
135148
/**
136149
* Add custom conditions to the esbuild build. For example, if you are importing `ai/rsc`, you'll need to add "react-server" condition.

packages/core/src/v3/consoleInterceptor.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ import { clock } from "./clock-api.js";
1010
export class ConsoleInterceptor {
1111
constructor(
1212
private readonly logger: logsAPI.Logger,
13-
private readonly sendToStdIO: boolean
13+
private readonly sendToStdIO: boolean,
14+
private readonly interceptingDisabled: boolean
1415
) {}
1516

1617
// Intercept the console and send logs to the OpenTelemetry logger
1718
// during the execution of the callback
1819
async intercept<T>(console: Console, callback: () => Promise<T>): Promise<T> {
20+
if (this.interceptingDisabled) {
21+
return await callback();
22+
}
23+
1924
// Save the original console methods
2025
const originalConsole = {
2126
log: console.log,

packages/core/src/v3/otel/tracingSDK.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DiagConsoleLogger, DiagLogLevel, TracerProvider, diag } from "@opentelemetry/api";
2+
import { RandomIdGenerator } from "@opentelemetry/sdk-trace-base";
23
import { logs } from "@opentelemetry/api-logs";
34
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
45
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
@@ -15,6 +16,8 @@ import {
1516
import {
1617
BatchLogRecordProcessor,
1718
LoggerProvider,
19+
LogRecordExporter,
20+
ReadableLogRecord,
1821
SimpleLogRecordProcessor,
1922
} from "@opentelemetry/sdk-logs";
2023
import {
@@ -87,9 +90,12 @@ export type TracingSDKConfig = {
8790
resource?: IResource;
8891
instrumentations?: Instrumentation[];
8992
exporters?: SpanExporter[];
93+
logExporters?: LogRecordExporter[];
9094
diagLogLevel?: TracingDiagnosticLogLevel;
9195
};
9296

97+
const idGenerator = new RandomIdGenerator();
98+
9399
export class TracingSDK {
94100
public readonly asyncResourceDetector = new AsyncResourceDetector();
95101
private readonly _logProvider: LoggerProvider;
@@ -158,7 +164,7 @@ export class TracingSDK {
158164
)
159165
);
160166

161-
const externalTraceId = crypto.randomUUID();
167+
const externalTraceId = idGenerator.generateTraceId();
162168

163169
for (const exporter of config.exporters ?? []) {
164170
traceProvider.addSpanProcessor(
@@ -210,6 +216,28 @@ export class TracingSDK {
210216
)
211217
);
212218

219+
for (const externalLogExporter of config.logExporters ?? []) {
220+
loggerProvider.addLogRecordProcessor(
221+
getEnvVar("OTEL_BATCH_PROCESSING_ENABLED") === "1"
222+
? new BatchLogRecordProcessor(
223+
new ExternalLogRecordExporterWrapper(externalLogExporter, externalTraceId),
224+
{
225+
maxExportBatchSize: parseInt(getEnvVar("OTEL_LOG_MAX_EXPORT_BATCH_SIZE") ?? "64"),
226+
scheduledDelayMillis: parseInt(
227+
getEnvVar("OTEL_LOG_SCHEDULED_DELAY_MILLIS") ?? "200"
228+
),
229+
exportTimeoutMillis: parseInt(
230+
getEnvVar("OTEL_LOG_EXPORT_TIMEOUT_MILLIS") ?? "30000"
231+
),
232+
maxQueueSize: parseInt(getEnvVar("OTEL_LOG_MAX_QUEUE_SIZE") ?? "512"),
233+
}
234+
)
235+
: new SimpleLogRecordProcessor(
236+
new ExternalLogRecordExporterWrapper(externalLogExporter, externalTraceId)
237+
)
238+
);
239+
}
240+
213241
this._logProvider = loggerProvider;
214242
this._spanExporter = spanExporter;
215243
this._traceProvider = traceProvider;
@@ -306,3 +334,50 @@ class ExternalSpanExporterWrapper {
306334
: Promise.resolve();
307335
}
308336
}
337+
338+
class ExternalLogRecordExporterWrapper {
339+
constructor(
340+
private underlyingExporter: LogRecordExporter,
341+
private externalTraceId: string
342+
) {}
343+
344+
export(logs: any[], resultCallback: (result: any) => void): void {
345+
const modifiedLogs = logs.map(this.transformLogRecord.bind(this));
346+
347+
this.underlyingExporter.export(modifiedLogs, resultCallback);
348+
}
349+
350+
shutdown(): Promise<void> {
351+
return this.underlyingExporter.shutdown();
352+
}
353+
354+
transformLogRecord(logRecord: ReadableLogRecord): ReadableLogRecord {
355+
// If there's no spanContext, or if the externalTraceId is not set, return the original logRecord.
356+
if (!logRecord.spanContext || !this.externalTraceId) {
357+
return logRecord;
358+
}
359+
360+
// Capture externalTraceId for use within the proxy's scope.
361+
const { externalTraceId } = this;
362+
363+
return new Proxy(logRecord, {
364+
get(target, prop, receiver) {
365+
if (prop === "spanContext") {
366+
// Intercept access to spanContext.
367+
const originalSpanContext = target.spanContext;
368+
// Ensure originalSpanContext exists (it should, due to the check above, but good for safety).
369+
if (originalSpanContext) {
370+
return {
371+
...originalSpanContext,
372+
traceId: externalTraceId, // Override traceId.
373+
};
374+
}
375+
// Fallback if, for some reason, originalSpanContext is undefined here.
376+
return undefined;
377+
}
378+
// For all other properties, defer to the original object.
379+
return Reflect.get(target, prop, receiver);
380+
},
381+
});
382+
}
383+
}

pnpm-lock.yaml

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

references/d3-chat/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"@trigger.dev/python": "workspace:*",
2828
"@trigger.dev/react-hooks": "workspace:*",
2929
"@trigger.dev/sdk": "workspace:*",
30+
"@opentelemetry/exporter-logs-otlp-http": "0.52.1",
31+
"@opentelemetry/exporter-trace-otlp-http": "0.52.1",
3032
"@vercel/postgres": "^0.10.0",
3133
"ai": "4.2.5",
3234
"class-variance-authority": "^0.7.1",

references/d3-chat/src/trigger/chat.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ export const interruptibleChat = schemaTask({
227227
prompt: z.string().describe("The prompt to chat with the AI"),
228228
}),
229229
run: async ({ prompt }, { signal }) => {
230+
logger.info("interruptible-chat: starting", { prompt });
231+
230232
const chunks: TextStreamPart<{}>[] = [];
231233

232234
// 👇 This is a global onCancel hook, but it's inside of the run function

references/d3-chat/trigger.config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
import { defineConfig } from "@trigger.dev/sdk";
22
import { pythonExtension } from "@trigger.dev/python/extension";
33
import { installPlaywrightChromium } from "./src/extensions/playwright";
4+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
5+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
46

57
export default defineConfig({
68
project: "proj_cdmymsrobxmcgjqzhdkq",
79
dirs: ["./src/trigger"],
10+
telemetry: {
11+
logExporters: [
12+
new OTLPLogExporter({
13+
url: "https://api.axiom.co/v1/logs",
14+
headers: {
15+
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
16+
"X-Axiom-Dataset": "d3-chat-tester",
17+
},
18+
}),
19+
],
20+
exporters: [
21+
new OTLPTraceExporter({
22+
url: "https://api.axiom.co/v1/traces",
23+
headers: {
24+
Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
25+
"X-Axiom-Dataset": "d3-chat-tester",
26+
},
27+
}),
28+
],
29+
},
830
maxDuration: 3600,
931
build: {
1032
extensions: [

0 commit comments

Comments
 (0)