| 1 | /** |
| 2 | * @title Setting up tracing with Otlp modules |
| 3 | * |
| 4 | * Configure Otlp tracing + log export with a reusable observability layer. |
| 5 | */ |
| 6 | import { NodeRuntime } from "@effect/platform-node" |
| 7 | import { Context, Effect, Layer } from "effect" |
| 8 | import { FetchHttpClient } from "effect/http" |
| 9 | import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/observability" |
| 10 | |
| 11 | // Configure OTLP span export. |
| 12 | export const OtlpTracingLayer = OtlpTracer.layer({ |
| 13 | url: "http://localhost:4318/v1/traces", |
| 14 | resource: { |
| 15 | serviceName: "checkout-api", |
| 16 | serviceVersion: "1.0.0", |
| 17 | attributes: { |
| 18 | "deployment.environment": "staging" |
| 19 | } |
| 20 | } |
| 21 | }) |
| 22 | |
| 23 | // Configure OTLP log export. |
| 24 | export const OtlpLoggingLayer = OtlpLogger.layer({ |
| 25 | url: "http://localhost:4318/v1/logs", |
| 26 | resource: { |
| 27 | serviceName: "checkout-api", |
| 28 | serviceVersion: "1.0.0" |
| 29 | } |
| 30 | }) |
| 31 | |
| 32 | // Reusable app-wide observability layer. |
| 33 | // |
| 34 | // - OtlpTracer/OtlpLogger require an OTLP serializer and an HttpClient. |
| 35 | // - FetchHttpClient.layer provides the HttpClient used by the exporter. |
| 36 | export const ObservabilityLayer = Layer.merge(OtlpTracingLayer, OtlpLoggingLayer).pipe( |
| 37 | Layer.provide(OtlpSerialization.layerJson), |
| 38 | Layer.provide(FetchHttpClient.layer) |
| 39 | ) |
| 40 | |
| 41 | export class Checkout extends Context.Service<Checkout, { |
| 42 | processCheckout(orderId: string): Effect.Effect<void> |
| 43 | }>()("acme/Checkout") { |
| 44 | static readonly layer = Layer.effect( |
| 45 | Checkout, |
| 46 | Effect.gen(function*() { |
| 47 | yield* Effect.logInfo("setting up checkout service") |
| 48 | |
| 49 | return Checkout.of({ |
| 50 | processCheckout: Effect.fn("Checkout.processCheckout")(function*(orderId: string) { |
| 51 | yield* Effect.logInfo("starting checkout", { orderId }) |
| 52 | |
| 53 | yield* Effect.sleep("50 millis").pipe( |
| 54 | Effect.withSpan("checkout.charge-card"), |
| 55 | Effect.annotateSpans({ |
| 56 | "checkout.order_id": orderId, |
| 57 | "checkout.provider": "acme-pay" |
| 58 | }) |
| 59 | ) |
| 60 | |
| 61 | yield* Effect.sleep("20 millis").pipe( |
| 62 | Effect.withSpan("checkout.persist-order") |
| 63 | ) |
| 64 | |
| 65 | yield* Effect.logInfo("checkout completed", { orderId }) |
| 66 | }) |
| 67 | }) |
| 68 | }) |
| 69 | ) |
| 70 | } |
| 71 | |
| 72 | // Example usage of the Checkout service. |
| 73 | const CheckoutTest = Layer.effectDiscard( |
| 74 | Effect.gen(function*() { |
| 75 | const checkout = yield* Checkout |
| 76 | yield* checkout.processCheckout("ord_123") |
| 77 | }).pipe( |
| 78 | Effect.withSpan("checkout-test-run") |
| 79 | ) |
| 80 | ).pipe( |
| 81 | // You can also attach spans to Layers |
| 82 | Layer.withSpan("checkout-test"), |
| 83 | Layer.provide(Checkout.layer) |
| 84 | ) |
| 85 | |
| 86 | const Main = CheckoutTest.pipe( |
| 87 | // Provide the observability layer at the very end, so that all spans created |
| 88 | // by the app are exported. |
| 89 | Layer.provide(ObservabilityLayer) |
| 90 | ) |
| 91 | |
| 92 | // Launch the app |
| 93 | Layer.launch(Main).pipe( |
| 94 | NodeRuntime.runMain |
| 95 | ) |