1.1.0
Breaking changes and migration
Breaking: Turning the Cold pill on no longer re-reads the page. It changes what the NEXT read asks for — the following auto-refresh round, or whatever you do first, typically choosing the time range the cold data lives in. Flipping it used to start a round of its own, which on a cold tier routinely ran past the sixty-second cap; the sweep that followed then re-queued exactly the queries the cap had just cancelled, so the cap was at its most useless during the outage it exists for. And because a cold read REPLACES the hot one rather than widening it, an operator who had not yet moved the time range watched the whole page empty on the click. Anything already in flight is cancelled, so no batch is left half hot and half cold — the stage is read when a request goes out, and a call still queued behind the concurrency limiter would otherwise leave under the new stage while belonging to the old one. Nothing to migrate: if you want the page re-read immediately after flipping, press Refresh.
Sign-in and access control
Features
The login audit can now be stored in BanyanDB instead of PostgreSQL. Set audit.provider: banyandb and point audit.banyandb.address at a liaison’s gRPC port — including the BanyanDB SkyWalking already stores its telemetry in, since Horizon keeps the records in groups of its own (horizon_audit and horizon_audit_metrics, prefixed by namespace where two deployments share one server). The page is the same on either backend: the same rows, the same three filters, the same hourly summary and token-usage tab. Retention is the groups’ own lifetime rather than a job Horizon runs, so there is no sweep interval to tune; on a cluster, set shards and replicas, because the single-node defaults would keep the whole audit log on one data node. As with PostgreSQL, an address that is not loopback must use TLS unless allowCleartext says otherwise. See Login audit.
Fixes
The BanyanDB audit configuration is only what a deployment actually differs on — where the server is, how to authenticate to it, a namespace prefix, and how long to keep records. The groups’ layout is bundled rather than exposed: one shard, which a sign-in rate never outgrows. That is also the safe default, because BanyanDB fixes a group’s shard count at creation and does not move data when it changes — a shard count edited after records exist would route new ones away from the old ones rather than resharding. Horizon now reports such a difference instead of applying it. Replication is left to you: Horizon sets no replica count, so a deployment that wants the audit log replicated can create the two groups with the count it wants and Horizon will keep them that way.
The login audit list pages the way every other SkyWalking list does. It asks for a page number rather than carrying a position between requests, which is the arrangement OAP uses for traces, logs, alarms and events alike. The page still shows 50 rows at a time and still reports only whether more exist; paging is bounded at 500 pages deep, and a request past that is refused rather than quietly answered with something else.
The hourly summary and token-usage totals now refuse a window they cannot read completely. Both are sums, so a row that did not fit did not look missing — it looked like a smaller number. A window holding more rows than one read may return now reports that it cannot be shown, instead of drawing a total that is quietly low.
A database outage no longer costs the hourly sign-in counts it spanned. Each Horizon process now records a running total for the hour rather than a count of what happened since its last write, so the first write after the database comes back restores the figures for the hour in progress — and a write that is retried after an uncertain outcome leaves the same number instead of counting twice. The sign-in list is unchanged: rows from an outage are still not recorded.
Refreshing
Features
Refreshing is one coordinated round. A page used to refresh in pieces, on clocks that drifted apart: the header and the service roster followed the topbar timer, while a dashboard’s widgets and the alarms card each polled on intervals of their own. A refresh is now a single round — header, roster, every widget, the alarms card and the maps — all asking about one time window, and the round is not finished until the last of them lands. They still appear as they arrive, so a large comparison fills in over a second or two; what changed is that they are all answering the same question. The countdown measures the gap between rounds rather than between starts: while the readings are out it reads Refreshing, and it begins again when the last of them lands, so time spent loading is never charged against the interval. On a slow backend the interval genuinely stretches and two rounds can no longer overlap. A round that has started always finishes, even if you switch refreshing off or move to a page that pauses it — the next one simply does not begin. One that runs longer than a minute is given up on, so a single wedged screen cannot stop everything else refreshing.
Auto-refresh has its own on/off, separate from the interval. Turning it off and back on returns to the interval you last chose rather than forgetting it, and off means off everywhere: passing through a page that pauses refreshing and coming back no longer triggers a refresh you had switched off. Switching it on while a page is paused or the tab is in the background saves the setting without refreshing then and there, and the interval menu stays usable on pages that pause refreshing — how often to refresh outlives the page you happen to be on. Pages and overlays pause refreshing independently of each other, so opening the Smartscape hierarchy overlay freezes the background while you pan through it and changing the time range no longer unfreezes it underneath you.
The refresh control says whether anything is actually loading. A download arrow beside the countdown appears while requests are in flight and goes when they land — the icon used to spin whenever auto-refresh was merely enabled, which told you nothing about whether data was arriving. While a round is out the refresh buttons are disabled rather than accepting a click that could only ask for what is already being fetched.
A refresh that fails is recorded where you can go and read it. Failures from the timer collect beside the refresh control, newest five first, with a count of the ones you have not seen — nobody asked for that round, so it waits rather than interrupting. Each entry names the screen, what it was trying to do, the request and the server’s answer; secrets in the URL or the response are removed before it is shown. Failures from something you just did — expanding a node, for instance — appear immediately instead, as a message that pauses while you read it and can be opened for the same detail.
Fixes
Overview widget grids and layer dashboards emptied on every refresh. Both treated a moving time window as a new question, so each tick blanked the grid and replaced it with a loading line — over widgets that were usually about to be filled with the same numbers. They keep the previous values while the next reading is out and replace them in one go when it lands.
The Alarms card on a dashboard refreshed on a clock of its own. It polled every minute regardless of the page’s own cadence, so a dashboard set to refresh every fifteen seconds showed an alarm list up to a minute older than the metrics beside it. It moves with the rest of the page now.
A read you walked away from went on costing OAP work. Navigating away mid-load, or a refresh that gave up, stopped the browser waiting but not the query behind it — so an abandoned page went on costing OAP the whole fan-out, multiplied by the node count on a cluster. Those reads are now cancelled all the way through. Anything that CHANGES something — creating a profiling task, pushing a template — still runs to completion, so a closed tab cannot leave it half applied.
Clicking Refresh against a dead backend appeared to do nothing. The failure was filed away and the screen said nothing, though you had just pressed the button and were watching. A refresh you asked for now answers on screen when part of it fails — once, however many widgets failed — and points at the list beside the control, where the detail is.
One outage was listed several times. Two screens sharing the same reading each recorded it, so the failure list showed one problem as several. Each is recorded once now, and a comparison entity that fails on its own is recorded at all, which it was not before.
A failed read was drawn as a page of zeroes. When a layer read could not reach OAP the answer came back empty, and an overview’s KPIs rendered that as 0 and its service counts as none — indistinguishable on screen from a system that genuinely had nothing running. Overviews and the layer service list now keep the last values they read and report the failure instead. Layer dashboards keep their widgets through the same failure rather than emptying them.
Query cold stage could leave hot and cold answers on screen together. Flipping it re-read each screen on its own, so the page showed both stages while it settled, and a screen that had not caught up yet could file its answer under the wrong one. The whole page is re-read as one round now.
A refresh that gave up left its heaviest requests running. The sixty-second cap cancelled the query but not the request behind it for the layer landing, the dashboard batches and the overview — the browser stopped waiting while OAP finished a fan-out nobody would read, multiplied by the node count on a cluster. Those now stop with the round.
Two reads racing could leave every request an hour wrong. The server timezone and the capability probe were each fetched per request, so a round’s dozen reads probed in parallel; behind a load balancer a fast success could be overwritten by a slower timeout falling back to UTC, and that answer then served every request for the next minute. Each is now read once per expiry and shared.
A brief storage hiccup emptied the sidebar for a minute. A failed service-catalog read replaced the known-good roster with an empty one and cached it for the full minute, so service counts fell to zero and group navigation collapsed long after OAP was healthy. The last good roster is kept and marked stale instead, and a failed read is retried within seconds — for as long as the outage lasts, rather than surviving only the first failure.
A landing metric batch that timed out silently reordered the layer. Its services were left without a value, sorted to the bottom as though idle, and could fall out of the top-N — choosing a different busiest service and a different default — while the response still reported success. Services whose metric could not be READ are now ranked above those that genuinely reported nothing, and the layer header says so on screen — naming how many batches failed — rather than leaving the blanks to be read as zeroes. An overview whose widgets aggregate across services says the same thing, because there a lost batch leaves a total quietly low rather than visibly absent. It keeps saying it for as long as the incomplete reading is on screen, not only on the refresh that hit the timeout — and an incomplete reading is re-read once, behind the values already shown, so a momentary backend failure costs a few seconds of a partial header rather than the rest of the hour.
Topology, deployment and dependency maps
Features
Changing what you are looking at says what it is loading. Picking a different service, endpoint, depth or time range names its target — Loading topology for “checkout”… — instead of leaving the previous answer under the new heading while the next one is fetched.
Fixes
An expansion on the API dependency graph could land in a graph you had left. Nothing stopped you switching endpoints while one was loading, and the branch then arrived in whatever graph had replaced it. Expanding now shows its pending state and holds the endpoint picker and the other expand handles until it lands. An expansion started while an admin preview is open also resolves against the draft being previewed, rather than against the published template the rest of the graph is not showing.
A map that failed its FIRST read showed a loading line for ever. With nothing cached to fall back on there was no way to tell a read still in progress from one that had already failed, so the graph sat on “Reading data…” indefinitely. It now says the read failed and offers Retry.
The maps blanked on every refresh. The Topology, Deployment, API dependency and instance-relationship graphs dropped to a loading line and came back on each tick, taking your zoom, pan and any nodes you had dragged with them. They redraw in place now, and are re-framed only when the question changes — a different layer, service, endpoint, focus, depth or time range. A service appearing or disappearing no longer re-frames the canvas or discards placements.
A failed reading erased the map you were looking at. When a round could not reach OAP the graph was replaced by an empty one, so an empty map could mean either that there was nothing to show or that the read had failed. The previous picture stays and the failure is reported beside it. A layer whose template an administrator has disabled is the deliberate exception — that is an answer rather than a failure, so the map is cleared and says so, instead of telling you to check a backend that is fine.
A failed expansion on the API dependency graph was reported as “no further dependencies”. A read that could not be completed marked the branch exhausted and faded its handle — a claim about your system made from a failure to read it. It now says the expansion failed and leaves the handle live, so the click can simply be repeated.
The API dependency columns re-sorted themselves on every refresh. Rows were ranked by the centre metric, which moves every cycle, so an endpoint you were watching had to be found again after each tick. Which endpoints get a row is still decided live — one that becomes busy enough still appears — but an endpoint already on screen keeps its place and an arrival takes the next free one.
Pods drifted on the Deployment map, and dragging could stop working. A placement was held as an offset from the packed position, so a refresh that added a pod carried yours along with the re-pack; and the map only re-armed its drag handles when a count changed, so a refresh that swapped one instance for another silently left them unbound. Placements are absolute now, and a node on the API dependency graph keeps its place the same way.
Dashboard templates
Features
Cluster Status shows what the template store has actually loaded. A new Dashboard templates pane reports how many layer templates, overviews, alert pages and translation overlays are being served from OAP, and when they were last read — and, when a read fails, the message it failed with, so “unreachable” is no longer the only thing you are told about a 404, a 401 and a timeout alike. Its badge separates a store that cannot be read while your dashboards keep rendering from one that has never been read at all.
A template published elsewhere reaches an open browser on its own. Horizon re-reads the template store on a slow cycle of its own, so a dashboard pushed from another Horizon, from swctl, or from anything else writing the same OAP store appears within about a minute — without anyone reloading the page.
Fixes
A template store that could not be read emptied every dashboard, overview and map. They are rendered from the last successful read now, so a brief outage of OAP’s admin port leaves the console up, with the banner saying how stale it is. Horizon still never substitutes the templates bundled in the release — showing shipped defaults in place of your own configuration would misrepresent what is on screen — so a Horizon that has never read the store still blocks those pages rather than inventing content for them.
The unreachable banner never cleared once the store came back. Nothing re-read the store’s status after the page had loaded, so the warning stayed up — and the pages behind it stayed blocked — however healthy OAP had become. Both recover on their own now.
Operating Horizon
Fixes
Data retention now reads the same on every storage backend, and says which setting governs each figure. The page used to mirror OAP’s wire shape — a Records / Metrics split, a Minute / Hour / Day trio, and a class called Normal — which on ElasticSearch, MySQL, PostgreSQL, H2 and TiDB meant nine numbers that were really two, presented as nine knobs you could turn apart. Those backends now show the five things retention is actually decided for — Metadata, Metrics, Logs, Traces and Others — each naming core.recordDataTTL or core.metricsDataTTL beneath it, with a note saying the two settings are all there is. BanyanDB shows the same five in the same order, expanding Metrics into its minute / hour / day groups and adding Zipkin traces and Browser error logs, because there each one is a separate bydb.yml group that genuinely can be tuned apart.
The retention chart hid data classes whose retention happened to match. Classes sharing a figure were merged into a single All records (5) bar — which reads as a fact about the backend when it is only a fact about today’s configuration, and left an operator unable to see what they could change. Every class now has its own row. Others is the one row named for a group rather than a data type: BanyanDB keeps alarms, events, sampled traces, top-N and every profiling record together in records, so they share one retention and no name for a single one of them would be true.
Query cold stage
Features
Cold is asked for only where cold data is kept. Traces, logs and metrics are the classes a deployment is advised to age into cold storage, and they are now the only ones Horizon sends the flag for. Alarms, the instance and endpoint pickers, events and everything from profiling stay hot — they are small, and they are the first things you reach for during an incident, so a Cold toggle emptying them bought nothing. Those pages keep answering while Cold is on.
Fixes
The cold-stage warning told you to pick a window that does not exist. On a deployment with no cold stage configured — the BanyanDB default — it still said “pick a window older than N days”, which cannot work, and following it moved the range out of hot+warm so the warning hid itself: a blank page with the one sentence explaining it gone. It now says plainly that no cold stage is configured and stays up until you turn Cold off. Where cold IS configured, the suggested window is the one that clears the deepest class rather than the shallowest — it used to name the records boundary (3 days by default) while every metric widget needed 7, so following it brought traces back and left the metrics exactly as empty. Both the warning and its advice are now decided by the classes Horizon actually reads under Cold — traces, logs and metrics. Alarms, events and profiling retention no longer count: they are usually kept for the shortest time of anything, and reading them here hid the warning for exactly the windows an operator turning Cold on is most likely to be looking at.
Dashboards
Features
The layer header’s KPIs describe one completed hour, and say which one. They used to be read over whatever window the time picker held, which meant reading the sort metric for every service in the layer on every refresh — hundreds of requests on a large layer, repeated every thirty seconds, for figures that only move by the hour. The header now reads one completed hour and holds it, and the hour it covers is named above the table (09:00–10:00) so the numbers are never older than they appear. Ten minutes are left for the backend to finish aggregating that hour, so the header moves on shortly after ten past. While the next hour is being read the previous one stays on screen with a star against each figure, instead of the table emptying. A deployment too new to have any completed hour shows the hour in progress instead — a finished hour is preferred to it whenever one is available. Whenever no hour can answer at all, the header reads the time picker’s window exactly as it always did: on a deployment young enough that the backend has written no hour-level figures yet (they are aggregated on a longer cycle than minute ones), and on the first visit to a large layer while the hour is still being read. Either way the page opens with real numbers rather than a table of dashes. Everything below the header — the trend lines, the dashboards, the maps — still follows the time picker as before.
Fixes
A compared service that failed to refresh was drawn as though it had. In Compare, each entity keeps its last good reading when its own read fails — deliberately, so one failure does not blank its siblings — but the retained series was then plotted across the CURRENT axis and ran to the right-hand edge beside the fresh ones. Ten-minute-old latency sat next to live latency with nothing to tell them apart. Each series is now placed at the buckets it was actually read for, so a stale one stops where its data does and the gap is visible.
A layer whose dashboard an administrator removed no longer reports an outage. The page said the template was disabled and pointed at an admin screen to re-enable it — a control most operators cannot reach, on a page that does not exist under that name — while the refresh history recorded “OAP could not be reached” for what was an administrative decision on a healthy server. The page now simply says it is not available, and the layer leaves the sidebar on the next menu read.
Reactivating a layer brings back its own dashboard, not the shipped default. OAP keeps a disabled template’s configuration; Reactivate was re-pushing the bundled default over it, discarding every edit the layer carried. It now restores what was there, falling back to the bundled default only when there is nothing to restore.