Files
gh-tenequm-claude-plugins-c…/skills/skill/references/observability.md
2025-11-30 09:01:09 +08:00

18 KiB

Observability

Monitoring, logging, and debugging Workers in production.

Logging

Real-time Logs (Wrangler Tail)

View logs in real-time during development and production.

# Tail all logs
wrangler tail

# Tail specific environment
wrangler tail --env production

# Filter by status
wrangler tail --status error
wrangler tail --status ok

# Filter by HTTP method
wrangler tail --method POST

# Filter by header
wrangler tail --header "User-Agent: Chrome"

# Filter by IP
wrangler tail --ip 203.0.113.1

# Search in logs
wrangler tail --search "database error"

# Sample rate (% of requests)
wrangler tail --sampling-rate 0.1  # 10%

# Pretty format
wrangler tail --format pretty

Console Logging

Use console methods in your Worker.

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Log levels
    console.log("Info message", { userId: 123 });
    console.info("Info message");
    console.warn("Warning message");
    console.error("Error message", error);
    console.debug("Debug message", { data });

    // Structured logging
    console.log(JSON.stringify({
      level: "info",
      timestamp: Date.now(),
      message: "Request processed",
      userId: 123,
      duration: 45,
    }));

    return new Response("OK");
  },
};

Workers Logs

Persistent logs stored in Cloudflare (Enterprise feature).

Enable via Dashboard:

  1. Workers & Pages → Logs
  2. Enable Workers Logs
  3. Set retention period (1-30 days)

Query logs:

# Via GraphQL API
curl -X POST https://api.cloudflare.com/client/v4/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "query": "query { viewer { accounts(filter: {accountTag: \"$ACCOUNT_ID\"}) { workersLogsData(filter: {datetime_gt: \"2025-01-01T00:00:00Z\"}) { logs { timestamp message } } } } }"
  }'

Filter logs:

// Add custom fields for filtering
console.log(JSON.stringify({
  level: "error",
  service: "api",
  endpoint: "/users",
  userId: "123",
  error: error.message,
}));

Logpush Integration

Stream logs to external services.

Supported destinations:

  • Amazon S3
  • Google Cloud Storage
  • Azure Blob Storage
  • Datadog
  • Splunk
  • New Relic
  • HTTP endpoint

Setup via Dashboard:

  1. Logs → Workers Logs → Create Job
  2. Select destination
  3. Configure filters
  4. Set fields to include

Fields available:

  • timestamp - Request timestamp
  • level - Log level (log, error, warn, info, debug)
  • message - Log message
  • scriptName - Worker name
  • outcome - Request outcome (ok, exception, exceededCpu, etc.)
  • logs - Array of console.log() messages

Custom Logging Service

Send logs to your own service.

interface LogEntry {
  level: "info" | "warn" | "error";
  message: string;
  timestamp: number;
  metadata?: Record<string, any>;
}

class Logger {
  constructor(private env: Env) {}

  private async send(entry: LogEntry) {
    // Send to logging service
    await fetch("https://logs.example.com/ingest", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${this.env.LOG_TOKEN}`,
      },
      body: JSON.stringify(entry),
    });
  }

  info(message: string, metadata?: Record<string, any>) {
    const entry: LogEntry = {
      level: "info",
      message,
      timestamp: Date.now(),
      metadata,
    };

    console.log(JSON.stringify(entry));
    this.send(entry).catch(console.error);
  }

  error(message: string, error: Error, metadata?: Record<string, any>) {
    const entry: LogEntry = {
      level: "error",
      message,
      timestamp: Date.now(),
      metadata: {
        ...metadata,
        error: error.message,
        stack: error.stack,
      },
    };

    console.error(JSON.stringify(entry));
    this.send(entry).catch(console.error);
  }
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const logger = new Logger(env);

    try {
      logger.info("Request received", {
        url: request.url,
        method: request.method,
      });

      const response = await handleRequest(request, env);

      logger.info("Request completed", {
        status: response.status,
      });

      return response;
    } catch (error) {
      logger.error("Request failed", error as Error, {
        url: request.url,
      });

      throw error;
    }
  },
};

Metrics and Analytics

Workers Analytics

View request metrics in the dashboard.

Available metrics:

  • Requests (count, rate)
  • Errors (count, rate)
  • Success rate
  • CPU time (p50, p99)
  • Duration (p50, p99)
  • Subrequests

Filter by:

  • Time range
  • Status code
  • Path
  • User agent
  • Country

GraphQL Analytics API

Query analytics programmatically.

const query = `
  query {
    viewer {
      accounts(filter: {accountTag: "${accountId}"}) {
        workersInvocationsAdaptive(
          filter: {
            datetime_gt: "2025-01-01T00:00:00Z"
            datetime_lt: "2025-01-02T00:00:00Z"
            scriptName: "my-worker"
          }
          limit: 100
        ) {
          sum {
            requests
            errors
            subrequests
          }
          quantiles {
            cpuTimeP50
            cpuTimeP99
            durationP50
            durationP99
          }
        }
      }
    }
  }
`;

const response = await fetch("https://api.cloudflare.com/client/v4/graphql", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ query }),
});

const data = await response.json();

Custom Metrics (Analytics Engine)

Write custom metrics to Analytics Engine.

Configuration:

[[analytics_engine_datasets]]
binding = "ANALYTICS"

Write data points:

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const start = Date.now();

    try {
      const response = await handleRequest(request, env);
      const duration = Date.now() - start;

      // Write metrics
      ctx.waitUntil(
        env.ANALYTICS.writeDataPoint({
          // String fields (up to 20)
          blobs: [
            request.url,
            request.method,
            String(response.status),
            request.headers.get("user-agent") || "unknown",
          ],
          // Numeric fields (up to 20)
          doubles: [
            duration,
            response.headers.get("content-length")
              ? parseInt(response.headers.get("content-length")!)
              : 0,
          ],
          // Indexed fields (up to 20) - for filtering
          indexes: [
            request.cf?.country as string || "unknown",
            request.cf?.colo as string || "unknown",
          ],
        })
      );

      return response;
    } catch (error) {
      const duration = Date.now() - start;

      ctx.waitUntil(
        env.ANALYTICS.writeDataPoint({
          blobs: [request.url, request.method, "error"],
          doubles: [duration],
          indexes: ["error"],
        })
      );

      throw error;
    }
  },
};

Query metrics:

SELECT
  blob1 AS url,
  blob2 AS method,
  blob3 AS status,
  COUNT() AS requests,
  AVG(double1) AS avg_duration,
  MAX(double1) AS max_duration
FROM ANALYTICS_DATASET
WHERE
  timestamp >= NOW() - INTERVAL '1' DAY
  AND index1 = 'US'
GROUP BY blob1, blob2, blob3
ORDER BY requests DESC
LIMIT 100

Traces (OpenTelemetry)

Export traces to observability platforms.

Supported platforms:

  • Datadog
  • New Relic
  • Honeycomb
  • Grafana Cloud
  • Sentry

Export to Honeycomb

import { trace } from "@opentelemetry/api";
import { WorkersSDK } from "@cloudflare/workers-honeycomb-logger";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const sdk = new WorkersSDK(request, env, ctx, {
      apiKey: env.HONEYCOMB_API_KEY,
      dataset: "my-worker",
    });

    const tracer = trace.getTracer("my-worker");

    return tracer.startActiveSpan("fetch", async (span) => {
      try {
        span.setAttribute("http.method", request.method);
        span.setAttribute("http.url", request.url);

        const response = await handleRequest(request, env);

        span.setAttribute("http.status_code", response.status);
        span.end();

        return response;
      } catch (error) {
        span.recordException(error as Error);
        span.end();
        throw error;
      }
    });
  },
};

Export to Datadog

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const startTime = Date.now();

    try {
      const response = await handleRequest(request, env);
      const duration = Date.now() - startTime;

      // Send trace to Datadog
      ctx.waitUntil(
        fetch("https://http-intake.logs.datadoghq.com/v1/input", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "DD-API-KEY": env.DATADOG_API_KEY,
          },
          body: JSON.stringify({
            ddsource: "cloudflare-workers",
            service: "my-worker",
            message: "Request completed",
            duration,
            status: response.status,
            url: request.url,
            method: request.method,
          }),
        })
      );

      return response;
    } catch (error) {
      // Log error to Datadog
      ctx.waitUntil(
        fetch("https://http-intake.logs.datadoghq.com/v1/input", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "DD-API-KEY": env.DATADOG_API_KEY,
          },
          body: JSON.stringify({
            ddsource: "cloudflare-workers",
            service: "my-worker",
            status: "error",
            error: {
              message: (error as Error).message,
              stack: (error as Error).stack,
            },
          }),
        })
      );

      throw error;
    }
  },
};

Error Tracking

Error Boundaries

Catch and track errors globally.

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    try {
      return await handleRequest(request, env, ctx);
    } catch (error) {
      // Log error
      console.error("Unhandled error:", error);

      // Send to error tracking service
      ctx.waitUntil(reportError(error as Error, request, env));

      // Return error response
      return Response.json(
        {
          error: "Internal server error",
          requestId: crypto.randomUUID(),
        },
        { status: 500 }
      );
    }
  },
};

async function reportError(error: Error, request: Request, env: Env) {
  await fetch("https://errors.example.com/report", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${env.ERROR_TOKEN}`,
    },
    body: JSON.stringify({
      error: {
        message: error.message,
        stack: error.stack,
        name: error.name,
      },
      request: {
        url: request.url,
        method: request.method,
        headers: Object.fromEntries(request.headers),
      },
      timestamp: Date.now(),
    }),
  });
}

Sentry Integration

npm install @sentry/browser
import * as Sentry from "@sentry/browser";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    Sentry.init({
      dsn: env.SENTRY_DSN,
      environment: env.ENVIRONMENT,
    });

    try {
      return await handleRequest(request, env);
    } catch (error) {
      Sentry.captureException(error);
      throw error;
    }
  },
};

Performance Monitoring

Request Timing

Track request performance.

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const timings = {
      start: Date.now(),
      auth: 0,
      database: 0,
      external: 0,
      total: 0,
    };

    // Auth
    const authStart = Date.now();
    const user = await authenticate(request, env);
    timings.auth = Date.now() - authStart;

    // Database
    const dbStart = Date.now();
    const data = await env.DB.prepare("SELECT * FROM users WHERE id = ?")
      .bind(user.id)
      .first();
    timings.database = Date.now() - dbStart;

    // External API
    const apiStart = Date.now();
    const externalData = await fetch("https://api.example.com/data");
    timings.external = Date.now() - apiStart;

    timings.total = Date.now() - timings.start;

    // Log timings
    console.log("Performance:", timings);

    // Add to response headers
    return Response.json(data, {
      headers: {
        "X-Timing-Auth": String(timings.auth),
        "X-Timing-Database": String(timings.database),
        "X-Timing-External": String(timings.external),
        "X-Timing-Total": String(timings.total),
      },
    });
  },
};

Performance API

Use the Performance API for detailed timing.

export default {
  async fetch(request: Request): Promise<Response> {
    performance.mark("start");

    performance.mark("db-start");
    await queryDatabase();
    performance.mark("db-end");
    performance.measure("database", "db-start", "db-end");

    performance.mark("api-start");
    await fetchExternal();
    performance.mark("api-end");
    performance.measure("external", "api-start", "api-end");

    performance.mark("end");
    performance.measure("total", "start", "end");

    // Get measurements
    const measurements = performance.getEntriesByType("measure");

    console.log("Performance measurements:", measurements);

    return Response.json({ ok: true });
  },
};

Debugging

Local Debugging

Debug Workers locally with DevTools.

# Start with inspector
wrangler dev --inspector

# Connect Chrome DevTools
# Open chrome://inspect in Chrome
# Click "inspect" on your Worker

Features:

  • Set breakpoints
  • Step through code
  • Inspect variables
  • View console logs
  • Profile performance

Remote Debugging

Debug production Workers.

Using console.log:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    console.log("Request:", {
      url: request.url,
      method: request.method,
      headers: Object.fromEntries(request.headers),
    });

    const response = await handleRequest(request, env);

    console.log("Response:", {
      status: response.status,
      headers: Object.fromEntries(response.headers),
    });

    return response;
  },
};

View logs:

wrangler tail --format pretty

Source Maps

Enable source maps for better error traces.

tsconfig.json:

{
  "compilerOptions": {
    "sourceMap": true
  }
}

wrangler.toml:

upload_source_maps = true

Debugging Tips

  1. Use structured logging - JSON format for easier parsing
  2. Log request IDs - Track requests across services
  3. Time operations - Identify performance bottlenecks
  4. Test locally first - Use wrangler dev before deploying
  5. Use staging environment - Test in production-like environment
  6. Monitor after deploy - Watch logs and metrics after deployment

Alerting

Custom Alerts

Send alerts based on metrics.

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    try {
      const response = await handleRequest(request, env);

      // Alert on slow requests
      const duration = Date.now() - startTime;
      if (duration > 5000) {
        ctx.waitUntil(sendAlert("Slow request", { duration, url: request.url }, env));
      }

      return response;
    } catch (error) {
      // Alert on errors
      ctx.waitUntil(sendAlert("Request error", { error: error.message }, env));
      throw error;
    }
  },
};

async function sendAlert(message: string, data: any, env: Env) {
  // Send to Slack
  await fetch(env.SLACK_WEBHOOK, {
    method: "POST",
    body: JSON.stringify({
      text: `🚨 ${message}`,
      blocks: [
        {
          type: "section",
          text: { type: "mrkdwn", text: `*${message}*` },
        },
        {
          type: "section",
          text: { type: "mrkdwn", text: `\`\`\`${JSON.stringify(data, null, 2)}\`\`\`` },
        },
      ],
    }),
  });
}

Health Checks

Implement health check endpoints.

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return healthCheck(env);
    }

    return handleRequest(request, env);
  },
};

async function healthCheck(env: Env): Promise<Response> {
  const checks = {
    database: false,
    kv: false,
    external: false,
  };

  // Check database
  try {
    await env.DB.prepare("SELECT 1").first();
    checks.database = true;
  } catch (error) {
    console.error("Database check failed:", error);
  }

  // Check KV
  try {
    await env.MY_KV.get("health-check");
    checks.kv = true;
  } catch (error) {
    console.error("KV check failed:", error);
  }

  // Check external API
  try {
    const response = await fetch("https://api.example.com/health", {
      signal: AbortSignal.timeout(2000),
    });
    checks.external = response.ok;
  } catch (error) {
    console.error("External API check failed:", error);
  }

  const allHealthy = Object.values(checks).every((c) => c);

  return Response.json(
    { healthy: allHealthy, checks },
    { status: allHealthy ? 200 : 503 }
  );
}

Additional Resources