@decocms/start 4.2.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/sdk/observability.ts +2 -0
- package/src/sdk/otel.ts +44 -6
- package/src/sdk/otelAdapters.test.ts +138 -2
- package/src/sdk/otelAdapters.ts +83 -1
package/package.json
CHANGED
package/src/sdk/observability.ts
CHANGED
|
@@ -60,9 +60,11 @@ export {
|
|
|
60
60
|
createAnalyticsEngineMeterAdapter,
|
|
61
61
|
createOtelLoggerAdapter,
|
|
62
62
|
createOtelMeterAdapter,
|
|
63
|
+
flushOtelProviders,
|
|
63
64
|
getRuntimeEnv,
|
|
64
65
|
type OtelLoggerAdapterOptions,
|
|
65
66
|
type OtelMeterAdapterOptions,
|
|
67
|
+
registerOtelFlushHandler,
|
|
66
68
|
setRuntimeEnv,
|
|
67
69
|
} from "./otelAdapters";
|
|
68
70
|
// Sampler
|
package/src/sdk/otel.ts
CHANGED
|
@@ -42,16 +42,25 @@ import { type Resource, resourceFromAttributes } from "@opentelemetry/resources"
|
|
|
42
42
|
|
|
43
43
|
import { configureMeter, configureTracer } from "../middleware/observability";
|
|
44
44
|
import { createCompositeLogger, createCompositeMeter } from "./composite";
|
|
45
|
-
import { configureLogger, defaultLoggerAdapter, logger } from "./logger";
|
|
45
|
+
import { configureLogger, defaultLoggerAdapter, type LogLevel, logger } from "./logger";
|
|
46
46
|
import {
|
|
47
47
|
createAnalyticsEngineMeterAdapter,
|
|
48
48
|
createOtelLoggerAdapter,
|
|
49
49
|
createOtelMeterAdapter,
|
|
50
|
+
flushOtelProviders,
|
|
50
51
|
setRuntimeEnv,
|
|
51
52
|
} from "./otelAdapters";
|
|
52
53
|
import { RequestContext } from "./requestContext";
|
|
53
54
|
import { createUrlBasedHeadSampler, decodeSamplingConfig } from "./sampler";
|
|
54
55
|
|
|
56
|
+
const VALID_LOG_LEVELS: readonly LogLevel[] = ["debug", "info", "warn", "error"];
|
|
57
|
+
|
|
58
|
+
function parseLogLevel(value: unknown): LogLevel | undefined {
|
|
59
|
+
if (typeof value !== "string") return undefined;
|
|
60
|
+
const lc = value.toLowerCase();
|
|
61
|
+
return VALID_LOG_LEVELS.find((l) => l === lc);
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
// ---------------------------------------------------------------------------
|
|
56
65
|
// Types
|
|
57
66
|
// ---------------------------------------------------------------------------
|
|
@@ -69,6 +78,15 @@ export interface OtelOptions {
|
|
|
69
78
|
analyticsEngineEnabled?: boolean;
|
|
70
79
|
/** Push interval for OTLP metrics, in ms. Defaults to env.OTEL_EXPORT_INTERVAL or 60_000. */
|
|
71
80
|
metricsExportIntervalMillis?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Minimum severity to forward to OTLP logs (HyperDX). Below the floor
|
|
83
|
+
* the framework still writes a structured JSON line to `console.*`
|
|
84
|
+
* (Cloudflare Workers Logs), so nothing is silently lost.
|
|
85
|
+
*
|
|
86
|
+
* Defaults to `"warn"`. Falls back to env `OTEL_LOG_MIN_SEVERITY` when
|
|
87
|
+
* unset. Set to `"debug"` to forward everything.
|
|
88
|
+
*/
|
|
89
|
+
otlpMinSeverity?: LogLevel;
|
|
72
90
|
/**
|
|
73
91
|
* Version of `@decocms/start` to advertise as `deco.runtime.version`.
|
|
74
92
|
* Falls back to a build-time constant; override only for tests.
|
|
@@ -179,12 +197,27 @@ export function instrumentWorker(
|
|
|
179
197
|
return handler.fetch(request, env, ctx);
|
|
180
198
|
};
|
|
181
199
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
try {
|
|
201
|
+
// RequestContext may already be active (createDecoWorkerEntry sets it
|
|
202
|
+
// up). If so, run inline; otherwise wrap. Cheap to detect via current.
|
|
203
|
+
if (RequestContext.current) {
|
|
204
|
+
return await wrap();
|
|
205
|
+
}
|
|
206
|
+
return await RequestContext.run(request, wrap);
|
|
207
|
+
} finally {
|
|
208
|
+
// Drain OTLP logger + meter batches inside the post-response window
|
|
209
|
+
// the platform guarantees via `waitUntil`. Without this hook,
|
|
210
|
+
// BatchLogRecordProcessor (5s flush) and PeriodicExportingMetricReader
|
|
211
|
+
// (60s flush) batches usually die with the isolate before the timer
|
|
212
|
+
// fires and never reach HyperDX. `flushOtelProviders` is a no-op when
|
|
213
|
+
// OTLP isn't configured, so this is safe in every code path.
|
|
214
|
+
try {
|
|
215
|
+
ctx.waitUntil(flushOtelProviders());
|
|
216
|
+
} catch {
|
|
217
|
+
// `waitUntil` only throws if `ctx` isn't a real ExecutionContext
|
|
218
|
+
// (e.g. test stubs). Telemetry flush failures are not request-fatal.
|
|
219
|
+
}
|
|
186
220
|
}
|
|
187
|
-
return RequestContext.run(request, wrap);
|
|
188
221
|
},
|
|
189
222
|
};
|
|
190
223
|
|
|
@@ -216,6 +249,9 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
|
|
|
216
249
|
});
|
|
217
250
|
|
|
218
251
|
// ---- Logger ----------------------------------------------------------
|
|
252
|
+
const otlpMinSeverity =
|
|
253
|
+
opts.otlpMinSeverity ?? parseLogLevel(env.OTEL_LOG_MIN_SEVERITY) ?? "warn";
|
|
254
|
+
|
|
219
255
|
const otelLogger =
|
|
220
256
|
otlpEndpoint != null
|
|
221
257
|
? createOtelLoggerAdapter({
|
|
@@ -223,6 +259,7 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
|
|
|
223
259
|
headers: otlpHeaders,
|
|
224
260
|
resource,
|
|
225
261
|
name: serviceName,
|
|
262
|
+
minSeverity: otlpMinSeverity,
|
|
226
263
|
})
|
|
227
264
|
: null;
|
|
228
265
|
|
|
@@ -262,6 +299,7 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
|
|
|
262
299
|
logger.info("observability booted", {
|
|
263
300
|
service: serviceName,
|
|
264
301
|
otlp: Boolean(otlpEndpoint),
|
|
302
|
+
otlpMinSeverity: otlpEndpoint != null ? otlpMinSeverity : null,
|
|
265
303
|
analyticsEngine: aeEnabled,
|
|
266
304
|
sampling: Boolean(env.OTEL_SAMPLING_CONFIG),
|
|
267
305
|
runtimeVersion: opts.decoRuntimeVersion ?? DECO_RUNTIME_VERSION,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
3
|
import {
|
|
4
|
+
_getFlushHandlerCountForTests,
|
|
5
|
+
_resetFlushHandlersForTests,
|
|
3
6
|
createAnalyticsEngineMeterAdapter,
|
|
4
7
|
createOtelLoggerAdapter,
|
|
5
8
|
createOtelMeterAdapter,
|
|
9
|
+
flushOtelProviders,
|
|
10
|
+
registerOtelFlushHandler,
|
|
6
11
|
setRuntimeEnv,
|
|
7
12
|
} from "./otelAdapters";
|
|
8
13
|
import { RequestContext } from "./requestContext";
|
|
@@ -40,7 +45,13 @@ describe("createOtelLoggerAdapter / createOtelMeterAdapter", () => {
|
|
|
40
45
|
});
|
|
41
46
|
|
|
42
47
|
it("logs without throwing for a variety of attribute shapes", () => {
|
|
43
|
-
|
|
48
|
+
// minSeverity:"debug" so the test exercises the actual emit path; the
|
|
49
|
+
// default "warn" floor would short-circuit before the OTel emit runs
|
|
50
|
+
// and the attribute-sanitization branch wouldn't be hit.
|
|
51
|
+
const log = createOtelLoggerAdapter({
|
|
52
|
+
endpoint: "https://otel.example.invalid",
|
|
53
|
+
minSeverity: "debug",
|
|
54
|
+
});
|
|
44
55
|
expect(log).not.toBeNull();
|
|
45
56
|
expect(() =>
|
|
46
57
|
log!.log("info", "ok", {
|
|
@@ -58,6 +69,131 @@ describe("createOtelLoggerAdapter / createOtelMeterAdapter", () => {
|
|
|
58
69
|
});
|
|
59
70
|
});
|
|
60
71
|
|
|
72
|
+
describe("createOtelLoggerAdapter — minSeverity floor", () => {
|
|
73
|
+
// Every call to `createOtelLoggerAdapter` constructs its own local
|
|
74
|
+
// `LoggerProvider`. The OTel global provider is set the first time
|
|
75
|
+
// (`setGlobalLoggerProvider` is "first wins"), so spying on the global
|
|
76
|
+
// doesn't observe later adapters' emits. Spying on the *prototype*
|
|
77
|
+
// sidesteps that — every provider instance, local or global, returns
|
|
78
|
+
// the same fake logger and we can count emits across all of them.
|
|
79
|
+
let emitSpy: ReturnType<typeof vi.fn>;
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
_resetFlushHandlersForTests();
|
|
83
|
+
emitSpy = vi.fn();
|
|
84
|
+
vi.spyOn(LoggerProvider.prototype, "getLogger").mockReturnValue({
|
|
85
|
+
emit: emitSpy,
|
|
86
|
+
} as unknown as ReturnType<LoggerProvider["getLogger"]>);
|
|
87
|
+
});
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
vi.restoreAllMocks();
|
|
90
|
+
_resetFlushHandlersForTests();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function makeAdapter(minSeverity?: "debug" | "info" | "warn" | "error") {
|
|
94
|
+
const adapter = createOtelLoggerAdapter({
|
|
95
|
+
endpoint: "https://otel.example.invalid",
|
|
96
|
+
headers: { authorization: "test" },
|
|
97
|
+
...(minSeverity ? { minSeverity } : {}),
|
|
98
|
+
});
|
|
99
|
+
expect(adapter).not.toBeNull();
|
|
100
|
+
return adapter!;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
it("defaults to 'warn' — drops debug + info, keeps warn + error", () => {
|
|
104
|
+
const adapter = makeAdapter(undefined);
|
|
105
|
+
adapter.log("debug", "d");
|
|
106
|
+
adapter.log("info", "i");
|
|
107
|
+
adapter.log("warn", "w");
|
|
108
|
+
adapter.log("error", "e");
|
|
109
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(emitSpy.mock.calls[0]?.[0].severityText).toBe("WARN");
|
|
111
|
+
expect(emitSpy.mock.calls[1]?.[0].severityText).toBe("ERROR");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("explicit minSeverity='debug' lets every level through", () => {
|
|
115
|
+
const adapter = makeAdapter("debug");
|
|
116
|
+
adapter.log("debug", "d");
|
|
117
|
+
adapter.log("info", "i");
|
|
118
|
+
adapter.log("warn", "w");
|
|
119
|
+
adapter.log("error", "e");
|
|
120
|
+
expect(emitSpy).toHaveBeenCalledTimes(4);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("explicit minSeverity='error' only forwards error", () => {
|
|
124
|
+
const adapter = makeAdapter("error");
|
|
125
|
+
adapter.log("debug", "d");
|
|
126
|
+
adapter.log("info", "i");
|
|
127
|
+
adapter.log("warn", "w");
|
|
128
|
+
adapter.log("error", "e");
|
|
129
|
+
expect(emitSpy).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(emitSpy.mock.calls[0]?.[0].severityText).toBe("ERROR");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("flushOtelProviders / registerOtelFlushHandler", () => {
|
|
135
|
+
beforeEach(() => _resetFlushHandlersForTests());
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
vi.restoreAllMocks();
|
|
138
|
+
_resetFlushHandlersForTests();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("resolves immediately when no handlers are registered", async () => {
|
|
142
|
+
expect(_getFlushHandlerCountForTests()).toBe(0);
|
|
143
|
+
await expect(flushOtelProviders()).resolves.toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("invokes every registered handler exactly once", async () => {
|
|
147
|
+
const a = vi.fn(() => Promise.resolve());
|
|
148
|
+
const b = vi.fn(() => Promise.resolve());
|
|
149
|
+
registerOtelFlushHandler(a);
|
|
150
|
+
registerOtelFlushHandler(b);
|
|
151
|
+
expect(_getFlushHandlerCountForTests()).toBe(2);
|
|
152
|
+
await flushOtelProviders();
|
|
153
|
+
expect(a).toHaveBeenCalledOnce();
|
|
154
|
+
expect(b).toHaveBeenCalledOnce();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("does NOT reject when a handler throws — uses Promise.allSettled", async () => {
|
|
158
|
+
const ok = vi.fn(() => Promise.resolve());
|
|
159
|
+
const fail = vi.fn(() => Promise.reject(new Error("OTLP 503")));
|
|
160
|
+
registerOtelFlushHandler(fail);
|
|
161
|
+
registerOtelFlushHandler(ok);
|
|
162
|
+
// Must resolve, not throw — flush failures are telemetry incidents,
|
|
163
|
+
// not request incidents. This is the surface contract callers rely on.
|
|
164
|
+
await expect(flushOtelProviders()).resolves.toBeUndefined();
|
|
165
|
+
expect(ok).toHaveBeenCalledOnce();
|
|
166
|
+
expect(fail).toHaveBeenCalledOnce();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("createOtelLoggerAdapter registers exactly one flush handler", () => {
|
|
170
|
+
const before = _getFlushHandlerCountForTests();
|
|
171
|
+
const adapter = createOtelLoggerAdapter({
|
|
172
|
+
endpoint: "https://otel.example.invalid",
|
|
173
|
+
});
|
|
174
|
+
expect(adapter).not.toBeNull();
|
|
175
|
+
expect(_getFlushHandlerCountForTests()).toBe(before + 1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("createOtelMeterAdapter registers exactly one flush handler", () => {
|
|
179
|
+
const before = _getFlushHandlerCountForTests();
|
|
180
|
+
const meter = createOtelMeterAdapter({
|
|
181
|
+
endpoint: "https://otel.example.invalid",
|
|
182
|
+
exportIntervalMillis: 60_000,
|
|
183
|
+
});
|
|
184
|
+
expect(meter).not.toBeNull();
|
|
185
|
+
expect(_getFlushHandlerCountForTests()).toBe(before + 1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("returning-null factories register no handlers", () => {
|
|
189
|
+
expect(_getFlushHandlerCountForTests()).toBe(0);
|
|
190
|
+
createOtelLoggerAdapter(null);
|
|
191
|
+
createOtelLoggerAdapter({ endpoint: "" });
|
|
192
|
+
createOtelMeterAdapter(null);
|
|
193
|
+
expect(_getFlushHandlerCountForTests()).toBe(0);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
61
197
|
describe("createAnalyticsEngineMeterAdapter", () => {
|
|
62
198
|
afterEach(() => vi.restoreAllMocks());
|
|
63
199
|
|
package/src/sdk/otelAdapters.ts
CHANGED
|
@@ -39,6 +39,56 @@ import { MetricNames } from "../middleware/observability";
|
|
|
39
39
|
import type { LoggerAdapter, LogLevel } from "./logger";
|
|
40
40
|
import { RequestContext } from "./requestContext";
|
|
41
41
|
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Flush registry — per-request `ctx.waitUntil` hook target
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Cloudflare Workers isolates are recycled aggressively (often before the
|
|
48
|
+
* next 5s `BatchLogRecordProcessor` tick or 60s `PeriodicExportingMetricReader`
|
|
49
|
+
* tick fires). Without a per-request flush, in-memory log/metric batches die
|
|
50
|
+
* with the isolate and never reach OTLP — observable as "spans show up in
|
|
51
|
+
* HyperDX, logs and metrics don't".
|
|
52
|
+
*
|
|
53
|
+
* Each OTLP adapter registers a `forceFlush` handler here at construction
|
|
54
|
+
* time. `instrumentWorker` calls `flushOtelProviders()` from
|
|
55
|
+
* `ctx.waitUntil(...)` after every response so the batch is drained inside
|
|
56
|
+
* the post-response window the platform guarantees.
|
|
57
|
+
*
|
|
58
|
+
* The registry is module-scoped (one entry per provider, not per request)
|
|
59
|
+
* and survives worker reloads — the boot guard in `bootObservability`
|
|
60
|
+
* prevents duplicate registration in steady state. Tests must call
|
|
61
|
+
* `_resetFlushHandlersForTests()` between fixtures to avoid leakage.
|
|
62
|
+
*/
|
|
63
|
+
const flushHandlers: Array<() => Promise<unknown>> = [];
|
|
64
|
+
|
|
65
|
+
/** Register a `forceFlush()`-style handler. Idempotency is the caller's problem. */
|
|
66
|
+
export function registerOtelFlushHandler(fn: () => Promise<unknown>): void {
|
|
67
|
+
flushHandlers.push(fn);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Drain every registered OTLP provider in parallel. Resolves once they
|
|
72
|
+
* all settle — never rejects, because flush failures are telemetry
|
|
73
|
+
* incidents, not request incidents.
|
|
74
|
+
*
|
|
75
|
+
* Safe to call when no adapters are registered (returns immediately).
|
|
76
|
+
*/
|
|
77
|
+
export async function flushOtelProviders(): Promise<void> {
|
|
78
|
+
if (flushHandlers.length === 0) return;
|
|
79
|
+
await Promise.allSettled(flushHandlers.map((fn) => fn()));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Test-only: clear the flush registry between fixtures. Do not call from app code. */
|
|
83
|
+
export function _resetFlushHandlersForTests(): void {
|
|
84
|
+
flushHandlers.length = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Test-only: introspect the registry size. Do not call from app code. */
|
|
88
|
+
export function _getFlushHandlerCountForTests(): number {
|
|
89
|
+
return flushHandlers.length;
|
|
90
|
+
}
|
|
91
|
+
|
|
42
92
|
// ---------------------------------------------------------------------------
|
|
43
93
|
// Env / binding access
|
|
44
94
|
// ---------------------------------------------------------------------------
|
|
@@ -79,13 +129,37 @@ export interface OtelLoggerAdapterOptions {
|
|
|
79
129
|
resource?: Resource;
|
|
80
130
|
/** OTel logger name. Defaults to "@decocms/start". */
|
|
81
131
|
name?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Minimum severity to forward to OTLP. Calls below this floor are dropped
|
|
134
|
+
* by this adapter (and therefore don't ship to HyperDX or any other OTLP
|
|
135
|
+
* sink), but the framework's default console adapter still sees them, so
|
|
136
|
+
* they remain visible in Cloudflare Workers Logs.
|
|
137
|
+
*
|
|
138
|
+
* Defaults to `"warn"`. The reasoning: in Cloudflare Workers every OTLP
|
|
139
|
+
* emit eventually becomes an outbound subrequest (after batching). Routine
|
|
140
|
+
* `info` chatter compounded over many isolates can exhaust either the
|
|
141
|
+
* subrequest budget (if a hot loop logs) or the HyperDX log-ingest quota.
|
|
142
|
+
* Warn+ keeps the trace ↔ log correlation that matters during incidents
|
|
143
|
+
* without ingesting noise that's already captured by Cloudflare Logs.
|
|
144
|
+
*
|
|
145
|
+
* Override per-site via `OtelOptions.otlpMinSeverity` or the
|
|
146
|
+
* `OTEL_LOG_MIN_SEVERITY` env var. Set to `"debug"` to forward everything.
|
|
147
|
+
*/
|
|
148
|
+
minSeverity?: LogLevel;
|
|
82
149
|
}
|
|
83
150
|
|
|
84
151
|
/**
|
|
85
|
-
* Streams `logger.*` calls to an OTLP/HTTP logs
|
|
152
|
+
* Streams `logger.*` calls (at or above `minSeverity`) to an OTLP/HTTP logs
|
|
153
|
+
* endpoint (e.g. HyperDX).
|
|
86
154
|
*
|
|
87
155
|
* Returns `null` when no endpoint is configured — `instrumentWorker()`
|
|
88
156
|
* uses that signal to skip registering this adapter.
|
|
157
|
+
*
|
|
158
|
+
* Side effect: registers the underlying `LoggerProvider`'s `forceFlush()`
|
|
159
|
+
* with `registerOtelFlushHandler` so per-request hooks in `instrumentWorker`
|
|
160
|
+
* can drain the batch before the Workers isolate is recycled. Without that
|
|
161
|
+
* registration, `BatchLogRecordProcessor`'s 5s timer rarely fires before
|
|
162
|
+
* GC and log records are silently dropped.
|
|
89
163
|
*/
|
|
90
164
|
export function createOtelLoggerAdapter(
|
|
91
165
|
options: OtelLoggerAdapterOptions | null,
|
|
@@ -101,6 +175,7 @@ export function createOtelLoggerAdapter(
|
|
|
101
175
|
resource: options.resource,
|
|
102
176
|
});
|
|
103
177
|
provider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter));
|
|
178
|
+
registerOtelFlushHandler(() => provider.forceFlush());
|
|
104
179
|
|
|
105
180
|
// Register globally so `@opentelemetry/api-logs` consumers (if any)
|
|
106
181
|
// also pick it up. Idempotent — safe across multiple worker reloads.
|
|
@@ -111,10 +186,12 @@ export function createOtelLoggerAdapter(
|
|
|
111
186
|
}
|
|
112
187
|
|
|
113
188
|
const otelLogger = provider.getLogger(options.name ?? "@decocms/start");
|
|
189
|
+
const minSev = SEVERITY[options.minSeverity ?? "warn"].number;
|
|
114
190
|
|
|
115
191
|
return {
|
|
116
192
|
log(level, msg, attrs) {
|
|
117
193
|
const sev = SEVERITY[level];
|
|
194
|
+
if (sev.number < minSev) return;
|
|
118
195
|
otelLogger.emit({
|
|
119
196
|
severityNumber: sev.number,
|
|
120
197
|
severityText: sev.text,
|
|
@@ -229,6 +306,11 @@ export function createOtelMeterAdapter(
|
|
|
229
306
|
readers: [reader],
|
|
230
307
|
views,
|
|
231
308
|
});
|
|
309
|
+
// See `flushHandlers` registry comment — `PeriodicExportingMetricReader`
|
|
310
|
+
// ticks every `exportIntervalMillis` (default 60s); without per-request
|
|
311
|
+
// forceFlush the metric batch typically dies with the Workers isolate
|
|
312
|
+
// before its first scheduled tick.
|
|
313
|
+
registerOtelFlushHandler(() => provider.forceFlush());
|
|
232
314
|
|
|
233
315
|
try {
|
|
234
316
|
metricsApi.setGlobalMeterProvider(provider);
|