Build an error rate dashboard in ZipLogger
When something breaks, the first five minutes usually go on opening tabs: the log search, the alert list, the deploy history, the service status. A dashboard puts the answers you check every time on one screen, so you open one link instead.
This post walks through building one in ZipLogger: an error rate board for a single service, then the full widget catalog, variables, filters, sharing, export, and what dashboards do not do.
Create a dashboard
Open Dashboards in the sidebar. The top of the page is the Services overview: a card for every service that sent logs or metrics in the last 24 hours, with its status (Healthy, Errors, Silent or Down). It refreshes every 30 seconds on its own. Your dashboards sit below it.
Click + New and give the dashboard a name. It starts empty. Click + Widget to open the Add a widget catalog, which is grouped by where the data comes from and has a search box. Click a widget to add it.
Creating and editing dashboards needs the Editor or Admin role. A Viewer can open them but sees no edit buttons. Every plan includes dashboards; how many you can keep depends on the plan, see pricing.
Example: an error rate board for one service
Say checkout-api is the service you get paged about. A useful first board has five widgets:
| Widget | Settings | What it tells you |
|---|---|---|
| Error rate | none | Errors as a share of all log lines, with the raw counts |
| Error trend | none | When the errors started |
| Top errors | none | Which error messages, grouped by shape |
| Active alerts | none | What is firing right now |
| Release impact | Service: checkout-api | Error rate before and after the latest release |
Then open Filters in the toolbar and set Service to checkout-api. That one filter points the log widgets at the service, so Error rate, Error trend and Top errors only count its lines.
One thing in that table needs an explanation:
- Release impact compares the error rate for a window before and after the latest release ZipLogger saw for that service. The window is 6 hours unless you change Compare window (hours), which goes from 1 to 72. It only has something to show if your logs carry a release. The post on finding the commit that broke production covers how releases get there.
The Error rate number turns red at 5% or more. It is a share of log lines, not of requests: a service that logs ten info lines per request will show a lower rate than one that logs only failures.
Arrange and configure widgets
Each widget has a ⋯ menu in its header with Configure, Duplicate, Width (1 to 4 columns), Height (1 to 3 rows) and Remove. Drag a widget to move it. The grid is four columns wide.
Configure opens the widget's own settings: a title, fields such as Query (optional), Severity, How many or Chart, and Widget filters. Widget filters narrow that one widget on top of the dashboard filters. Where both set the same thing, the widget's value wins. So a board filtered to production can still hold one widget pinned to a single service.
Nothing is stored until you click Save, which appears in the toolbar once you have changed something. The layout, the time range and the filters are saved on the server with the dashboard, so it looks the same on every machine. A dashboard holds up to 40 widgets.
The widget catalog
Every widget in the catalog reads from an endpoint the app already has. The log widgets use the same queries as the Search page, so you can check any number there.
| Category | Widgets |
|---|---|
| Events | Event trend, Event count, Unique users, Sessions, Top events, Events by country, Events by device, Events by browser, Event activity heatmap, Funnel, Process conversion, Event → errors, Event property total |
| Errors | Error count, Error rate, Error trend, Top errors |
| Logs | Log volume, Log count, Logs by severity, Logs by service, Log activity heatmap, Log search results |
| Alerts | Active alerts, Recent alerts |
| Services & health | Service health, Response time (p95), Request volume, Trace error rate |
| Deployments & Git | Recent deployments, Release impact, Suspected regressions |
| Users | Recent sessions |
| AI | AI production summary |
| Usage | Usage summary, Usage meter, Usage over time, Top quota consumers |
| Text | Text (Markdown) |
A few notes on where the data comes from:
- Log search results is a saved search pinned to the board: the matching lines themselves, 5 to 100 of them.
- Response time (p95) and Request volume chart request metrics, the data sent to
/ingest/v1/metrics. You needZipLogger.Metrics.AspNetCoreor your own client posting there, as described in the ASP.NET Core metrics post. Without that they stay empty, even if you send traces. - Trace error rate is the share of spans in error per day, from traces, for 1 to 90 days.
- Funnel takes two to eight event names in order, and shows on the widget whether it counts by presence in the window or strictly in sequence.
- Process conversion runs a saved process report over the dashboard's range. Latest version follows edits to the report; a pinned version stays fixed.
- AI production summary writes a short summary of what your logs show. It needs your own AI key under Settings → Integrations and a plan that includes AI analysis, which Free does not.
- Pie. Logs by severity, Logs by service, Events by country, Events by device and Events by browser have Show as with bars, table or pie. A pie draws eight slices and folds the rest into "other", so use it for a handful of values, not fifty services.
- Log activity heatmap and Event activity heatmap count by weekday and hour of day, in your own time zone. They show the weekly rhythm a line chart hides: when the nightly job fills the logs, when people actually use the product.
Notes on the board: the Text widget
Text (Markdown), in the Text category, puts words on the dashboard: what the board is for, who owns the service, the first three things to check. It takes headings, lists, bold, italic, inline code, links and code blocks:
## Checkout on-call
- Owner: **payments team**
- Runbook: https://wiki.example.com/runbooks/checkout
- First check: `Release impact` below. A deploy in the last hour is the usual cause.
HTML in the text is shown as text, not rendered. Only http(s) links and links inside the app become links. Variables (next section) are filled in here too.
One board for every service: variables
Five copies of the same board, one per service, drift apart within a month. A variable lets one board serve all of them.
Open the dashboard's ⋯ menu and choose Variables…, then + Variable. Each variable has a Name (what you write in widgets), a Label (what the dropdown says), Values from, an optional Default and Offer "All". A dashboard can have up to 12. Values from is one of:
| Values from | The dropdown lists |
|---|---|
| Fixed list | the options you type, comma-separated |
| Free text | nothing; the viewer types a value |
| Services | the services seen in your telemetry |
| Environments | the environment values on your events and logs |
| Metric label values | the values of one label of one metric, for example host.name of system.cpu.utilization |
| Log field values | the most common values of a log field such as source, severity or fields.region |
| Event property values | the most common values of an event property |
Then write $service (or ${service}) in any widget setting. Before a widget runs its query, the variable is replaced with what the viewer picked. With a variable named service of the kind Services:
| Widget | Setting |
|---|---|
| Log count | Query (optional): source:$service AND severity:error |
| Log search results | Query: source:$service |
| any widget | Widget filters, Service: $service |
| any widget | Title: Errors in $service |
A second variable, host, with Values from set to Metric label values, metric system.cpu.utilization and label key host.name, drives a Metric widget through its Label filters: host.name:$host.
What All does depends on where the variable sits, because "all services" should mean no filter, not a filter on a literal star:
- A setting that is only the variable, such as a service filter of
$service, is dropped. - Inside a search query the variable becomes
*. - In metric label filters, the pair with the variable is dropped and the others stay.
- In titles and text widgets it reads "All".
A $ followed by a name that is not a variable is left alone, so a text widget can still say costs $5.
The value picked goes into the URL as ?var-service=checkout-api. Paste that link in an incident channel and it opens on the same service. Picking a value does not change the saved dashboard; only the Default does.
Time range and filters
The time range selector offers Last 15 minutes, Last 1 hour, Last 4 hours, Last 24 hours and Last 7 days, and applies to every widget that uses time. Some widgets ignore it on purpose because they answer "right now" or have their own window: Active alerts, Recent alerts, Service health, Recent deployments, Release impact, Suspected regressions, Trace error rate and the Usage widgets.
Filters has five fields: Environment, Service, Country, Version and User. Each widget applies the ones it understands. For log widgets:
- Service matches the log's
source. - Environment matches a custom field called
environment, so it only works if your logs send one. It is not the environment selector at the top of the sidebar: a dashboard always shows the data of the environment you are viewing, and this filter narrows that data by the field. - Version matches
release. - User matches a custom field called
userId.
Event widgets use all five. A combination you use often can be saved with Save this combination as… and Save filter, then picked from Saved filters… on any dashboard.
Sharing
A new dashboard is visible to everyone in the workspace. In the ⋯ menu next to Save, Make private limits it to you, and Share with the team reverses that. Private is enforced on the server: another member who opens the link gets "That dashboard does not exist, or is private to someone else."
Each dashboard has its own URL, /env/<environment>/dashboards/ followed by its id, so you can paste it into an incident channel and it opens in the same environment. The same menu has Rename, Duplicate and Delete. The star button marks a dashboard as a favorite for you only, and favorites get a ★ in the dashboard picker.
Export and import
The ⋯ menu has Export JSON. It downloads the board as <name>-<date>.dashboard.json, for example checkout-2026-10-15.dashboard.json. The file holds the widgets with their settings and layout, the time range, the filters, the variables and whether it is private. It holds no ids, no owner and no favorites, so it can go into another workspace or into git. Trimmed to one widget, with that widget's settings shortened to its query, it looks like this:
{
"schema": "ziplogger.dashboard/v1",
"name": "Checkout",
"panels": [
{
"id": null,
"title": "Errors in $service",
"type": "log_search",
"query": null,
"field": null,
"position": 0,
"w": 2,
"h": 2,
"config": { "query": "source:$service AND severity:error" }
}
],
"timeRange": "Last 24 hours",
"filters": {},
"variables": [
{ "name": "service", "label": "Service", "kind": "service", "options": [], "includeAll": true }
],
"visibility": "team"
}
Import, next to + New, creates a new dashboard from such a file. It never overwrites an existing one. A file with any other schema is refused rather than guessed at. Importing needs the Editor role and counts against your plan's dashboard limit like any new board.
The same two steps exist in the API, for keeping boards in a repository:
curl -s -H "Authorization: Bearer $ZIPLOGGER_TOKEN" https://app.ziplogger.ai/api/v1/dashboards/$DASHBOARD_ID/export > checkout.dashboard.json
curl -s -X POST -H "Authorization: Bearer $ZIPLOGGER_TOKEN" -H "Content-Type: application/json" --data @checkout.dashboard.json https://app.ziplogger.ai/api/v1/dashboards/import
$ZIPLOGGER_TOKEN is a personal access token. Export works for anyone who can see the dashboard, so a Read only token is enough. Import needs a Read and write token from someone with the Editor role.
What dashboards do not do
- No auto-refresh. Widgets load when you open the board, change the time range or filters, or click ↻ (reload every widget). Only the Services overview refreshes itself. A board left open on a wall screen shows the data from when it was last loaded.
- No public links. Everyone who opens a dashboard needs to be signed in to the workspace.
- No custom charts. You pick from the catalog and configure it. There is no query language for building a new chart type, and no formulas across widgets.
- No ranges beyond 7 days on the dashboard itself. Trace error rate is the exception, with its own Days setting.
- Import creates, never updates. Importing the same file twice gives two dashboards. To change a board from a file, import it and delete the old one.
- No alerting from a widget. A chart turning red does not page anyone. Alerts are separate rules; the alert rules post covers them, and the Active alerts and Recent alerts widgets show what they are doing.
To build your first board, create a free workspace, send some logs, and go to Dashboards → + New. The dashboards section of the docs has the short reference.