From 8492f5b944459fc966807202eccc9b7802799e6f Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 26 Apr 2024 16:27:53 +1200 Subject: [PATCH] allow for multiple otel span processors & metric readers (#2623) --- .changeset/metal-fireants-remember.md | 5 +++ packages/opentelemetry/src/Metrics.ts | 9 ++++-- packages/opentelemetry/src/NodeSdk.ts | 31 ++++++++++++------- packages/opentelemetry/src/WebSdk.ts | 27 ++++++++++------ .../opentelemetry/src/internal/metrics.ts | 19 +++++++++--- packages/opentelemetry/test/Tracer.test.ts | 2 +- 6 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 .changeset/metal-fireants-remember.md diff --git a/.changeset/metal-fireants-remember.md b/.changeset/metal-fireants-remember.md new file mode 100644 index 0000000000..4c77fc4f0c --- /dev/null +++ b/.changeset/metal-fireants-remember.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +allow for multiple otel span processors & metric readers diff --git a/packages/opentelemetry/src/Metrics.ts b/packages/opentelemetry/src/Metrics.ts index 4d565f7eb8..616c65c62a 100644 --- a/packages/opentelemetry/src/Metrics.ts +++ b/packages/opentelemetry/src/Metrics.ts @@ -2,6 +2,7 @@ * @since 1.0.0 */ import type { MetricProducer, MetricReader } from "@opentelemetry/sdk-metrics" +import type { NonEmptyReadonlyArray } from "effect/Array" import type * as Effect from "effect/Effect" import type { LazyArg } from "effect/Function" import type { Layer } from "effect/Layer" @@ -21,11 +22,13 @@ export const makeProducer: Effect.Effect = inte */ export const registerProducer: ( self: MetricProducer, - metricReader: LazyArg -) => Effect.Effect = internal.registerProducer + metricReader: LazyArg> +) => Effect.Effect, never, Scope.Scope> = internal.registerProducer /** * @since 1.0.0 * @category layers */ -export const layer: (evaluate: LazyArg) => Layer = internal.layer +export const layer: ( + evaluate: LazyArg> +) => Layer = internal.layer diff --git a/packages/opentelemetry/src/NodeSdk.ts b/packages/opentelemetry/src/NodeSdk.ts index 43970d342b..d11b556718 100644 --- a/packages/opentelemetry/src/NodeSdk.ts +++ b/packages/opentelemetry/src/NodeSdk.ts @@ -6,6 +6,7 @@ import type * as Resources from "@opentelemetry/resources" import type { MetricReader } from "@opentelemetry/sdk-metrics" import type { SpanProcessor, TracerConfig } from "@opentelemetry/sdk-trace-base" import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node" +import type { NonEmptyReadonlyArray } from "effect/Array" import * as Effect from "effect/Effect" import type { LazyArg } from "effect/Function" import * as Layer from "effect/Layer" @@ -18,9 +19,9 @@ import * as Tracer from "./Tracer.js" * @category model */ export interface Configuration { - readonly spanProcessor?: SpanProcessor | undefined + readonly spanProcessor?: SpanProcessor | ReadonlyArray | undefined readonly tracerConfig?: Omit | undefined - readonly metricReader?: MetricReader | undefined + readonly metricReader?: MetricReader | ReadonlyArray | undefined readonly resource?: { readonly serviceName: string readonly serviceVersion?: string @@ -33,7 +34,7 @@ export interface Configuration { * @category layers */ export const layerTracerProvider = ( - processor: SpanProcessor, + processor: SpanProcessor | NonEmptyReadonlyArray, config?: Omit ): Layer.Layer => Layer.scoped( @@ -47,7 +48,11 @@ export const layerTracerProvider = ( ...(config ?? undefined), resource }) - provider.addSpanProcessor(processor) + if (Array.isArray(processor)) { + processor.forEach((p) => provider.addSpanProcessor(p)) + } else { + provider.addSpanProcessor(processor as any) + } return provider }), (provider) => Effect.ignoreLogged(Effect.promise(() => provider.shutdown())) @@ -74,14 +79,16 @@ export const layer: { const ResourceLive = config.resource === undefined ? Resource.layerFromEnv() : Resource.layer(config.resource) - const TracerLive = config.spanProcessor ? - Tracer.layer.pipe( - Layer.provide(layerTracerProvider(config.spanProcessor, config.tracerConfig)) - ) - : Layer.empty - const MetricsLive = config.metricReader - ? Metrics.layer(() => config.metricReader!) - : Layer.empty + const TracerLive = + config.spanProcessor && !(Array.isArray(config.spanProcessor) && config.spanProcessor.length === 0) ? + Tracer.layer.pipe( + Layer.provide(layerTracerProvider(config.spanProcessor as any, config.tracerConfig)) + ) + : Layer.empty + const MetricsLive = + config.metricReader && !(Array.isArray(config.metricReader) && config.metricReader.length === 0) + ? Metrics.layer(() => config.metricReader as any) + : Layer.empty return Layer.merge(TracerLive, MetricsLive).pipe( Layer.provideMerge(ResourceLive) ) diff --git a/packages/opentelemetry/src/WebSdk.ts b/packages/opentelemetry/src/WebSdk.ts index 231f86ab05..94b47842f9 100644 --- a/packages/opentelemetry/src/WebSdk.ts +++ b/packages/opentelemetry/src/WebSdk.ts @@ -6,6 +6,7 @@ import type * as Resources from "@opentelemetry/resources" import type { MetricReader } from "@opentelemetry/sdk-metrics" import type { SpanProcessor, TracerConfig } from "@opentelemetry/sdk-trace-base" import { WebTracerProvider } from "@opentelemetry/sdk-trace-web" +import type { NonEmptyReadonlyArray } from "effect/Array" import * as Effect from "effect/Effect" import type { LazyArg } from "effect/Function" import * as Layer from "effect/Layer" @@ -18,9 +19,9 @@ import * as Tracer from "./Tracer.js" * @category model */ export interface Configuration { - readonly spanProcessor?: SpanProcessor + readonly spanProcessor?: SpanProcessor | ReadonlyArray | undefined readonly tracerConfig?: Omit - readonly metricReader?: MetricReader + readonly metricReader?: MetricReader | ReadonlyArray | undefined readonly resource: { readonly serviceName: string readonly serviceVersion?: string @@ -33,7 +34,7 @@ export interface Configuration { * @category layers */ export const layerTracerProvider = ( - processor: SpanProcessor, + processor: SpanProcessor | NonEmptyReadonlyArray, config?: Omit ): Layer.Layer => Layer.scoped( @@ -47,7 +48,11 @@ export const layerTracerProvider = ( ...(config ?? undefined), resource }) - provider.addSpanProcessor(processor) + if (Array.isArray(processor)) { + processor.forEach((p) => provider.addSpanProcessor(p)) + } else { + provider.addSpanProcessor(processor as any) + } return provider }), (provider) => Effect.ignoreLogged(Effect.promise(() => provider.shutdown())) @@ -72,12 +77,14 @@ export const layer: { : Effect.sync(evaluate), (config) => { const ResourceLive = Resource.layer(config.resource) - const TracerLive = config.spanProcessor ? - Tracer.layer.pipe(Layer.provide(layerTracerProvider(config.spanProcessor, config.tracerConfig))) - : Layer.effectDiscard(Effect.void) - const MetricsLive = config.metricReader - ? Metrics.layer(() => config.metricReader!) - : Layer.effectDiscard(Effect.void) + const TracerLive = + config.spanProcessor && !(Array.isArray(config.spanProcessor) && config.spanProcessor.length === 0) ? + Tracer.layer.pipe(Layer.provide(layerTracerProvider(config.spanProcessor as any, config.tracerConfig))) + : Layer.effectDiscard(Effect.void) + const MetricsLive = + config.metricReader && !(Array.isArray(config.metricReader) && config.metricReader.length === 0) + ? Metrics.layer(() => config.metricReader as any) + : Layer.effectDiscard(Effect.void) return Layer.merge(TracerLive, MetricsLive).pipe( Layer.provideMerge(ResourceLive) ) diff --git a/packages/opentelemetry/src/internal/metrics.ts b/packages/opentelemetry/src/internal/metrics.ts index 2f883cf389..bef336dcc9 100644 --- a/packages/opentelemetry/src/internal/metrics.ts +++ b/packages/opentelemetry/src/internal/metrics.ts @@ -290,18 +290,27 @@ export const makeProducer = Effect.map( ) /** @internal */ -export const registerProducer = (self: MetricProducer, metricReader: LazyArg) => +export const registerProducer = ( + self: MetricProducer, + metricReader: LazyArg> +) => Effect.acquireRelease( Effect.sync(() => { const reader = metricReader() - reader.setMetricProducer(self) - return reader + const readers: Array = Array.isArray(reader) ? reader : [reader] as any + readers.forEach((reader) => reader.setMetricProducer(self)) + return readers }), - (reader) => Effect.ignoreLogged(Effect.promise(() => reader.shutdown())) + (readers) => + Effect.ignoreLogged(Effect.promise(() => + Promise.all( + readers.map((reader) => reader.shutdown()) + ) + )) ) /** @internal */ -export const layer = (evaluate: LazyArg) => +export const layer = (evaluate: LazyArg>) => Layer.scopedDiscard(Effect.flatMap( makeProducer, (producer) => registerProducer(producer, evaluate) diff --git a/packages/opentelemetry/test/Tracer.test.ts b/packages/opentelemetry/test/Tracer.test.ts index ee297dd356..d7d48795a4 100644 --- a/packages/opentelemetry/test/Tracer.test.ts +++ b/packages/opentelemetry/test/Tracer.test.ts @@ -11,7 +11,7 @@ const TracingLive = NodeSdk.layer(Effect.sync(() => ({ resource: { serviceName: "test" }, - spanProcessor: new SimpleSpanProcessor(new InMemorySpanExporter()) + spanProcessor: [new SimpleSpanProcessor(new InMemorySpanExporter())] }))) // needed to test context propagation