Sazabi
Investigate

Dashboards

Saved, live views of your logs. View them in the web app, and create or edit them by asking the Sazabi agent or with the CLI, API, MCP, or Terraform.

A dashboard is a saved page of live panels — single numbers, charts, and tables — built from your project's logs. Every panel runs its query when you open the dashboard or refresh it, so what you see is current as of that moment. Dashboards belong to a project, and everyone who can open the project can view them.

You view dashboards in the web app. You create and edit them by asking the Sazabi agent, or with the CLI, API, MCP server, or Terraform; the web app has no dashboard editor.

Use a dashboard for the questions you ask again and again: how many errors each service logs, how fast requests are, how many people signed up this week. For a one-off question, ask the agent in a thread instead.

View a dashboard

Select Dashboards in the web app's sidebar to open your project's dashboards. The list on the left shows every dashboard in the project; use Search dashboards to find one by title or description. On a narrow screen, the list becomes a menu above the dashboard. Every project includes two dashboards: Issues & alerts shows the issues Sazabi opened, how they were resolved, and the alerts and pull requests that followed; Log health shows the logs sent to the project and their errors. You can't edit or delete them, but you can copy one (ask the agent, or run sazabi dashboards create --from-dashboard) and change the copy.

While viewing a dashboard you can:

  • Change the time range. Pick Last 1 hour, Last 6 hours, Last 24 hours, Last 7 days, Last 30 days, Last 90 days, or a Custom range of start and end dates and times. Custom times are in your browser's time zone, and a range can't end in the future. Every panel reloads for the new range. The range is kept in the page URL, so a link you share opens on the same range.
  • Zoom into a spike. Drag across a time chart to make that span the dashboard's time range. Every panel reloads for it. Press Escape to cancel a drag.
  • Focus on some series. Click a series in a chart's legend to show only that series, click others to add them, and click a selected series again to remove it. With nothing selected, every series shows.
  • Sort a table. Click a column header to sort by it, again to reverse the order, and a third time to clear it.
  • Refresh. Select Refresh to reload every panel. A relative range such as the last hour moves forward to now. Dashboards do not refresh on their own.

A chart or table with nothing in the selected range shows No data in this window., and a single-number panel shows a dash. Some panels add a short note under their title:

  • Shows and a time span, for example Shows Oct 12, 09:00 – 10:00: the panel's results cover that span instead of the selected range, either part of it or the whole minutes or hours around it.
  • Last bucket still accumulating: the last bar of a bar chart is still filling, so it's drawn lighter and a short final bar doesn't read as a drop.

A panel that could not load says why and what to do, and offers Retry when trying again can help:

  • Took too long to load: select Retry, or choose a shorter time range.
  • Reads too much data or has more data than it can show: choose a shorter time range, or ask the agent to narrow the panel.
  • Ask the agent to fix this panel: the panel's query needs a change.
  • Included usage is exhausted: your plan's included usage is used up. Upgrade your plan or wait until it resets.
  • Too many requests: select Retry after a moment.

Create a dashboard with the agent

The quickest way to build a dashboard is to ask the agent in a thread — for example, "create a dashboard of checkout error rates and p95 latency by route". The agent finds the fields in your logs, writes the panels, checks that each query is valid, and replies with a link to the new dashboard. You don't need to know your log fields' names; describe what you want to see:

  • Errors overview: "Create a dashboard called Errors overview with error lines by service over time, failed requests, and the services with the most errors."
  • Latency by service: "Build a dashboard of request duration by service over the last 6 hours, with request counts and the slowest routes."
  • Product analytics: "Make a product analytics dashboard from our signup and checkout events: daily counts, events per hour, and distinct users."

Ask in the same way to change a dashboard: add or remove a panel, change a chart, rearrange the layout, rename it, change its default time range, or delete it. For example, "add a table of the 10 routes with the most failed requests to Errors overview", or "make a copy of Errors overview that only shows the checkout service". The agent only builds panels from live queries. If your logs don't contain a signal you asked for, it leaves that panel out and tells you what would provide it.

Create and edit with the CLI

A dashboard is saved as a single file that holds its title, description, and default time range, followed by a grid of panels. Each live panel holds a log query, the same query format you can run with sazabi logs query execute (see Log search). The dashboard supplies the time range, so a panel's query never names one.

# Start from an example and check it before saving
sazabi dashboards validate --example live > dashboard.mdx
sazabi dashboards validate --file dashboard.mdx

# Create it; the output includes a link to open it
sazabi dashboards create --file dashboard.mdx

# Or start from the Log volume template
sazabi dashboards create --from-template log-volume --template-version 1 --title "My logs" --last 24h

create checks the file the same way validate does and saves nothing if it has a problem; each problem names its line and what to change. Run sazabi dashboards validate --schema to list every panel type, option, and query field you can use in the file.

To change a dashboard you created from a file, edit that file and publish the next revision with update. To change any other dashboard, such as one the agent built, first save an editable copy to a new file:

sazabi dashboards get your-dashboard-id --out checkout.mdx
sazabi dashboards update your-dashboard-id --file checkout.mdx

get --out marks the copy with the revision it came from. update uses that mark to tell whether someone saved a newer revision, and create and update refresh it after each save, so you can keep editing the same file. If someone else saved a newer revision since your copy, the update is refused rather than overwriting their change. Save a fresh copy to a new file with get --out, re-apply your edit, and update again.

Other commands: sazabi dashboards list lists the project's dashboards, sazabi dashboards create --from-dashboard your-dashboard-id --revision 1 copies a dashboard (the revision is shown by sazabi dashboards list; the copy doesn't change when the original does), and sazabi dashboards delete your-dashboard-id deletes one. Deleting a dashboard removes it for everyone in the project and can't be undone. Add --project-id to work in a project other than the active one, and run sazabi dashboards --help for every command and option.

Example files

Each example is a complete file you can create as it is. The panels read the service and level of each log line, request records, and named events (event.name); if your logs record these another way, ask the agent to adapt the file. The product analytics table leaves out the events Sazabi records in your project under the service sazabi, such as issue.opened, which Issues & alerts shows. In a table, columns relabel the result's fields: one per group (service, or a field such as http.route) and one for the value (count, requests, errors, or a measure such as max duration).

errors-overview.mdx
<DashboardMeta
  version={1}
  title="Errors overview"
  description="How many errors each service logged, and which requests failed."
  defaultTimeRange={{ kind: "relative", lookbackSeconds: 86400 }}
/>

<Grid columns={12}>
  <GridItem id="errors-panel" span={6} height="compact">
    <Metric
      id="errors"
      label="Error lines"
      artifact={{
        version: 2,
        type: "metric",
        view: { unit: "errors" },
        query: {
          version: 2,
          predicate: {
            op: "compare",
            field: { kind: "severity" },
            comparator: "gte",
            value: "ERROR",
          },
          dimensions: [],
          measure: { op: "count" },
          output: "table",
        },
      }}
    />
  </GridItem>
  <GridItem id="failed-requests-panel" span={6} height="compact">
    <Metric
      id="failed-requests"
      label="Failed requests"
      artifact={{
        version: 2,
        type: "metric",
        view: { unit: "requests" },
        query: {
          version: 2,
          predicate: { op: "and", children: [] },
          dimensions: [],
          measure: { op: "error_count" },
          output: "table",
        },
      }}
    />
  </GridItem>
  <GridItem id="errors-by-service-panel" span={12} height="medium">
    <AreaChart
      id="errors-by-service"
      artifact={{
        version: 2,
        type: "area_chart",
        view: { title: "Errors by service", unit: "errors", stacked: true },
        query: {
          version: 2,
          predicate: {
            op: "compare",
            field: { kind: "severity" },
            comparator: "gte",
            value: "ERROR",
          },
          dimensions: [{ kind: "service" }],
          measure: { op: "count" },
          bucket: "15m",
          output: "series",
          series: { limit: 6, overflow: "other" },
        },
      }}
    />
  </GridItem>
  <GridItem id="failed-requests-over-time-panel" span={6} height="medium">
    <Timeseries
      id="failed-requests-over-time"
      artifact={{
        version: 2,
        type: "timeseries",
        view: { title: "Failed requests", unit: "requests", mode: "bar" },
        query: {
          version: 2,
          predicate: { op: "and", children: [] },
          dimensions: [],
          measure: { op: "error_count" },
          bucket: "15m",
          output: "series",
        },
      }}
    />
  </GridItem>
  <GridItem id="top-services-panel" span={6} height="medium">
    <Table
      id="top-services"
      artifact={{
        version: 2,
        type: "table",
        view: {
          title: "Services with the most errors",
          columns: [
            { field: "service", label: "Service" },
            { field: "count", label: "Errors" },
          ],
        },
        query: {
          version: 2,
          predicate: {
            op: "compare",
            field: { kind: "severity" },
            comparator: "gte",
            value: "ERROR",
          },
          dimensions: [{ kind: "service" }],
          measure: { op: "count" },
          output: "table",
          series: { limit: 10, overflow: "other" },
        },
      }}
    />
  </GridItem>
</Grid>
latency-by-service.mdx
<DashboardMeta
  version={1}
  title="Latency by service"
  description="Request volume and duration for each service, and the slowest routes."
  defaultTimeRange={{ kind: "relative", lookbackSeconds: 21600 }}
/>

<Grid columns={12}>
  <GridItem id="requests-panel" span={6} height="compact">
    <Metric
      id="requests"
      label="Requests"
      artifact={{
        version: 2,
        type: "metric",
        view: { unit: "requests" },
        query: {
          version: 2,
          predicate: { op: "and", children: [] },
          dimensions: [],
          measure: { op: "request_count" },
          output: "table",
        },
      }}
    />
  </GridItem>
  <GridItem id="avg-duration-panel" span={6} height="compact">
    <Metric
      id="avg-duration"
      label="Average duration"
      artifact={{
        version: 2,
        type: "metric",
        view: { unit: "ms" },
        query: {
          version: 2,
          predicate: { op: "and", children: [] },
          dimensions: [],
          measure: { op: "duration", aggregate: "avg" },
          output: "table",
        },
      }}
    />
  </GridItem>
  <GridItem id="duration-by-service-panel" span={12} height="medium">
    <Timeseries
      id="duration-by-service"
      artifact={{
        version: 2,
        type: "timeseries",
        view: {
          title: "Average duration by service",
          unit: "ms",
          mode: "line",
        },
        query: {
          version: 2,
          predicate: { op: "and", children: [] },
          dimensions: [{ kind: "service" }],
          measure: { op: "duration", aggregate: "avg" },
          bucket: "5m",
          output: "series",
          series: { limit: 6, overflow: "other" },
        },
      }}
    />
  </GridItem>
  <GridItem id="requests-by-service-panel" span={6} height="medium">
    <AreaChart
      id="requests-by-service"
      artifact={{
        version: 2,
        type: "area_chart",
        view: { title: "Requests by service", unit: "requests", stacked: true },
        query: {
          version: 2,
          predicate: { op: "and", children: [] },
          dimensions: [{ kind: "service" }],
          measure: { op: "request_count" },
          bucket: "5m",
          output: "series",
          series: { limit: 6, overflow: "other" },
        },
      }}
    />
  </GridItem>
  <GridItem id="slowest-routes-panel" span={6} height="medium">
    <Table
      id="slowest-routes"
      artifact={{
        version: 2,
        type: "table",
        view: {
          title: "Slowest routes",
          columns: [
            { field: "http.route", label: "Route" },
            { field: "max duration", label: "Slowest request", unit: "ms" },
          ],
        },
        query: {
          version: 2,
          predicate: {
            op: "compare",
            field: { kind: "column", name: "operation_kind" },
            comparator: "eq",
            value: "http_server",
          },
          dimensions: [{ kind: "column", name: "http.route" }],
          measure: { op: "duration", aggregate: "max" },
          output: "table",
          series: { limit: 15, overflow: "other" },
        },
      }}
    />
  </GridItem>
</Grid>
product-analytics.mdx
<DashboardMeta
  version={1}
  title="Product analytics"
  description="Signups, checkouts, and every other product event the app records."
  defaultTimeRange={{ kind: "relative", lookbackSeconds: 604800 }}
/>

<Callout tone="info" title="What this counts">
  Each panel counts the product events the app logs with an event name, such as
  `signup.completed`.
</Callout>

<Grid columns={12}>
  <GridItem id="signups-panel" span={6} height="compact">
    <Metric
      id="signups"
      label="Signups"
      artifact={{
        version: 2,
        type: "metric",
        view: { unit: "signups" },
        query: {
          version: 2,
          predicate: {
            op: "compare",
            field: { kind: "column", name: "event.name" },
            comparator: "eq",
            value: "signup.completed",
          },
          dimensions: [],
          measure: { op: "count" },
          output: "table",
        },
      }}
    />
  </GridItem>
  <GridItem id="checkouts-panel" span={6} height="compact">
    <Metric
      id="checkouts"
      label="Checkouts"
      artifact={{
        version: 2,
        type: "metric",
        view: { unit: "checkouts" },
        query: {
          version: 2,
          predicate: {
            op: "compare",
            field: { kind: "column", name: "event.name" },
            comparator: "eq",
            value: "checkout.completed",
          },
          dimensions: [],
          measure: { op: "count" },
          output: "table",
        },
      }}
    />
  </GridItem>
  <GridItem id="events-over-time-panel" span={12} height="medium">
    <Timeseries
      id="events-over-time"
      artifact={{
        version: 2,
        type: "timeseries",
        view: {
          title: "Signups and checkouts per hour",
          unit: "events",
          mode: "bar",
        },
        query: {
          version: 2,
          predicate: {
            op: "compare",
            field: { kind: "column", name: "event.name" },
            comparator: "in",
            value: ["signup.completed", "checkout.completed"],
          },
          dimensions: [{ kind: "column", name: "event.name" }],
          measure: { op: "count" },
          bucket: "1h",
          output: "series",
          series: { limit: 2, overflow: "drop" },
        },
      }}
    />
  </GridItem>
  <GridItem id="all-events-panel" span={12} height="medium">
    <Table
      id="all-events"
      artifact={{
        version: 2,
        type: "table",
        view: {
          title: "Events by name",
          columns: [
            { field: "event.name", label: "Event" },
            { field: "count", label: "Count" },
          ],
        },
        query: {
          version: 2,
          predicate: {
            op: "and",
            children: [
              {
                op: "compare",
                field: { kind: "column", name: "event.name" },
                comparator: "neq",
                value: "",
              },
              {
                op: "compare",
                field: { kind: "service" },
                comparator: "neq",
                value: "sazabi",
              },
            ],
          },
          dimensions: [{ kind: "column", name: "event.name" }],
          measure: { op: "count" },
          output: "table",
          series: { limit: 25, overflow: "other" },
        },
      }}
    />
  </GridItem>
</Grid>

Use the API, MCP, or Terraform

  • API: the /v1/dashboards endpoints list, read, validate, create, update, and delete dashboards with a secret key. See API reference.
  • MCP: your coding agent can list, read, validate, create, update, and delete dashboards through the Sazabi MCP server. See MCP server.
  • Terraform: the sazabi_dashboard resource manages a dashboard as code, with its file kept beside your configuration. Every apply that changes the file publishes the next revision, and destroying the resource deletes the dashboard. See sazabi_dashboard.
resource "sazabi_dashboard" "errors_overview" {
  project_id = sazabi_project.production.id
  source     = file("${path.module}/dashboards/errors-overview.mdx")
}

Panels

Live panels come in four kinds:

  • Metric: a single number for the selected range, such as error lines in the last 24 hours.
  • Timeseries: a value over time as a line, bar, or area chart, either one series or one per group, such as average request duration per service every 5 minutes.
  • Area chart: values over time as filled areas, optionally stacked to show each group's share, such as requests per service.
  • Table: grouped values, such as the 15 routes with the slowest requests, or the most recent matching log lines.

A dashboard can also hold headings, text, and callouts to explain what it shows, and static charts and tables whose data is written into the dashboard rather than read from your logs, such as a rollout plan or a list of service owners. Static panels don't change with the time range.

Time ranges

Each dashboard opens on its own default time range. The range you pick in the web app applies to every panel at once.

  • How far back: relative ranges reach back up to 90 days. A custom range can start earlier, but panels only show logs your project still keeps, and how long logs are kept depends on your plan. See Log storage & retention.
  • Longer ranges, coarser charts: when a range is too long to chart at a panel's usual detail, its time buckets widen (for example, from every 5 minutes to every hour) so the chart still fits.
  • Load time: each panel has 30 seconds to load. Long ranges over busy logs take longer; if a panel took too long, select Retry or choose a shorter range.

Limits

  • Charts, tables, and metrics: 40 per dashboard.
  • Distinct queries: 16 per dashboard. Panels with an identical query count once.
  • Dashboard file: 80,000 characters.
  • Series per chart: 8, including an "other" series, with up to 1,000 points each.
  • Table size: 200 rows and 30 columns.
  • Panel load time: 30 seconds per panel.

Who can do what

  • View: anyone who can open the project.
  • Create, edit, and delete: owners, admins, and members who can open the project. In a restricted project, someone whose project role is Viewer can only view.

Common questions

Can I edit a dashboard in the web app? No. The web app shows dashboards; to change one, ask the agent or use the CLI, API, MCP server, or Terraform.

Does a dashboard update on its own? No. Panels load when you open the dashboard, change the time range, or select Refresh.

How do I share a dashboard? Everyone who can open the project can see its dashboards, so send them the page URL; it keeps the time range you picked. There are no public links: people outside the project can't open a dashboard.

Can I filter a whole dashboard, for example to one service? The only control that applies to every panel is the time range. Ask the agent for a copy filtered to what you need.

Why is a panel empty? No logs matched it in the selected range. Try a longer range. If it stays empty when you expect data, ask the agent to check the panel against your logs.

Why does a panel say it took too long? The panel needed more than 30 seconds for this range. Select Retry, choose a shorter range, or ask the agent to simplify the panel. On busy services, averages and maximums load faster than percentiles such as p95.

Can a dashboard alert me? No. Dashboards show data when you open them. Sazabi watches your logs for problems and opens issues on its own. See Issues.

Can I chart metrics and traces? Yes. Metrics and traces you send are stored as log records, so panels count and chart them like any other logs. Ask the agent for a chart of a metric by name.

Can one dashboard show several projects? No. A dashboard reads the logs of the project it belongs to.

Can I export a panel's data? The web app has no export or download. To get a panel's numbers, copy its query from sazabi dashboards get and run it with sazabi logs query execute.

Can I restore a deleted dashboard? No. If you keep its file, or manage it with Terraform, you can create it again.

Further reading