horizon.yaml Reference
horizon.yaml is the single configuration file for the Horizon BFF. Validation runs at startup and again on every hot reload. A file that fails validation is rejected; the BFF keeps the previously valid config rather than serving with broken settings.
This page is the top-level map. Each subsection has its own detail page:
| Section | Purpose | Details |
|---|---|---|
server |
HTTP listener and static asset path. | server |
templates |
Template source mode: OAP-backed (live) or local read-only bundle (readonly). |
below |
security |
Which outbound link domains a dashboard template may point at. | security |
audit |
Optional durable record of who signed in, when and from where. Off by default; Postgres-backed. | login-audit |
oap |
OAP query / admin / Zipkin URLs, timeouts, basic-auth, MQE override. | oap |
auth |
Active backend (local or LDAP), local users, LDAP binding, break-glass, single sign-on, API tokens. | auth |
rbac |
Role definitions, permission grants, landing route per role. | rbac |
session |
Cookie name, TTL, secure flag. | session |
debugLog |
Wire-level request/response log for troubleshooting. | debugLog |
query |
Per-request query limits (layer-landing service cap, Overview top-N). | below |
sourceMaps |
In-memory source-map budgets + static mount for the Browser Logs tab. | Browser Logs & Source Maps |
ai |
AI assistant: provider, model, credentials, prompt overrides, history cap. | AI Assistant |
mcp |
Whether an external agent may read this Horizon over the Model Context Protocol. | MCP |
oauth |
Horizon as an authorization server, so an MCP client can sign its operator in through a browser. Off by default. | MCP |
performance |
How hard the BFF fans queries out to OAP, plus render caps and the largest page a list may display. | below |
layers |
Layers to hide from the sidebar. | below |
Top-level shape
server: { host, port, staticDir?, publicUrl?, trustProxy? } # publicUrl = the PUBLIC base URL
templates: { mode? } # live (default) | readonly
security:
trustedLinkDomains?: string[] # outbound link allow-list for templates
oap:
queryUrl: string
adminUrl: string
zipkinUrl?: string
timeoutMs?: number
auth?: { username, password }
mqe?: { host?, port? }
auth:
backend: local | ldap
local?: { users: [{ username, passwordHash, roles? }] }
ldap?: { ... }
breakGlass?: { username, passwordHash, roles? }
sso?: # single sign-on — Horizon as the CLIENT
providers: [{ id, displayName?, kind?, issuer?, clientId, clientSecret?,
allowedDomains? }] # HOW you sign in, per provider
roles?: { defaultRoles?, roleByEmail?, roleByDomain? } # WHAT you get, ONE table
tokensFile?: string # path to API tokens for callers with no browser
rbac:
enabled?: boolean
roles?: { <name>: [verb, ...] }
landingByRole?: { <name>: "/route" }
session: { ttlMinutes?, cookieName?, cookieSecure? }
debugLog: { enabled?, file?, maxBodyChars?, redactAuthHeaders? }
query: { landingServiceCap?, overviewTopN? }
sourceMaps: { enabled?, maxFileBytes?, maxTotalBytes?, maxFileCount?, bootMountDir? }
ai:
enabled?: boolean # off by default
provider?: openai-compatible | bedrock
model?: string
baseUrl?: string # openai-compatible only
region?: string # bedrock only; blank → AWS_REGION / AWS_DEFAULT_REGION
apiKey?: string # secret — set via env interpolation only
systemPrompt?: string # blank → bundled default
starters?: [string, ...] # blank → bundled defaults
history?: { maxMb? }
mcp:
enabled?: boolean # ON by default
name?: string # what this deployment calls itself to an agent
oauth: # authorization server — OFF by default
enabled?: boolean
issuer?: string # defaults to server.publicUrl
signingKey?: string # secret — env only, 32+ chars (openssl rand -base64 32)
accessTokenMinutes?: number
refreshTokenDays?: number # 0 disables refresh tokens
clientMetadataHosts?: [string, ...] # hosts whose client-metadata URLs are accepted
performance:
bulk:
topology: { nodeBulkSize?, edgeBulkSize?, concurrency? }
infra3d: { metricBulkSize?, metricConcurrency?, topologyConcurrency?, templateConcurrency? }
landing: { bulkSize?, concurrency? }
dashboard: { bulkSize? }
limits:
topologyMaxNodes?: number
topologyMaxEdges?: number
maxPageSize: { traces?, logs?, browserLogs?, events? }
layers: { excluded?: [{ key, reason? }] }
The ai block’s fields, env-var forms, and provider recipes are documented on AI Assistant.
ai and mcp are independent. ai configures the model this Horizon talks to for its own assistant panel, and is off until you give it one. mcp lets an external agent — Claude Code, Codex, Claude Desktop — read this Horizon through the Model Context Protocol, with the model staying on the caller’s side; it needs no provider and no key, so it is on by default. Turn it off with enabled: false (or HORIZON_MCP_ENABLED=false) and POST /api/mcp answers 503.
oauth is a third, separate thing again: it makes Horizon an authorization server — the role Google plays when an application offers “Sign in with Google”, except here Horizon is the one issuing and the login page shown is its own. It lets an MCP client send its operator through a browser login instead of being handed a token. It is off by default and needs two values to start — a public base URL (from server.publicUrl, or oauth.issuer to override it) and signingKey, a secret. Both are required: with either missing the server stays off and its endpoints answer 404, and a warning names what is missing at boot. See MCP.
Environment variable interpolation
${VAR} and ${VAR:default} are expanded before YAML parsing.
${VAR}— fail-loud. Expands to the env var; if unset, expands to empty string and the schema decides whether empty is valid. Use for secrets so a missing env var stops startup.${VAR:default}— fail-soft. Expands to the env var, or the literaldefaultif unset. Use for optional non-secret values.
oap:
auth:
password: "${HORIZON_OAP_PW}" # fails loud if unset
ldap:
bindPassword: "${HORIZON_LDAP_PW:}" # empty if unset (works for anonymous bind)
Bootstrap rules
The BFF validates the file shape at startup and on every hot reload. Schema errors still reject the file; auth bootstrap gaps are softer so a first-run container can render the login page with a setup-required banner.
Auth gaps that boot with a warning but reject login:
auth.backend: localandauth.local.usersis empty.auth.backend: ldapandauth.ldapblock is missing.auth.backend: ldapandauth.ldap.groupMappingsis empty.
There is no “default admin/admin” fallback.
Warnings (do not block startup)
These combinations are legal but risky or ineffective; the BFF boots and logs a warning:
auth.backend: ldapbutauth.local.userspopulated → local users will be ignored.auth.breakGlassconfigured whileauth.backend: local→ break-glass only exists as an LDAP-outage fallback, so the block is unused.debugLog.enabled: truewithredactAuthHeaders: false→ outbound OAP basic-auth credentials are written to the wire log in clear text.debugLog.enabled: trueat startup → a reminder that every outbound OAP request/response is being appended to the wire log (very verbose).session.cookieSecure: falseoutside development → session cookies are sent over plain HTTP. Fine for localhost; set it totrue(behind HTTPS) in production.
Hot reload behavior
The config is re-read on file change and the new values take effect without a restart. An edit that fails validation does not apply: the BFF logs an error listing each failing field path, and the previous valid config keeps serving until the file is fixed.
Applied live:
- Auth backend selection (re-evaluated on next login).
- RBAC roles and policy (re-evaluated on next route call).
- OAP URLs and credentials (used on next outbound call).
- Session TTL (applies to every session immediately, already-signed-in ones included — see Sessions).
sourceMaps.enabled,sourceMaps.maxTotalBytes,sourceMaps.maxFileCount— applied on the next source-map upload / resolve / list. Lowering a budget trims the in-memory uploaded set then (least-recently-used first). It does not shrink maps already loaded from the static mount — see below. One caveat onenabled: turning it on in a live file enables uploads and resolves, but the static mount is scanned only at startup, so a BFF that booted withsourceMaps.enabled: falsehas an empty mounted set until it restarts.debugLogin full —enabled,fileand the redaction flags. Changingfilerotates the wire log to the new path, finishing the write in flight on the old one.security.trustedLinkDomains— applied the next time a template’s outbound link is published or read back.
These changes require a process restart:
server.host,server.port— the listener already bound.server.staticDir— the static root is registered with the HTTP server at startup.server.trustProxy— the HTTP server is constructed once with this value, so a live edit is ignored until restart.- The whole
auditblock — the store is opened and the writer started at boot, so turning the login audit on, off, or repointing it takes effect on the next restart. templates.mode— the template source is chosen at boot (the OAP seed either ran or was skipped). Editing it in a live file logs a warning and keeps the boot-time mode until restart.- Nothing about OAP capability detection. The schema-introspection and trace-API probes are cached for five minutes and keyed by
oap.queryUrl, so an OAP upgrade is picked up within that window and repointing OAP re-probes at once — neither needs a restart. sourceMaps.bootMountDir— the static source-map directory is scanned once at startup, so a new directory (and newly-dropped.mapfiles) needs a restart. The count of maps loaded from that mount is fixed by the startup scan as well: loweringsourceMaps.maxFileCountafterwards trims only the in-memory uploaded set, never the already-mounted maps — restart to re-scan a mount against a lower count.- Raising
sourceMaps.maxFileBytes— the multipart upload size limit is fixed at startup; lowering it applies live.
Template source mode
templates:
mode: live # default; or: readonly
templates.mode selects where dashboard / overview templates live:
live(default) — at boot, Horizon seeds any missing bundled templates into OAP through the OAP 11/ui-management/templates*REST API, then reads and writes templates through that API. Admin edits (layer dashboards, overview templates, translations) persist in OAP’sui_templatestorage, independent of the Horizon instance.readonly— templates render from the local bundle only. Horizon does not call an OAP template-management API and the template admin surface is read-only. OAP’s query API (metrics / traces / logs) is still used and health-checked either way. This is required on OAP 10.x: OAP 10 has persistent template management through legacy query-port GraphQL, but Horizon implements only the OAP 11/ui-management/templates*REST protocol. Inlivemode that protocol mismatch leaves the store unreachable, and Horizon blocks layer-driven pages (most visibly Traces) rather than render a layer whose published template it cannot read. See Compatibility → OAP Version.
Env form: HORIZON_TEMPLATES_MODE. Changing the mode requires a BFF restart — see Hot reload behavior.
Query limits
query:
landingServiceCap: 100 # default
overviewTopN: 100 # default
query.landingServiceCap bounds how many services a layer landing runs
column-metric MQE for, per request. The service picker always lists every
service in the layer, but only fetches metric columns for up to this many —
and when a layer has more, the BFF runs one cheap single-metric pass (the
landing’s order-by column over every service) to pick the true top-N, then
fetches the full columns for just those. Services below the cap still appear in
the picker, showing low in the order-by column (and — for the others,
which were never probed) — every service stays browsable and selectable. The
picker header reads “metrics: top N” so the metric trim is never silent.
- Default
100. Most layers have fewer services and render in full. - Raise it (e.g.
300,500) if your OAP and storage backend can take the larger fan-out and you want metrics for more services at once. - Lower it to protect a modest deployment from heavy landings.
What it bounds. The cap limits the full-column MQE fan-out (the expensive part — every configured column × service). When a layer exceeds it, the true top-N is found by a single cheap pass that evaluates only the order-by column for every service — so on a very large layer that one ranking pass still scales with the service count (it’s one metric, batched through a bounded-concurrency pool, not the full column set). The cap is therefore a bound on the expensive fan-out, not a hard ceiling on total OAP traffic. If you need a hard ceiling on a pathological layer, lower the cap and pair it with a tighter OAP rate limit.
Hot-reloadable — a change takes effect on the next landing request.
query.overviewTopN is the N for the Overview dashboards’ KPI tiles: each tile aggregates a whole layer with a server-side top_n(<metric>, N, …) rollup, so OAP does the ranking and one query per tile comes back regardless of how many services the layer holds. The default 100 covers every layer on a normal deployment — raise it only if a single layer holds more than 100 services and the tail matters to the aggregate; lower it to cheapen the rollup on a constrained backend. Hot-reloadable — applies on the next Overview request.
Performance tuning
performance:
bulk:
topology: { nodeBulkSize: 150, edgeBulkSize: 200, concurrency: 4 }
infra3d: { metricBulkSize: 6, metricConcurrency: 4, topologyConcurrency: 4, templateConcurrency: 8 }
landing: { bulkSize: 6, concurrency: 8 }
dashboard: { bulkSize: 6 }
limits:
topologyMaxNodes: 5000
topologyMaxEdges: 15000
maxPageSize: { traces: 100, logs: 100, browserLogs: 100, events: 200 }
The performance block tunes how hard Horizon drives your OAP and storage backend. Every default equals the built-in value, so the whole block is optional — omit it and Horizon behaves exactly as it does without it. Every value is also clamped to a hard ceiling: a number above the ceiling is pulled back down to it (config can only lower the load below a built-in limit, never raise it past one). Hot-reloadable — a change takes effect on the next request of that kind.
The rule of thumb: raise these on a beefy OAP with a fast storage backend that can absorb more parallel queries (you’ll fill pages and maps faster); lower them on a modest deployment where a busy OAP rejects or slows under the burst.
performance.bulk — query fan-out
These govern how Horizon batches and parallelizes its metric queries to OAP. Each family has a bulk size (how many metric expressions ride in one OAP request — fewer, larger requests vs. more, smaller ones) and most have a concurrency (how many of those requests are in flight at once).
| Section | Tunes | Defaults |
|---|---|---|
bulk.topology |
The service-map family (topology, instance topology, deployment, endpoint dependency) node/edge metric fan-out. | nodeBulkSize: 150, edgeBulkSize: 200, concurrency: 4 |
bulk.infra3d |
The 3D Infrastructure Map’s metric, topology, and template loading. | metricBulkSize: 6, metricConcurrency: 4, topologyConcurrency: 4, templateConcurrency: 8 |
bulk.landing |
The per-layer landing’s service-column metric batches. | bulkSize: 6, concurrency: 8 |
bulk.dashboard |
A dashboard’s widget metric fan-out. | bulkSize: 6 |
- Raise
concurrency/*Concurrencyto load a large topology, 3D map, landing, or dashboard faster when OAP has headroom. Lower it (toward1) if OAP rejects or slows under the burst of parallel requests. - Bulk sizes trade request count against request size: a larger bulk means fewer, fatter OAP requests. OAP rejects an oversized request, so each bulk size is capped — leave it at the default unless you have a specific reason to change it.
- For the 3D map specifically, these knobs are also described in context on the 3D Infrastructure Map page.
performance.limits — render & record caps
| Field | Caps | Default |
|---|---|---|
topologyMaxNodes |
The render valve for a service map — a graph with more nodes than this is rejected with a “narrow the scope” notice rather than drawn as an unreadable hairball. | 5000 |
topologyMaxEdges |
The same valve on edges. | 15000 |
maxPageSize.traces |
The largest page the Traces list will show — records displayed at once, not a page count. The page-size picker on the page maxes at this same value, so a client can’t out-ask the dropdown. Each read fetches one record beyond the page, which is how the list knows whether there is a next page. | 100 |
maxPageSize.logs |
The same displayed-page cap for Logs. | 100 |
maxPageSize.browserLogs |
The same displayed-page cap for Browser Logs. | 100 |
maxPageSize.events |
The same displayed-page cap for Events. Defaults deeper than the other feeds because events are grouped for display (one deployment produces many per-instance rows), so a raw page has to carry more records to fill a screen. | 200 |
topologyMaxNodes/topologyMaxEdgesare a readability and safety valve, not a data limit — if your deployment legitimately has a graph this large, raising them lets it render (at the cost of a denser scene and a heavier draw). Lower them if you’d rather force operators to scope down sooner.maxPageSize.*bound how many rows one Traces / Logs / Browser-Logs / Events page shows, and with it how much each list read pulls from storage (one record beyond the page — that extra row is how the list knows another page exists). Some storage backends fail or slow on large list queries — lower these to keep list pages cheap on a constrained backend; raise them (up to the ceiling) if your backend serves big result sets comfortably and operators want more rows per page.
Excluded layers
layers:
excluded: # defaults when the block is omitted:
- key: FAAS # deprecated
reason: Deprecated.
- key: VIRTUAL_GATEWAY # not planned
reason: Not planned to set up.
layers.excluded hides specific layers from the sidebar even when OAP
reports them in listLayers. Keys are OAP layer keys (UPPER_SNAKE), matched
case-insensitively. reason is a note for whoever reads the file — it is
not shown in the UI; an excluded layer simply doesn’t appear.
- Defaults:
FAASandVIRTUAL_GATEWAYare excluded when the block is omitted. Setexcluded: []to surface every layer OAP reports. - This is the only hide list — there is no hard-coded one. The other way a layer disappears is an admin explicitly disabling its template on the Layer dashboards page.
Cross-references
- A field that affects user-visible behavior at runtime is also visible on Admin → Auth Status (
/admin/auth-status) for live verification — see Admin Pages. - The wire-level effect of any
oap.*change is visible inhorizon-wire.jsonlwhendebugLog.enabled: true— see debugLog.