# Observability

> Write structured logs with ctx.log, then read one traced entry per execution in the Helix dashboard. Local runs pretty-print to your terminal.

Every function execution is observable without extra setup. Use `ctx.log` in your code for structured records; open an execution in the Helix dashboard to see the full trace of that request.

What you get today:

- **Execution logs in the dashboard** at [https://app.helix.tray.ai](https://app.helix.tray.ai). One entry per execution, HTTP request or scheduled job run; opening an entry shows the operations it ran, the third-party calls it made, and the request and response payloads.
- **`ctx.log` in your code**, exactly as shown below. It is the supported way to add your own structured records to an execution.
- **Local logs in your terminal.** `helix dev --pretty-logs` is the default and prints readable lines; `helix dev --raw-logs` prints the raw structured JSON instead.

## Write logs with ctx.log

`ctx.log` is the primary logging interface. It produces structured, execution-scoped log records that appear on the execution's timeline in the dashboard.

```typescript
interface ContextLogger {
  info(message: string, metadata?: Record<string, unknown>): void;
  warn(message: string, metadata?: Record<string, unknown>): void;
  error(message: string, metadata?: Record<string, unknown>): void;
  debug(message: string, metadata?: Record<string, unknown>): void;
}
```

```typescript
// functions/api/v1/sync.post.ts
import { defineFunction } from '@trayai/helix-sdk';
import { z } from 'zod';

export const input = z.object({
  userId: z.string(),
});

export default defineFunction<typeof input>(async (ctx) => {
  ctx.log.info('Starting sync', { userId: ctx.input.userId });

  const res = await ctx.http
    .authed('salesforce_prod')
    .get('https://acme.my.salesforce.com/services/data/v59.0/query', {
      query: { q: 'SELECT Id, Name FROM Contact LIMIT 10' },
    });
  const contacts = res.body as { records: Array<{ Id: string; Name: string }> };

  ctx.log.info('Fetched contacts', { count: contacts.records.length });

  await ctx.kv().set(`sync:last:${ctx.input.userId}`, {
    count: contacts.records.length,
    at: new Date().toISOString(),
  });

  ctx.log.warn('Duplicate check skipped (feature flag disabled)');
  return { synced: contacts.records.length };
});
```

Each call records:

- **Timestamp**
- **Severity** from the level (`info`, `warn`, `error`, `debug`)
- **Body**, the message string
- **Attributes**, your metadata object
- **Correlation** with the current execution, so the record lands on the right timeline

In local dev, `ctx.log` also pretty-prints to the terminal for immediate visibility.

The dashboard shows spans and correlated log records on one time-ordered execution timeline:

```
exec_789xyz | 2025-03-19T10:30:00.123Z | POST /api/v1/sync

10:30:00.123  LOG  INFO   Starting sync { userId: "user_abc" }
10:30:00.145  SPAN HTTP → GET https://acme.my.salesforce.com/services/data/v59.0/query
                          auth: salesforce_prod
10:30:00.412  SPAN HTTP ← 200 OK (267ms)
10:30:00.413  LOG  INFO   Fetched contacts { count: 10 }
10:30:00.415  SPAN KV   → SET sync:last:user_abc
10:30:00.418  SPAN KV   ← OK (3ms)
10:30:00.450  LOG  WARN   Duplicate check skipped (feature flag disabled)
10:30:00.451  ←    200 OK { synced: 10 } (328ms total)
```

Full API reference: [ctx.log](/documentation/reference/context/log/).

## Auto-instrumented operations

HTTP calls and KV operations appear as steps on the execution timeline without extra instrumentation.

**HTTP calls.** Authenticated calls via `ctx.http.authed(...)` capture the request method, URL, headers (credentials redacted), and body; the response status, headers, and body (truncated if large); the duration; and the auth alias used, plus whether a token refresh occurred. Unauthenticated calls capture method, URL, headers, response status, and duration.

**KV operations.** Every `ctx.kv()` operation produces a step with the operation type (get, set, delete, incr, list), the key (or prefix for list), and the duration.

## Dev and production

You write the same code in both environments.

- **Local dev (`helix dev`).** `ctx.log` pretty-prints to the terminal: colored level, readable timestamp, message, and metadata inline. Use `--raw-logs` for the structured JSON form.
- **Production.** The same `ctx.log` calls appear on the execution timeline in the [Helix dashboard](https://app.helix.tray.ai). Open an execution to see logs interleaved with the operations that ran.

## console.log capture

`console.log`, `console.warn`, `console.error`, and `console.debug` are captured best-effort alongside `ctx.log`, so functions that haven't adopted `ctx.log` still get some observability without refactoring. Prefer `ctx.log` for anything you need to rely on:

- **No structured attributes.** `console.log('foo', { bar: 1 })` produces a string; `ctx.log.info('foo', { bar: 1 })` produces a log record with queryable attributes.
- **Third-party library output.** Captured with the execution, but may lack useful structure.
- **Serialization.** Complex objects may not serialize cleanly; `ctx.log` serializes reliably.

| Scenario | Use |
|---|---|
| New code, structured logging | `ctx.log.info('message', { key: value })` |
| Quick debugging during development | `console.log(...)`, goes to the terminal plus best-effort capture |
| Existing function that hasn't adopted `ctx.log` | `console.log(...)`, captured best-effort |
| Production-critical logging | `ctx.log`, always reliable and structured |

## Error tracking

Unhandled errors are captured automatically on the same execution timeline. The record includes the stack trace with source-mapped file paths, and the timeline shows everything up to the failure plus the request that triggered it:

```
exec_abc123 | 2025-03-19T10:30:00.123Z | GET /api/v1/users/:userId (ERROR)

10:30:00.123  LOG  INFO   Fetching user
10:30:00.200  SPAN HTTP → GET https://acme.my.salesforce.com/...
10:30:00.450  SPAN HTTP ← 401 Unauthorized (250ms)
10:30:00.451  LOG  ERROR  TypeError: Cannot read property 'records' of undefined
                          at handler (functions/api/v1/users/[userId].get.ts:15:34)
10:30:00.452  ←    500 Internal Server Error (329ms total)
```

## Read logs

Open the project's **Logs** tab in the [Helix dashboard](https://app.helix.tray.ai). Each entry is one execution, an HTTP request or a scheduled job run; open it for the full execution timeline. See [the dashboard](/documentation/governance/dashboard/) for the walkthrough.

---

Canonical: https://helix.tray.ai/documentation/guides/observability/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt