Service catalog

Services are discovered from telemetry, never created by hand. The six annotations, where to set them, and what status means.

Service catalog

A service in upzero is never created. It is discovered: the moment a span, log or metric carrying a service.name resource attribute reaches your organization, a background workflow writes a row for it. There is no "Add service" button anywhere in the console, no POST /api/v1/services, and no up0 services create. If you want a service in the catalog, send it telemetry under that service.name.

Discovery is a batch, not instant

A new service is not visible the second its first span lands. Discovery runs on its own schedule, targeting roughly ten minutes of freshness. If a service you just started emitting from is missing, wait a cycle before assuming something is wrong.

What gets written

Discovery owns exactly four columns on a service row: name, first_seen_at, last_seen_at and status. Each discovery pass upserts on (org_id, name), so re-observing a known service advances last_seen_at and never creates a second row, no matter how many times the batch runs.

status is one of two values:

StatusMeaning
activeTelemetry has arrived recently.
dormantNothing has been seen from this service for over an hour.

A dormant service is not deleted and does not disappear. Its row, and any annotations on it, stay exactly where they are. The catalog is a Postgres table, not a view over telemetry, precisely so a service survives its signals aging out of ClickHouse's retention window: a quiet service should read as quiet, not vanish along with data it never depended on to exist.

Renaming a service on your side (a new service.name on the next deploy) is not a rename to upzero. The old name goes dormant on its own schedule and the new name is discovered as a new row. Nothing merges the two by guesswork.

Discovery is scoped to your organization

Two organizations can send telemetry under the identical service.name and each ends up with its own row. The natural key is (org_id, name), not name alone, so there is no cross-tenant collision and no visibility into another organization's services, however they are named.

Annotations

Discovery never writes an owner, a tier, an environment or a link. Those six fields are annotations, and they are the one thing a human (or a manifest) is allowed to set:

FieldPurpose
ownerTeam or person responsible
tierFree-form criticality label, with no fixed vocabulary
environmentFree-form deployment environment
repoUrlLink to the service's source repository
runbookUrlLink to its runbook
dashboardUrlLink to its dashboard

All six are optional, all six are plain strings, and the three link fields must be an absolute http:// or https:// URL if set at all. A bare string like not a url or a javascript: scheme is rejected.

You can set them three ways:

Open a service's detail page and use the annotation editor beneath its identity and links section. The form opens pre-filled with whatever is already set, not blank fields, and clearing a field back to empty is a real action, distinguishable from never having set it.

The list

Open Services in the console to see every service discovered in your organization, filterable by status and by tier. Both filters live in the URL, so a filtered view is a link you can share. A filter that matches nothing says so directly ("Services exist in this organization, just none with the current status or tier"), which reads differently from an organization with nothing discovered at all.

Empty service catalog, before any telemetry has been discovered

Before any telemetry has arrived, the list explains what would make something appear rather than leaving a blank panel: "No services discovered yet. Services appear here once telemetry carrying a service name arrives. Check that your instrumentation sets service.name and is reaching this organization."

Seeing it happen

Send a synthetic signal and watch a service appear:

terminal
up0 tokens create --name ci-token --signals traces
up0 ingest test --signal traces --token <token>

up0 ingest test sends one span under a generated service.name and polls the Query API until it lands, confirming ingestion works end to end. The service itself will not show up in the catalog until the next discovery cycle runs, roughly ten minutes later, even though the signal itself arrives in seconds.

Discovery is off by default on a local kind cluster

The Temporal workers that run discovery (correlation-harness-fast/-slow) are disabled by default in both kind profiles (deploy/charts/up0/values.yaml, decision #903). Traces sent to a local kind cluster's OTLP endpoint are ingested and queryable immediately, but no service row appears in the catalog until that worker is enabled and has run at least one cycle. The same is true on staging today, so do not expect a populated catalog there either. This is an environment default, not something a production deployment does.

On this page