# Cloudflare Tail Workers logging

## Start logging in 5 minutes

Forward `console.log` output, uncaught exceptions and failed invocations from your existing [Cloudflare Workers](https://workers.cloudflare.com) to Better Stack, without changing their code. A [Tail Worker](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) receives these events right after each invocation and sends them on.

[info]
#### On Cloudflare's Free plan?
Tail Workers need the Workers Paid or Enterprise plan. On the Free plan, [collect logs from your Workers with our library](https://betterstack.com/docs/logs/cloudflare-worker/) instead.
[/info]

### Create a source

[Create a new HTTP source](https://telemetry.betterstack.com/team/0/sources/new?platform=http ";_blank") in Better Stack.

### Create the Tail Worker

Create a new Worker called `better-stack-tail`:

```bash
[label Create a new Worker]
npm create cloudflare@latest -- better-stack-tail --type=hello-world --lang=js --no-deploy
cd better-stack-tail
```

Replace the contents of `src/index.js` with:

```js
[label src/index.js]
export default {
  async tail(events, env, ctx) {
    const logs = []

    for (const event of events) {
      const context = {
        worker: event.scriptName,
        outcome: event.outcome,
        request: event.event?.request && {
          method: event.event.request.method,
          url: event.event.request.url,
        },
      }

      for (const log of event.logs) {
        const message = log.message
          .map((part) => (typeof part === "string" ? part : JSON.stringify(part)))
          .join(" ")
        logs.push({ ...context, dt: log.timestamp, level: log.level, message })
      }

      for (const exception of event.exceptions) {
        logs.push({ ...context, dt: exception.timestamp, level: "error", message: `${exception.name}: ${exception.message}` })
      }

      // Record failed invocations without an exception, e.g. exceeded CPU or memory limits
      if (!["ok", "canceled"].includes(event.outcome) && event.exceptions.length === 0) {
        logs.push({ ...context, dt: event.eventTimestamp, level: "error", message: `Worker invocation ended with outcome ${event.outcome}` })
      }
    }

    if (logs.length === 0) return

    ctx.waitUntil(
      fetch("https://$INGESTING_HOST", {
        method: "POST",
        headers: {
          "Authorization": "Bearer $SOURCE_TOKEN",
          "Content-Type": "application/json",
        },
        body: JSON.stringify(logs),
      })
    )
  },
}
```

Deploy the Tail Worker:

```sh
[label Deploy the Tail Worker]
npx wrangler deploy
```

### Connect your Workers

Add `better-stack-tail` as a tail consumer in the Wrangler configuration of each Worker you want to collect logs from:

[code-tabs]
```json
[label wrangler.jsonc]
{
  "tail_consumers": [{ "service": "better-stack-tail" }]
}
```
```toml
[label wrangler.toml]
[[tail_consumers]]
service = "better-stack-tail"
```
[/code-tabs]

Redeploy each of those Workers:

```sh
[label Redeploy your Worker]
npx wrangler deploy
```

### View your logs

Send a request to one of your Workers and open [Better Stack → Live tail](https://telemetry.betterstack.com/team/0/tail ";_blank")

Each log carries the Worker's name in `worker`, the request's `method` and `url` in `request`, and the invocation's `outcome`.

## Need help?

Please let us know at hello@betterstack.com.  
We're happy to help! 🙏
