Skip to content
Open
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
30 changes: 26 additions & 4 deletions src/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ export interface MetricsCollectorOptions {

export interface MetricsExporter {
export(metrics: MetricPoint[]): Promise<void>;
/**
* Optional: exporters that buffer internally can expose a flush() hook to
* be drained by MetricsCollector.flush(). Critical for Cloudflare Workers
* where the isolate dies before a size-threshold auto-flush would fire.
*/
flush?(): Promise<void>;
}

function hasExporterFlush(
exporter: MetricsExporter | undefined
): exporter is MetricsExporter & { flush: () => Promise<void> } {
return typeof exporter?.flush === 'function';
}

export class CloudflareAnalyticsExporter implements MetricsExporter {
Expand Down Expand Up @@ -330,17 +342,27 @@ export class MetricsCollector {
}

/**
* Flush metrics to exporter
* Flush metrics to exporter.
*
* Drains the collector's local buffer AND calls the exporter's own flush()
* if it exposes one. See worker-observability#7 for why the second call
* matters on Cloudflare Workers.
*/
async flush(): Promise<void> {
if (this.buffer.length === 0) return;
const exporter = this.options.export;
if (this.buffer.length === 0 && !hasExporterFlush(exporter)) return;

const metrics = [...this.buffer];
this.buffer = [];

if (this.options.export) {
if (exporter) {
try {
await this.options.export.export(metrics);
if (metrics.length > 0) {
await exporter.export(metrics);
}
if (hasExporterFlush(exporter)) {
await exporter.flush();
}
} catch (error) {
console.error('Failed to export metrics:', error);
// Re-add metrics to buffer on failure
Expand Down
31 changes: 28 additions & 3 deletions src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export interface TracerOptions {

export interface SpanExporter {
export(spans: TraceSpan[]): Promise<void>;
/**
* Optional: exporters that buffer internally can expose a flush() hook to
* be drained by Tracer.flush(). Critical for Cloudflare Workers where the
* isolate dies before a size-threshold auto-flush would fire.
*/
flush?(): Promise<void>;
}

function hasExporterFlush(
exporter: SpanExporter | undefined
): exporter is SpanExporter & { flush: () => Promise<void> } {
return typeof exporter?.flush === 'function';
}

export interface SpanContext {
Expand Down Expand Up @@ -244,17 +256,30 @@ export class Tracer {
}

/**
* Flush buffered spans
* Flush buffered spans.
*
* Drains the tracer's local buffer into the configured exporter AND then
* calls the exporter's own flush() if it exposes one. This matters on
* Cloudflare Workers, where the default exporter (StackbiltCloudExporter)
* buffers across requests and only POSTs at a size/byte threshold. In a
* low-volume isolate that threshold is rarely reached before eviction, so
* without the second flush call the spans die with the isolate. See
* worker-observability#7.
*/
async flush(): Promise<void> {
if (this.buffer.length === 0) return;
if (this.buffer.length === 0 && !hasExporterFlush(this.options.export)) return;

const spans = [...this.buffer];
this.buffer = [];

if (this.options.export) {
try {
await this.options.export.export(spans);
if (spans.length > 0) {
await this.options.export.export(spans);
}
if (hasExporterFlush(this.options.export)) {
await this.options.export.flush();
}
} catch (error) {
console.error('Failed to export traces:', error);
// Re-add spans to buffer on failure
Expand Down