# ctx.log

> Reference for ctx.log with the info, warn, error, and debug signatures, metadata serialization, trace correlation, and dev versus production transport.

`ctx.log` is the structured logger. Each call emits one OpenTelemetry log record correlated with the current execution's trace, so log lines appear inline with HTTP, database, and KV spans on the execution timeline. Use it instead of `console.log`, which is captured only best-effort.

## ContextLogger

```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;
}
```

| Parameter | Type | Description |
|---|---|---|
| `message` | `string` | The log record body |
| `metadata` | `Record<string, unknown>` (optional) | Structured context, serialized reliably |

All four methods are synchronous and return `void`; the level sets the record's severity.

## How metadata is carried

Metadata is serialized into a single JSON attribute on the log record (`tray.log.meta`) rather than exploded into one attribute per key. This keeps attribute cardinality bounded while preserving the full object for display and search.

## Correlation with the execution trace

Every record carries the trace and span IDs of the active execution span, plus resource attributes identifying the tenant: `executionId`, `projectId`, `workspaceId`, `organizationId`, `functionPath`, and `method`. In production these are stamped server side at ingest from the verified execution token, so nothing the function process claims about identity is trusted. The dashboard interleaves log records with auto-instrumented spans into one time-ordered timeline per execution, filterable by any of those attributes.

## Dev versus production transport

The code is identical in both environments; the runtime picks the transport. Under `helix dev`, `ctx.log` pretty-prints to the terminal (colored level, timestamp, message, and metadata inline) with no export. In deployed Lambda mode, records are exported as OTLP/protobuf through a batching processor to the Helix ingest endpoint, authenticated per invocation with the short-lived execution token, and flushed at the end of every execution (on crash, whatever was buffered is flushed along with the error). Because the payload is standard OTLP, the same stream can also be pointed at any OTel-compatible backend.

## Example

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

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 contacts = await ctx.http
    .authed('salesforce_prod')
    .get('{{instance_url}}/services/data/v59.0/query', {
      searchParams: { q: 'SELECT Id, Name FROM Contact LIMIT 10' },
    })
    .json<{ records: unknown[] }>();

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

  if (contacts.records.length === 0) {
    ctx.log.warn('Nothing to sync');
  }

  return { synced: contacts.records.length };
});
```

The execution timeline, auto-instrumented spans, `console.log` capture limits, and `helix logs` filtering are covered in [the observability guide](/documentation/guides/observability/).

---

Canonical: https://helix.tray.ai/documentation/reference/context/log/
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