| name | effect-patterns-observability |
| description | Effect-TS patterns for Observability. Use when working with observability in Effect-TS applications. |
Effect-TS Patterns: Observability
This skill provides 13 curated Effect-TS patterns for observability.
Use this skill when working on tasks related to:
- observability
- Best practices in Effect-TS applications
- Real-world patterns and solutions
🟢 Beginner Patterns
Debug Effect Programs
Rule: Use Effect.tap and logging to inspect values without changing program flow.
Good Example:
import { Effect, pipe } from "effect"
const fetchUser = (id: string) =>
Effect.succeed({ id, name: "Alice", email: "alice@example.com" })
const processUser = (id: string) =>
fetchUser(id).pipe(
Effect.tap((user) => Effect.log(`Fetched user: ${user.name}`)),
Effect.map((user) => ({ ...user, processed: true })),
Effect.tap((user) => Effect.log(`Processed: ${JSON.stringify(user)}`))
)
const numbers = [1, 2, 3, 4, 5]
const pipeline = Effect.gen(function* () {
yield* Effect.log("Starting pipeline")
const step1 = numbers.filter((n) => n % 2 === 0)
yield* Effect.log(`After filter (even): ${JSON.stringify(step1)}`)
const step2 = step1.map((n) => n * 10)
yield* Effect.log(`After map (*10): ${JSON.stringify(step2)}`)
const step3 = step2.reduce((a, b) => a + b, 0)
yield* Effect.log(`After reduce (sum): ${step3}`)
return step3
})
const riskyOperation = (shouldFail: boolean) =>
Effect.gen(function* () {
yield* Effect.log("Starting risky operation")
if (shouldFail) {
yield* Effect.log("About to fail...")
return yield* Effect.fail(new Error("Something went wrong"))
}
yield* Effect.log("Success!")
return "result"
})
const debugErrors = riskyOperation(true).pipe(
Effect.tapError((error) => Effect.log(`Operation failed: ${error.message}`)),
Effect.catchAll((error) => {
return Effect.succeed(`Recovered from: ${error.message}`)
})
)
const step = (name: string, value: number) =>
Effect.gen(function* () {
yield* Effect.log(`[${name}] Input: ${value}`)
const result = value * 2
yield* Effect.log(`[${name}] Output: ${result}`)
return result
})
const tracedWorkflow = Effect.gen(function* () {
const a = yield* step("Step 1", 5)
const b = yield* step("Step 2", a)
const c = yield* step("Step 3", b)
yield* Effect.log(`Final result: ${c}`)
return c
})
const quickDebug = Effect.gen(function* () {
const value = yield* Effect.succeed(42)
yield* Effect.sync(() => console.log("Quick debug:", value))
return value
})
const program = Effect.gen(function* () {
yield* Effect.log("=== Tap Example ===")
yield* processUser("123")
yield* Effect.log("\n=== Pipeline Debug ===")
yield* pipeline
yield* Effect.log("\n=== Error Debug ===")
yield* debugErrors
yield* Effect.log("\n=== Traced Workflow ===")
yield* tracedWorkflow
})
Effect.runPromise(program)
Rationale:
Use Effect.tap to inspect values and Effect.log to trace execution without changing program behavior.
Debugging Effect code differs from imperative code:
- No breakpoints - Effects are descriptions, not executions
- Lazy evaluation - Code runs later when you call
runPromise
- Composition - Effects chain together
tap and logging let you see inside without breaking the chain.
Your First Logs
Rule: Use Effect.log and related functions for structured, contextual logging.
Good Example:
import { Effect, Logger, LogLevel } from "effect"
const basicLogging = Effect.gen(function* () {
yield* Effect.logDebug("Debug message - for development")
yield* Effect.logInfo("Info message - normal operation")
yield* Effect.log("Default log - same as logInfo")
yield* Effect.logWarning("Warning - something unusual")
yield* Effect.logError("Error - something went wrong")
})
const withContext = Effect.gen(function* () {
yield* Effect.log("User logged in").pipe(
Effect.annotateLogs({
userId: "user-123",
action: "login",
ipAddress: "192.168.1.1",
})
)
yield* Effect.log("Processing request").pipe(
Effect.annotateLogs("requestId", "req-456")
)
})
const withTiming = Effect.gen(function* () {
yield* Effect.log("Starting operation")
yield* Effect.sleep("100 millis").pipe(
Effect.withLogSpan("database-query")
)
yield* Effect.log("Operation complete")
})
interface User {
id: string
email: string
}
const processOrder = (orderId: string, userId: string) =>
Effect.gen(function* () {
yield* Effect.logInfo("Processing order").pipe(
Effect.annotateLogs({ orderId, userId })
)
yield* Effect.sleep("50 millis")
yield* Effect.logInfo("Order processed successfully").pipe(
Effect.annotateLogs({ orderId, status: "completed" })
)
return { orderId, status: "completed" }
}).pipe(
Effect.withLogSpan("processOrder")
)
const debugProgram = basicLogging.pipe(
Logger.withMinimumLogLevel(LogLevel.Debug)
)
const productionProgram = basicLogging.pipe(
Logger.withMinimumLogLevel(LogLevel.Warning)
)
const program = Effect.gen(function* () {
yield* Effect.log("=== Basic Logging ===")
yield* basicLogging
yield* Effect.log("\n=== With Context ===")
yield* withContext
yield* Effect.log("\n=== With Timing ===")
yield* withTiming
yield* Effect.log("\n=== Process Order ===")
yield* processOrder("order-789", "user-123")
})
Effect.runPromise(program)
Rationale:
Use Effect's built-in logging functions for structured, contextual logging that works with any logging backend.
Effect's logging is superior to console.log:
- Structured - Logs are data, not just strings
- Contextual - Automatically includes fiber info, timestamps
- Configurable - Change log levels, formats, destinations
- Type-safe - Part of the Effect type system
🟡 Intermediate Patterns
Instrument and Observe Function Calls with Effect.fn
Rule: Use Effect.fn to wrap functions with effectful instrumentation, such as logging, metrics, or tracing, in a composable and type-safe way.
Good Example:
import { Effect } from "effect";
function add(a: number, b: number): number {
return a + b;
}
const addWithLogging = Effect.fn("add")(add).pipe(
Effect.withSpan("add", { attributes: { "fn.name": "add" } })
);
const program = Effect.gen(function* () {
yield* Effect.logInfo("Calling add function");
const sum = yield* addWithLogging(2, 3);
yield* Effect.logInfo(`Sum is ${sum}`);
return sum;
});
Effect.runPromise(program);
Explanation:
Effect.fn("name")(fn) wraps a function with instrumentation capabilities, enabling observability.
- You can add tracing spans, logging, metrics, and other observability logic to function boundaries.
- Keeps instrumentation separate from business logic and fully composable.
- The wrapped function integrates seamlessly with Effect's observability and tracing infrastructure.
Anti-Pattern:
Scattering logging, metrics, or tracing logic directly inside business functions, making code harder to test, maintain, and compose.
Rationale:
Use Effect.fn to wrap and instrument function calls with effectful logic, such as logging, metrics, or tracing.
This enables you to observe, monitor, and debug function boundaries in a composable, type-safe way.
Instrumenting function calls is essential for observability, especially in complex or critical code paths.
Effect.fn lets you add effectful logic (logging, metrics, tracing, etc.) before, after, or around any function call, without changing the function’s core logic.
Leverage Effect's Built-in Structured Logging
Rule: Use Effect.log, Effect.logInfo, and Effect.logError to add structured, context-aware logging to your Effect code.
Good Example:
import { Effect } from "effect";
const program = Effect.gen(function* () {
yield* Effect.log("Starting the application");
});
const infoProgram = Effect.gen(function* () {
yield* Effect.logInfo("User signed in");
});
const errorProgram = Effect.gen(function* () {
yield* Effect.logError("Failed to connect to database");
});
const userId = 42;
const logUserProgram = Effect.gen(function* () {
yield* Effect.logInfo(`Processing user: ${userId}`);
});
const workflow = Effect.gen(function* () {
yield* Effect.log("Beginning workflow");
yield* Effect.logInfo("Workflow step completed");
yield* Effect.logError("Something went wrong");
});
Explanation:
Effect.log logs a message at the default level.
Effect.logInfo and Effect.logError log at specific levels.
- Logging is context-aware and can be used anywhere in your Effect workflows.
Anti-Pattern:
Using console.log or ad-hoc logging scattered throughout your code, which is not structured, not context-aware, and harder to manage in production.
Rationale:
Use Effect.log, Effect.logInfo, Effect.logError, and related functions to add structured, context-aware logging to your Effect code.
This enables you to capture important events, errors, and business information in a consistent and configurable way.
Structured logging makes it easier to search, filter, and analyze logs in production.
Effect’s logging functions are context-aware, meaning they automatically include relevant metadata and can be configured globally.
Add Custom Metrics to Your Application
Rule: Use Metric.counter, Metric.gauge, and Metric.histogram to instrument code for monitoring.
Good Example:
This example creates a counter to track how many times a user is created and a histogram to track the duration of the database operation.
import { Effect, Metric, Duration } from "effect";
const userRegisteredCounter = Metric.counter("users_registered_total", {
description: "A counter for how many users have been registered.",
});
const dbDurationTimer = Metric.timer(
"db_operation_duration",
"A timer for DB operation durations"
);
const saveUserToDb = Effect.succeed("user saved").pipe(
Effect.delay(Duration.millis(Math.random() * 100))
);
const createUser = Effect.gen(function* () {
yield* saveUserToDb.pipe(Metric.trackDuration(dbDurationTimer));
yield* Metric.increment(userRegisteredCounter);
return { status: "success" };
});
const programWithLogging = Effect.gen(function* () {
const result = yield* createUser;
yield* Effect.log(`Result: ${JSON.stringify(result)}`);
return result;
});
Effect.runPromise(programWithLogging);
Anti-Pattern:
Not adding any metrics to your application. Without metrics, you are flying blind. You have no high-level overview of your application's health, performance, or business KPIs. You can't build dashboards, you can't set up alerts for abnormal behavior (e.g., "error rate is too high"), and you are forced to rely on digging through logs to
understand the state of your system.
Rationale:
To monitor the health and performance of your application, instrument your code with Metrics. The three main types are:
Metric.counter("name"): To count occurrences of an event (e.g., users_registered_total). It only goes up.
Metric.gauge("name"): To track a value that can go up or down (e.g., active_connections).
Metric.histogram("name"): To track the distribution of a value (e.g., request_duration_seconds).
While logs are for events and traces are for requests, metrics are for aggregation. They provide a high-level, numerical view of your system's health over time, which is perfect for building dashboards and setting up alerts.
Effect's Metric module provides a simple, declarative way to add this instrumentation. By defining your metrics upfront, you can then use operators like Metric.increment or Effect.timed to update them. This is fully integrated with Effect's context system, allowing you to provide different metric backends (like Prometheus or StatsD) via a Layer.
This allows you to answer questions like:
- "What is our user sign-up rate over the last 24 hours?"
- "Are we approaching our maximum number of database connections?"
- "What is the 95th percentile latency for our API requests?"
Add Custom Metrics to Your Application
Rule: Use Effect's Metric module to define and update custom metrics for business and performance monitoring.
Good Example:
import { Effect, Metric } from "effect";
const jobsProcessed = Metric.counter("jobs_processed");
const processJob = Effect.gen(function* () {
yield* Effect.log("Job processed");
yield* Metric.increment(jobsProcessed);
});
const activeUsers = Metric.gauge("active_users");
const userSignedIn = Metric.set(activeUsers, 1);
const userSignedOut = Metric.set(activeUsers, 0);
const requestDuration = Metric.histogram("request_duration", [
0.1, 0.5, 1, 2, 5,
] as any);
const recordDuration = (duration: number) =>
Metric.update(requestDuration, duration);
Explanation:
Metric.counter tracks counts of events.
Metric.gauge tracks a value that can go up or down (e.g., active users).
Metric.histogram tracks distributions (e.g., request durations).
Effect.updateMetric updates the metric in your workflow.
Anti-Pattern:
Relying solely on logs for monitoring, or using ad-hoc counters and variables that are not integrated with your observability stack.
Rationale:
Use Effect's Metric module to define and update custom metrics such as counters, gauges, and histograms.
This allows you to track business events, performance indicators, and system health in a type-safe and composable way.
Metrics provide quantitative insight into your application's behavior and performance.
By instrumenting your code with metrics, you can monitor key events, detect anomalies, and drive business decisions.
Trace Operations Across Services with Spans
Rule: Use Effect.withSpan to create and annotate tracing spans for operations, enabling distributed tracing and performance analysis.
Good Example:
import { Effect } from "effect";
const fetchUser = Effect.sync(() => {
return { id: 1, name: "Alice" };
}).pipe(Effect.withSpan("db.fetchUser"));
const fetchData = Effect.tryPromise({
try: () => fetch("https://api.example.com/data").then((res) => res.json()),
catch: (err) => `Network error: ${String(err)}`,
}).pipe(
Effect.withSpan("http.fetchData", {
attributes: { url: "https://api.example.com/data" },
})
);
const program = Effect.gen(function* () {
yield* Effect.log("Starting workflow").pipe(
Effect.withSpan("workflow.start")
);
const user = yield* fetchUser;
yield* Effect.log(`Fetched user: ${user.name}`).pipe(
Effect.withSpan("workflow.end")
);
});
Explanation:
Effect.withSpan creates a tracing span around an operation.
- Spans can be named and annotated with attributes for richer context.
- Tracing enables distributed observability and performance analysis.
Anti-Pattern:
Relying only on logs or metrics for performance analysis, or lacking visibility into the flow of requests and operations across services.
Rationale:
Use Effect.withSpan to create custom tracing spans around important operations in your application.
This enables distributed tracing, performance analysis, and deep visibility into how requests flow through your system.
Tracing spans help you understand the flow and timing of operations, especially in distributed systems or complex workflows.
They allow you to pinpoint bottlenecks, visualize dependencies, and correlate logs and metrics with specific requests.
Trace Operations Across Services with Spans
Rule: Use Effect.withSpan to create custom tracing spans for important operations.
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(
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(
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(
Effect.withSpan("createUserOperation")
);
const program = Effect.gen(function* () {
yield* Effect.logInfo("=== Trace Operations with Spans Demo ===");
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}`);
});
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.
Rationale:
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.
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.
🟠 Advanced Patterns
Create Observability Dashboards
Rule: Create focused dashboards that answer specific questions about system health.
Rationale:
Design dashboards that answer specific questions about system health, performance, and user experience.
Good dashboards provide:
- Quick health check - See problems at a glance
- Trend analysis - Spot gradual degradation
- Debugging aid - Correlate metrics during incidents
- Capacity planning - Forecast resource needs
Set Up Alerting
Rule: Create alerts based on SLOs and symptoms, not causes.
Good Example:
import { Effect, Metric, Schedule, Duration, Ref } from "effect"
interface Alert {
readonly name: string
readonly severity: "critical" | "warning" | "info"
readonly message: string
readonly timestamp: Date
readonly labels: Record<string, string>
}
interface AlertRule {
readonly name: string
readonly condition: Effect.Effect<boolean>
readonly severity: "critical" | "warning" | "info"
readonly message: string
readonly labels: Record<string, string>
readonly forDuration: Duration.DurationInput
}
const createAlertRules = (metrics: {
errorRate: () => Effect.Effect<number>
latencyP99: () => Effect.Effect<number>
availability: () => Effect.Effect<number>
}): AlertRule[] => [
{
name: "HighErrorRate",
condition: metrics.errorRate().pipe(Effect.map((rate) => rate > 0.01)),
severity: "critical",
message: "Error rate exceeds 1%",
labels: { team: "backend", service: "api" },
forDuration: "5 minutes",
},
{
name: "HighLatency",
condition: metrics.latencyP99().pipe(Effect.map((p99) => p99 > 2)),
severity: "warning",
message: "P99 latency exceeds 2 seconds",
labels: { team: "backend", service: "api" },
forDuration: "10 minutes",
},
{
name: "LowAvailability",
condition: metrics.availability().pipe(Effect.map((avail) => avail < 99.9)),
severity: "critical",
message: "Availability below 99.9% SLO",
labels: { team: "backend", service: "api" },
forDuration: "5 minutes",
},
{
name: "ErrorBudgetLow",
condition: Effect.succeed(false),
severity: "warning",
message: "Error budget below 25%",
labels: { team: "backend", service: "api" },
forDuration: "0 seconds",
},
]
interface AlertState {
readonly firing: Map<string, { since: Date; alert: Alert }>
readonly resolved: Alert[]
}
const makeAlertManager = Effect.gen(function* () {
const state = yield* Ref.make<AlertState>({
firing: new Map(),
resolved: [],
})
const checkRule = (rule: AlertRule) =>
Effect.gen(function* () {
const isTriggered = yield* rule.condition
yield* Ref.modify(state, (s) => {
const firing = new Map(s.firing)
const resolved = [...s.resolved]
const key = rule.name
if (isTriggered) {
if (!firing.has(key)) {
firing.set(key, {
since: new Date(),
alert: {
name: rule.name,
severity: rule.severity,
message: rule.message,
timestamp: new Date(),
labels: rule.labels,
},
})
}
} else {
if (firing.has(key)) {
const prev = firing.get(key)!
resolved.push({
...prev.alert,
message: `[RESOLVED] ${prev.alert.message}`,
timestamp: new Date(),
})
firing.delete(key)
}
}
return [undefined, { firing, resolved }]
})
})
const getActiveAlerts = () =>
Ref.get(state).pipe(
Effect.map((s) => Array.from(s.firing.values()).map((f) => f.alert))
)
const getRecentResolved = () =>
Ref.get(state).pipe(Effect.map((s) => s.resolved.slice(-10)))
return {
checkRule,
getActiveAlerts,
getRecentResolved,
}
})
interface NotificationChannel {
readonly send: (alert: Alert) => Effect.Effect<void>
}
const slackChannel: NotificationChannel = {
send: (alert) =>
Effect.gen(function* () {
const emoji =
alert.severity === "critical"
? "🔴"
: alert.severity === "warning"
? "🟡"
: "🔵"
yield* Effect.log(`${emoji} [${alert.severity.toUpperCase()}] ${alert.name}`).pipe(
Effect.annotateLogs({
message: alert.message,
labels: JSON.stringify(alert.labels),
})
)
}),
}
const pagerDutyChannel: NotificationChannel = {
send: (alert) =>
Effect.gen(function* () {
if (alert.severity === "critical") {
yield* Effect.log("PagerDuty: Creating incident").pipe(
Effect.annotateLogs({ alert: alert.name })
)
}
}),
}
const runAlertEvaluation = (
rules: AlertRule[],
channels: NotificationChannel[],
interval: Duration.DurationInput
) =>
Effect.gen(function* () {
const alertManager = yield* makeAlertManager
const previousAlerts = yield* Ref.make(new Set<string>())
yield* Effect.forever(
Effect.gen(function* () {
for (const rule of rules) {
yield* alertManager.checkRule(rule)
}
const active = yield* alertManager.getActiveAlerts()
const current = new Set(active.map((a) => a.name))
const previous = yield* Ref.get(previousAlerts)
for (const alert of active) {
if (!previous.has(alert.name)) {
for (const channel of channels) {
yield* channel.send(alert)
}
}
}
yield* Ref.set(previousAlerts, current)
yield* Effect.sleep(interval)
})
)
})
const prometheusAlertRules = `
groups:
- name: effect-app-alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_errors_total[5m]))
/
sum(rate(http_requests_total[5m]))
> 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value | humanizePercentage }}"
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
) > 2
for: 10m
labels:
severity: warning
annotations:
summary: "High P99 latency"
description: "P99 latency is {{ $value }}s"
- alert: SLOViolation
expr: |
sum(rate(http_requests_total{status!~"5.."}[30m]))
/
sum(rate(http_requests_total[30m]))
< 0.999
for: 5m
labels:
severity: critical
annotations:
summary: "SLO violation"
description: "Availability is {{ $value | humanizePercentage }}"
`
Rationale:
Set up alerts based on user-facing symptoms (SLO violations) rather than system metrics (CPU usage).
Good alerting:
- Catches real problems - Alerts when users are affected
- Reduces noise - Fewer false positives
- Enables response - Actionable information
- Supports SLOs - Tracks service level objectives
Export Metrics to Prometheus
Rule: Use Effect metrics and expose a /metrics endpoint for Prometheus scraping.
Good Example:
import { Effect, Metric, MetricLabel, Duration } from "effect"
import { HttpServerResponse } from "@effect/platform"
const httpRequestsTotal = Metric.counter("http_requests_total", {
description: "Total number of HTTP requests",
})
const httpRequestsByStatus = Metric.counter("http_requests_by_status", {
description: "HTTP requests by status code",
})
const activeConnections = Metric.gauge("active_connections", {
description: "Number of active connections",
})
const requestDuration = Metric.histogram("http_request_duration_seconds", {
description: "HTTP request duration in seconds",
boundaries: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
})
const responseSizeBytes = Metric.summary("http_response_size_bytes", {
description: "HTTP response size in bytes",
maxAge: Duration.minutes(5),
maxSize: 100,
quantiles: [0.5, 0.9, 0.99],
})
const handleRequest = (path: string, status: number) =>
Effect.gen(function* () {
const startTime = Date.now()
yield* Metric.increment(httpRequestsTotal)
yield* Metric.increment(
httpRequestsByStatus.pipe(
Metric.tagged("status", String(status)),
Metric.tagged("path", path)
)
)
yield* Metric.increment(activeConnections)
yield* Effect.sleep("100 millis")
const duration = (Date.now() - startTime) / 1000
yield* Metric.update(requestDuration, duration)
yield* Metric.update(responseSizeBytes, 1024)
yield* Metric.decrement(activeConnections)
})
interface MetricSnapshot {
name: string
type: "counter" | "gauge" | "histogram" | "summary"
help: string
values: Array<{
labels: Record<string, string>
value: number
}>
buckets?: Array<{
le: number
count: number
labels?: Record<string, string>
}>
sum?: number
count?: number
}
const formatPrometheusMetrics = (metrics: MetricSnapshot[]): string => {
const lines: string[] = []
for (const metric of metrics) {
lines.push(`# HELP ${metric.name} ${metric.help}`)
lines.push(`# TYPE ${metric.name} ${metric.type}`)
for (const { labels, value } of metric.values) {
const labelStr = Object.entries(labels)
.map(([k, v]) => `${k}="${v}"`)
.join(",")
if (labelStr) {
lines.push(`${metric.name}{${labelStr}} ${value}`)
} else {
lines.push(`${metric.name} ${value}`)
}
}
if (metric.buckets) {
for (const bucket of metric.buckets) {
const labelStr = Object.entries(bucket.labels || {})
.map(([k, v]) => `${k}="${v}"`)
.concat([`le="${bucket.le}"`])
.join(",")
lines.push(`${metric.name}_bucket{${labelStr}} ${bucket.count}`)
}
lines.push(`${metric.name}_sum ${metric.sum}`)
lines.push(`${metric.name}_count ${metric.count}`)
}
lines.push("")
}
return lines.join("\n")
}
const metricsHandler = Effect.gen(function* () {
const metrics: MetricSnapshot[] = [
{
name: "http_requests_total",
type: "counter",
help: "Total number of HTTP requests",
values: [{ labels: {}, value: 1234 }],
},
{
name: "http_requests_by_status",
type: "counter",
help: "HTTP requests by status code",
values: [
{ labels: { status: "200", path: "/api/users" }, value: 1000 },
{ labels: { status: "404", path: "/api/users" }, value: 50 },
{ labels: { status: "500", path: "/api/users" }, value: 10 },
],
},
{
name: "active_connections",
type: "gauge",
help: "Number of active connections",
values: [{ labels: {}, value: 42 }],
},
{
name: "http_request_duration_seconds",
type: "histogram",
help: "HTTP request duration in seconds",
values: [],
buckets: [
{ le: 0.01, count: 100 },
{ le: 0.05, count: 500 },
{ le: 0.1, count: 800 },
{ le: 0.25, count: 950 },
{ le: 0.5, count: 990 },
{ le: 1, count: 999 },
{ le: Infinity, count: 1000 },
],
sum: 123.456,
count: 1000,
},
]
const body = formatPrometheusMetrics(metrics)
return HttpServerResponse.text(body, {
headers: {
"Content-Type": "text/plain; version=0.0.4; charset=utf-8",
},
})
})
Rationale:
Create metrics with Effect's Metric API and expose them via an HTTP endpoint in Prometheus text format.
Prometheus metrics enable:
- Real-time monitoring - See what's happening now
- Historical analysis - Track trends over time
- Alerting - Get notified of issues
- Dashboards - Visualize system health
Implement Distributed Tracing
Rule: Propagate trace context across service boundaries to correlate requests.
Good Example:
import { Effect, Context, Layer } from "effect"
import { HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "@effect/platform"
interface TraceContext {
readonly traceId: string
readonly spanId: string
readonly parentSpanId?: string
readonly sampled: boolean
}
class CurrentTrace extends Context.Tag("CurrentTrace")<
CurrentTrace,
TraceContext
>() {}
const TRACEPARENT_HEADER = "traceparent"
const TRACESTATE_HEADER = "tracestate"
const generateTraceId = (): string =>
Array.from(crypto.getRandomValues(new Uint8Array(16)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
const generateSpanId = (): string =>
Array.from(crypto.getRandomValues(new Uint8Array(8)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
const parseTraceparent = (header: string): TraceContext | null => {
const parts = header.split("-")
if (parts.length !== 4) return null
return {
traceId: parts[1],
spanId: generateSpanId(),
parentSpanId: parts[2],
sampled: parts[3] === "01",
}
}
const formatTraceparent = (ctx: TraceContext): string =>
`00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? "01" : "00"}`
const extractTraceContext = Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const traceparent = request.headers[TRACEPARENT_HEADER]
if (traceparent) {
const parsed = parseTraceparent(traceparent)
if (parsed) {
yield* Effect.log("Extracted trace context").pipe(
Effect.annotateLogs({
traceId: parsed.traceId,
parentSpanId: parsed.parentSpanId,
})
)
return parsed
}
}
const newTrace: TraceContext = {
traceId: generateTraceId(),
spanId: generateSpanId(),
sampled: Math.random() < 0.1,
}
yield* Effect.log("Started new trace").pipe(
Effect.annotateLogs({ traceId: newTrace.traceId })
)
return newTrace
})
const makeTracedHttpClient = Effect.gen(function* () {
const baseClient = yield* HttpClient.HttpClient
const trace = yield* CurrentTrace
return {
get: (url: string) =>
Effect.gen(function* () {
const childSpan: TraceContext = {
traceId: trace.traceId,
spanId: generateSpanId(),
parentSpanId: trace.spanId,
sampled: trace.sampled,
}
yield* Effect.log("Making traced HTTP request").pipe(
Effect.annotateLogs({
traceId: childSpan.traceId,
spanId: childSpan.spanId,
url,
})
)
const request = HttpClientRequest.get(url).pipe(
HttpClientRequest.setHeader(
TRACEPARENT_HEADER,
formatTraceparent(childSpan)
)
)
return yield* baseClient.execute(request)
}),
}
})
const withTracing = <A, E, R>(
handler: Effect.Effect<A, E, R | CurrentTrace>
): Effect.Effect<A, E, R | HttpServerRequest.HttpServerRequest> =>
Effect.gen(function* () {
const traceContext = yield* extractTraceContext
return yield* handler.pipe(
Effect.provideService(CurrentTrace, traceContext),
Effect.withLogSpan(`request-${traceContext.spanId}`),
Effect.annotateLogs({
"trace.id": traceContext.traceId,
"span.id": traceContext.spanId,
"parent.span.id": traceContext.parentSpanId ?? "none",
})
)
})
const serviceBHandler = withTracing(
Effect.gen(function* () {
const trace = yield* CurrentTrace
yield* Effect.log("Service B processing request")
yield* Effect.sleep("50 millis")
return HttpServerResponse.json({
message: "Hello from Service B",
traceId: trace.traceId,
})
})
)
const serviceAHandler = withTracing(
Effect.gen(function* () {
const trace = yield* CurrentTrace
yield* Effect.log("Service A processing request")
const tracedClient = yield* makeTracedHttpClient
const response = yield* tracedClient.get("http://service-b/api/data")
yield* Effect.log("Service A received response from B")
return HttpServerResponse.json({
message: "Hello from Service A",
traceId: trace.traceId,
})
})
)
const program = Effect.gen(function* () {
yield* Effect.log("=== Distributed Tracing Demo ===")
const incomingTrace: TraceContext = {
traceId: generateTraceId(),
spanId: generateSpanId(),
sampled: true,
}
yield* Effect.log("Processing traced request").pipe(
Effect.provideService(CurrentTrace, incomingTrace),
Effect.annotateLogs({
"trace.id": incomingTrace.traceId,
"span.id": incomingTrace.spanId,
})
)
})
Effect.runPromise(program)
Rationale:
Implement distributed tracing by propagating trace context through HTTP headers and using consistent span naming across services.
Distributed tracing shows the complete request journey:
- End-to-end visibility - See entire request flow
- Latency analysis - Find slow services
- Error correlation - Link errors across services
- Dependency mapping - Understand service relationships
Integrate Effect Tracing with OpenTelemetry
Rule: Integrate Effect.withSpan with OpenTelemetry to export traces and visualize request flows across services.
Good Example:
import { Effect } from "effect";
import { trace, context, SpanStatusCode } from "@opentelemetry/api";
function withOtelSpan<T>(
name: string,
effect: Effect.Effect<unknown, T, unknown>
) {
return Effect.gen(function* () {
const otelSpan = trace.getTracer("default").startSpan(name);
try {
const result = yield* effect;
otelSpan.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (err) {
otelSpan.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
throw err;
} finally {
otelSpan.end();
}
});
}
const program = withOtelSpan(
"fetchUser",
Effect.sync(() => {
return { id: 1, name: "Alice" };
})
);
Explanation:
- Start an OpenTelemetry span when entering an Effectful operation.
- Set status and attributes as needed.
- End the span when the operation completes or fails.
- This enables full distributed tracing and visualization in your observability platform.
Anti-Pattern:
Using Effect.withSpan without exporting to OpenTelemetry, or lacking distributed tracing, which limits your ability to diagnose and visualize complex request flows.
Rationale:
Connect Effect's tracing spans to OpenTelemetry to enable distributed tracing, visualization, and correlation across your entire stack.
OpenTelemetry is the industry standard for distributed tracing.
By integrating Effect's spans with OpenTelemetry, you gain deep visibility into request flows, performance bottlenecks, and dependencies—across all your services and infrastructure.