Skip to main content
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.

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

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.
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.

The log directory

forge env up tees every host process’s output into one file per service:
These are plain files in your working directory, so they work with every tool you already have:
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:
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.
  • 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. 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.
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:
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: 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, 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 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: 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 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.
What every boundary records explains what produces these signals. Querying with forge obs reads them from the command line, locally or hosted. Environments & deploys covers releases and promotion.