> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reliantlabs.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Where the telemetry goes

> Where logs, traces, metrics and browser errors land in local development and on Reliant hosting: the local ClickStack, .forge/logs, the browser console mirror, the hosted store, retention and opting out

The app never learns where its telemetry goes. A forge service exports standard OpenTelemetry to whatever endpoint its environment hands it, and writes JSON logs to stdout. What changes between your laptop and production is the other end of that wire, so the same build behaves the same in both places and what you see in development is what you see in production, minus the platform-only data.

| | Local (`forge env up dev`) | Hosted (`forge env deploy <env>`) |
| - | - | - |
| Traces and metrics | OTLP to a local collector on `127.0.0.1` | OTLP to an endpoint the platform injects into every pod |
| Logs | Files in `.forge/logs/dev/`, tailed by the collector | Container stdout, collected by a node agent |
| Browser | Console mirrored to a log file, OTLP through the dev server's `/_otel` | OTLP through `/_otel` on your site's own origin |
| Store and UI | ClickStack (ClickHouse and the HyperDX UI) in one container | Your organization's isolated store, read in **Apps** |
| Sign-up or keys | None | None |

## Locally

`forge env up dev` starts a ClickStack as the `clickstack` service in `docker-compose.yml`: ClickHouse, the HyperDX UI and an OTLP collector in one container, about 0.8 GB resident. It is on by default, needs no account and no key, and publishes its ports on `127.0.0.1` only. `forge env up` and `forge env status dev` list the HyperDX URL under **Compose services**; its port is declared once as `_hyperdx_port` in `deploy/kcl/dev/main.k`. The first time you open it, three sources (**Logs**, **Traces**, **Metrics**) already exist, so search works immediately.

Host processes that `forge env up` starts get the standard variables, `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_PROTOCOL`, pointing at the collector. A value you set yourself, in your shell or in a workload's `env`, wins. Continuous profiles are pushed to a second collector and stored beside the rest.

To turn the local stack off, set `_observability = False` in `deploy/kcl/dev/main.k`. No container starts and no endpoint is exported. Trace context still propagates, so a request still carries its `traceparent` and a log line still carries its `trace_id`. To send telemetry somewhere else, declare a `destination` on the environment's `forge.Observability`; the local ClickStack is only the default one. Agents can load `observability/destination`.

### Check that the pipeline works

```bash theme={null}
forge env status dev                    # every runtime check
forge env status dev --signal traces    # one signal: traces, metrics, logs, profiles or app
forge env status dev --json
```

The checks read ClickHouse itself, so a pass means rows are in the store from the last 15 minutes, with the services that sent them named, not just that a container answers. A check that cannot get the facts it needs reports **UNDETERMINED**, never a pass. Three outcomes, not two. If the ClickStack is off the checks skip and say so.

> [!NOTE]
>
> These checks prove that the telemetry pipeline works. They do not prove your application flow works: containers, endpoints and ingestion can all be healthy while a flow is broken. To prove a flow, use an exit-coded assertion or `task test:e2e`. For judging a release against real traffic, see [`forge obs check`](https://docs.reliantlabs.io/forge/observability/forge-obs#obs-check-did-the-deploy-work).

### The log directory

`forge env up` tees every host process's output into one file per service:

```
.forge/logs/dev/
├── <workload>.log          one file per backend workload (JSON lines)
├── frontend_<name>.log     the dev server, plus the browser's console
└── …
```

These are plain files in your working directory, so they work with every tool you already have:

```bash theme={null}
tail -f .forge/logs/dev/<workload>.log
grep 0af7651916cd43dd8448eb211c80319c .forge/logs/dev/*.log     # one request, by trace id
```

The collector tails these same files to feed ClickStack, so what you grep and what HyperDX shows are one set of lines. A log record written with a request's context carries `trace_id` and `span_id`, so a line links to its trace. A line that is not JSON (a rebuild message, a panic) is kept as plain text under the same service. If you run several checkouts of a project, each worktree has its own `.forge/logs/` and its own stack.

### Browser console in the log directory

A scaffolded frontend calls `installDevLogging()` from `@reliantlabs/forge-web-runtime` in development. It forwards every `console.log`, `info`, `warn`, `error` and `debug` call to the dev server, and so does every uncaught error and unhandled promise rejection. The dev server prints them to stdout, and `forge env up` already tees that into `frontend_<name>.log`, each line prefixed with its level:

```bash theme={null}
grep '\[browser:' .forge/logs/dev/frontend_web.log | tail -40
grep '\[browser:error\]' .forge/logs/dev/*.log          # errors across every frontend
```

The uncaught-error capture is the part that matters. A `TypeError` that crashes a render, or a rejected promise, makes no `console` call anywhere, so searching your source for it finds nothing. It still lands in this file, with its stack. That makes "what did the user's browser do" a `grep`, with no browser to open and no screenshot to ask for.

Three things to know:

* **Development only.** The receiving endpoint is a Vite plugin or a Next.js route that does not exist in a production build, and the capture does nothing outside dev. An empty file in a deployed environment is expected. Deployed browser errors go to the store instead; see [Hosted dashboards, alerts and issues](https://docs.reliantlabs.io/forge/observability/dashboards-and-alerts.md#frontend-errors-issues-and-source-maps).
* **Lines arrive in batches.** Console calls are coalesced into one request every 250 ms (errors flush at once, and the buffer is sent when the page unloads), so `tail -f` lags the browser slightly. This keeps one request per log line from using up the browser's connections to your dev server.
* **It is ordinary scaffolded code you own.** One call in `src/main.tsx` or `src/app/providers.tsx` and one plugin entry. If a file has no `[browser:` lines at all, check those two places before concluding the code never ran.

## On Reliant hosting

An app you deploy to Reliant hosting reports on itself from its first deploy, with no configuration. The platform injects its endpoint and credential into every pod, so there is nothing to sign up for, no collector to run and no API key to paste into your code. Your services' logs, traces and metrics, your frontend's errors, page loads and sessions, and the platform's own view of each workload (restarts, memory against its limit, Kubernetes events) all land in a telemetry store that belongs to your organization, and the **Apps** area of the Reliant app reads it.

## What the platform collects

Each organization has its own isolated database in Reliant's ClickHouse-based store. Every environment you deploy writes into it, labelled with the environment's name, so one view can show all of them or one at a time.

| Signal | Where it comes from |
| - | - |
| **Traces** | Every RPC, every call between your components, and every database query is a span, with no instrumentation to write. The hosted gateway keeps every trace that contains an error and samples the healthy ones, so request, error and latency figures come from metrics rather than from counting spans. |
| **Metrics** | RPC rate, errors and latency per method, database pool and query metrics, Go runtime metrics, and any metric you declare in `internal/metrics/metrics.go`. |
| **Logs** | Your services' structured JSON logs on stdout, collected by the platform's node agent. A line logged with a request's context carries its trace ID, so a log entry links to the trace that produced it. |
| **Workload metadata** | Collected by the platform without your app's help: CPU and memory against each container's limits, restarts and the last exit reason, Kubernetes events, and the image each pod runs. |
| **Browser telemetry** | Uncaught errors and unhandled rejections, `console` output, fetch and XHR calls, page loads, web vitals (LCP, INP, CLS) and sessions, from the HyperDX browser SDK forge scaffolds into every Next.js and Vite frontend. |
| **Profiles** | Continuous CPU and live-heap profiles, sampled sparsely (see [Profile volume](#profile-volume)). Off on hosting unless the environment opts in ([Opting out](#opting-out)); on by default in the local stack. |
| **Session replay** | Off unless you opt in, in two places ([Session replay](https://docs.reliantlabs.io/forge/observability/dashboards-and-alerts.md#session-replay)). |

Browser data travels to the store through your own site: the browser posts to `/_otel` on the origin that served the page, and the platform routes that path to its ingest. There is no third-party host for an ad blocker to block, no CORS preflight, and no secret in your JavaScript bundle.

## Reading it in Apps

Open **Apps** from the sidebar and pick a project. An environment picker in the top bar scopes every page to one environment or to all of them; it defaults to `prod` and is remembered per project. Observability has its own section in the Apps navigation:

* **Observability** (the section's home) leads with requests, error rate, p95 latency and alerts firing, each compared with the previous window and marked with your releases. Below that, your services are sorted by what needs attention.
* **Logs**, **Traces** and **Profiles** are the explorers. Search, filter by service, severity or any attribute, and open a trace's waterfall from a log line.
* **Dashboards** and **Alerts** are described in [Hosted dashboards, alerts and issues](https://docs.reliantlabs.io/forge/observability/dashboards-and-alerts.md).

An environment's own page (**Environments → \<env>**) also has an **Observability** tab: the same views, pinned to that environment. The **Overview** shows each workload's CPU, memory, restarts and recent events.

A local environment has nothing here. `forge env up` keeps development telemetry in the ClickStack on your machine, and `forge env up` and `forge env status dev` print its HyperDX URL.

## Opting out

Observability is on for every hosted environment. You can turn all of it off, or any category, per environment:

```python theme={null}
observability = forge.Observability {enabled = False}        # everything

observability = forge.Observability {                        # or by category
    signals = forge.ObservabilitySignals {logs = False, metadata = False}
}
```

The categories are `traces`, `metrics`, `logs`, `profiles` (set `profiles = True` to turn continuous profiling on), `metadata` (the platform's workload data), `browser` and `replay`. A switched-off category is stopped at its source and never stored, so it is never billed. The change takes effect with the environment's next deploy. When `metadata` is off, the five workload alerts are left out of the release, since nothing would feed them.

You can also send an environment's telemetry to your own OpenTelemetry backend instead, by declaring a `destination`; agents can load the `observability/destination` skill for that.

## Retention and usage

The platform keeps telemetry for these periods:

| Data | Kept for |
| - | - |
| Logs | 3 days at DEBUG, 14 days at INFO, 30 days at WARN and above |
| Traces | 7 days |
| Metrics | 30 days |
| Profiles | 24 hours raw, 30 days aggregated |
| Browser errors | 30 days |
| Session replays | 14 days |
| Product events (funnels) | Up to 90 days without a paid compute subscription; up to 400 days with one, bounded by the environment's declaration |

Each organization includes **10 GiB of ingestion per UTC calendar month**, shared across its projects and environments, then billed per GB at the rate on the [pricing page](https://reliantlabs.io/pricing), with retention included. The pricing label GB means GiB (2³⁰ bytes). Ingestion is measured uncompressed after sampling and filtering, not as compressed disk space. **Settings → Billing & usage → Usage & limits → Observability** separates cumulative monthly ingestion from a timestamped snapshot of what's stored now.

Overage requires an in-force Stripe compute subscription, not just a saved card or wallet credit. The default \$50 monthly observability cap limits the chargeable ledger even while gateway enforcement catches up. At a funded cap, the gateway keeps ERROR+ logs, error-status spans and metrics, and drops profiles and other logs/spans. Without a qualifying subscription, reduced collection starts at 10 GiB and becomes stricter at 15 GiB; no overage is charged.

See [Observability billing & limits](https://docs.reliantlabs.io/account/observability-billing.md) for the worked bill, browser billing ceiling, cap changes and conflicts, usage notices, meter lag and free query fair-use lanes.

### Profile volume

Profiles are the most expensive signal per useful byte: every sample is stored as a row that repeats its whole stack, so a profile costs far more than its size on the wire. forge's defaults are therefore sparse, and a project gets them by pinning the forge release that ships them:

| | Before | Now |
| - | - | - |
| CPU profile | 10 seconds, every minute | 5 seconds, every 5 minutes |
| Live heap | every 15 minutes | every 2 hours |
| Allocation, goroutine, mutex, block | off | off (opt in per workload) |
| Raw retention (`--trace` lookups, short ranges) | 72 hours, then 48 | 24 hours |
| Aggregates (range flamegraphs) | 30 days | 30 days |

A model of Reliant's fleet predicts about **7.6× fewer expanded profile-row bytes** (2.56 GiB per day down to 0.336 GiB), excluding an older-pin staging service. This is not an achieved reduction or a compressed-disk/billing forecast; those depend on storage codecs, merges and workload behavior. Over an hour, a long-lived process has about a minute of wall-clock CPU capture, useful for sustained hotspots. Brief incidents or specific traces may have no samples at all. Captured trace-linked samples remain queryable in the raw tier for 24 hours; the aggregates retain range flamegraphs for 30 days.

To sample faster for a while on one workload, set the cadence in its `env` and revert it afterwards: `FORGE_PROFILES_CPU_INTERVAL` (default `5m`, at least `30s`), `FORGE_PROFILES_CPU_DURATION` (default `5s`, at most half the interval) and `FORGE_PROFILES_HEAP_INTERVAL` (default `2h`). The `profiling` block of a workload picks which types are captured and the heap interval; agents can load the `observability/profiles` skill for the details. To keep profiles off entirely, leave `signals.profiles` unset on a hosted environment.

## When nothing shows up

* **Local environments have none.** `dev` keeps its telemetry in the ClickStack on your machine; [`forge env status dev`](#check-that-the-pipeline-works) prints the HyperDX URL and checks that traces, metrics and logs are arriving.
* **Data appears a little after a deploy.** The first data arrives once the new pods are serving traffic.
* **A category is switched off.** Check the environment's `observability` block for `enabled = False` or a signal set to `False`. Profiles and session replay are off on hosting until you turn them on.
* **The frontend sends nothing.** A production build sends browser telemetry only when its OTLP endpoint is configured; a scaffolded frontend deployed to hosting is set up for it. `signals.browser = False` turns it off.
* **Alerts never notify.** A route names a receiver that is not set up in **Observability → Alerts → Receivers**; that tab lists routes with no receiver.
* **Usage is over the included ingestion or the cap.** Without a qualifying compute subscription, collection is reduced past 10 GiB; at a funded cap only ERROR+ logs, error-status spans and metrics are kept. **Settings → Billing & usage → Usage & limits → Observability** shows the state.

## Related topics

[What every boundary records](https://docs.reliantlabs.io/forge/observability/signals.md) explains what produces these signals. [Querying with forge obs](https://docs.reliantlabs.io/forge/observability/forge-obs.md) reads them from the command line, locally or hosted. [Environments & deploys](https://docs.reliantlabs.io/forge/environments-and-deploys.md) covers releases and promotion.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.