Skip to content

Latest commit

 

History

History
128 lines (102 loc) · 4.67 KB

File metadata and controls

128 lines (102 loc) · 4.67 KB
title Trace Operations Across Services with Spans
id trace-operations-with-spans
skillLevel intermediate
applicationPatternId observability
summary Use Effect.withSpan to create custom tracing spans, providing detailed visibility into the performance and flow of your application's operations.
tags
tracing
span
observability
opentelemetry
performance
debugging
rule
description
Use Effect.withSpan to create custom tracing spans for important operations.
related
observability-structured-logging
add-custom-metrics
model-dependencies-as-services
author effect_website
lessonOrder 6

Guideline

To gain visibility into the performance and flow of your application, wrap logical units of work with Effect.withSpan("span-name"). You can add contextual information to these spans using the attributes option.


Rationale

While logs tell you what happened, traces tell you why it was slow. In a complex application, a single user request might trigger calls to multiple services (authentication, database, external APIs). Tracing allows you to visualize this entire chain of events as a single, hierarchical "trace."

Each piece of work in that trace is a span. Effect.withSpan allows you to create your own custom spans. This is invaluable for answering questions like:

  • "For this API request, did we spend most of our time in the database or calling the external payment gateway?"
  • "Which part of our user creation logic is the bottleneck?"

Effect's tracing is built on OpenTelemetry, the industry standard, so it integrates seamlessly with tools like Jaeger, Zipkin, and Datadog.


Good Example

This example shows a multi-step operation. Each step, and the overall operation, is wrapped in a span. This creates a parent-child hierarchy in the trace that is easy to visualize.

import { Effect, Duration } from "effect";

const validateInput = (input: unknown) =>
  Effect.gen(function* () {
    yield* Effect.logInfo("Starting input validation...");
    yield* Effect.sleep(Duration.millis(10));
    const result = { email: "paul@example.com" };
    yield* Effect.logInfo(`✅ Input validated: ${result.email}`);
    return result;
  }).pipe(
    // This creates a child span
    Effect.withSpan("validateInput")
  );

const saveToDatabase = (user: { email: string }) =>
  Effect.gen(function* () {
    yield* Effect.logInfo(`Saving user to database: ${user.email}`);
    yield* Effect.sleep(Duration.millis(50));
    const result = { id: 123, ...user };
    yield* Effect.logInfo(`✅ User saved with ID: ${result.id}`);
    return result;
  }).pipe(
    // This span includes useful attributes
    Effect.withSpan("saveToDatabase", {
      attributes: { "db.system": "postgresql", "db.user.email": user.email },
    })
  );

const createUser = (input: unknown) =>
  Effect.gen(function* () {
    yield* Effect.logInfo("=== Creating User with Tracing ===");
    yield* Effect.logInfo(
      "This demonstrates how spans trace operations through the call stack"
    );

    const validated = yield* validateInput(input);
    const user = yield* saveToDatabase(validated);

    yield* Effect.logInfo(
      `✅ User creation completed: ${JSON.stringify(user)}`
    );
    yield* Effect.logInfo(
      "Note: In production, spans would be sent to a tracing system like Jaeger or Zipkin"
    );

    return user;
  }).pipe(
    // This is the parent span for the entire operation
    Effect.withSpan("createUserOperation")
  );

// Demonstrate the tracing functionality
const program = Effect.gen(function* () {
  yield* Effect.logInfo("=== Trace Operations with Spans Demo ===");

  // Create multiple users to show tracing in action
  const user1 = yield* createUser({ email: "user1@example.com" });

  yield* Effect.logInfo("\n--- Creating second user ---");
  const user2 = yield* createUser({ email: "user2@example.com" });

  yield* Effect.logInfo("\n=== Summary ===");
  yield* Effect.logInfo("Created users with tracing spans:");
  yield* Effect.logInfo(`User 1: ID ${user1.id}, Email: ${user1.email}`);
  yield* Effect.logInfo(`User 2: ID ${user2.id}, Email: ${user2.email}`);
});

// When run with a tracing SDK, this will produce traces with root spans
// "createUserOperation" and child spans: "validateInput" and "saveToDatabase".
Effect.runPromise(program);

Anti-Pattern

Not adding custom spans to your business logic. Without them, your traces will only show high-level information from your framework (e.g., "HTTP POST /users"). You will have no visibility into the performance of the individual steps inside your request handler, making it very difficult to pinpoint bottlenecks. Your application's logic remains a "black box" in your traces.