Dashboards

Build, validate and provision account dashboards with Kusto, Simple Query and Markdown.

Dashboards

Dashboards belong to an account. Start with the account's Explore / Query and Chart Capabilities dashboard for live Simple Query and Kusto examples, plus synthetic examples of grouped aggregation, percentiles and joins.

Build a page

Add a Markdown panel explaining the question, data source, units and time window. Place summary cards first, trends next, and detail tables last. Use the panel editor to choose Kusto, Simple Query or Markdown content. Keep titles explicit: a count per bucket is not a rate per second.

Kusto panels control filtering, aggregation and visualization:

logship.backend.metrics.inflow.count
| where timestamp > $__startTime() and timestamp <= $__endTime()
| summarize records = sum(sum) by timestamp = bin(timestamp, 5m), account
| order by timestamp asc
| render timechart with (title = "Records per five minutes", legend = true)

$__startTime() and $__endTime() refer to the dashboard time picker. A query using ago(5m) instead uses a rolling five-minute window regardless of that picker. Explain fixed windows in the page's Markdown.

Simple Query panels select a table, value fields, grouping dimensions, filters and a visualization. Line and area charts average stored values in automatic time buckets. They do not automatically sum histogram counts. Use Kusto sum(sum) for ingestion totals and sum(count) for histogram observation counts. A stored histogram's median is already an aggregate: taking the maximum median does not compute a global median. Compute percentiles from raw duration events when available.

Filter a whole dashboard with variables

Add a variable in the dashboard editor, choose a column or Kusto query as its option source, and enable All and multiple selection when useful. The provisioned Agent dashboards demonstrate shared Machine and Agent selectors; Trace Explorer also offers Operation. Query Activity demonstrates filtering every panel by recorded query text.

Each panel must explicitly consume the variable. Use its predicate helper before aggregation:

logship.agent.traces
| where timestamp > $__startTime() and timestamp <= $__endTime()
| where $__machine(machine)
| where $__agent(__INFLOW_AGENT)
| where $__operation(operationName)
| summarize spans = count() by timestamp = bin(timestamp, 5m), operationName
| order by timestamp asc
| render timechart with (title = "Spans for selected agents", legend = true)

$__machine(machine) matches any selected machine. All removes that predicate's restriction. Multiple values in one selector use OR; separate predicates use AND. Helpers bind typed values, including agent GUIDs, without interpolating strings into query text. Do not quote the helper or use a scalar helper such as $__machine() for a variable that permits All or multiple values.

In exported dashboard JSON, a Kusto variable can look like this:

{
  "name": "machine",
  "label": "Machine",
  "type": "kusto",
  "kusto": {
    "query": "logship.agent.traces | where timestamp > $__startTime() and timestamp <= $__endTime() | project value = machine | where isnull(value) == false | distinct value | order by value asc | take 1001"
  },
  "selectionMode": "multi",
  "includeAll": true,
  "defaultValues": []
}

The option query must return a column named value; an optional label column provides display names. Keep the original value type: agent IDs should remain GUIDs. A column source instead uses "type":"column" and "column":{"table":"logship.agent.traces","column":"machine"}. Column sources enumerate the column without a time filter; use a Kusto source for options from the selected time window. The frontend displays at most 1,000 options. Prefer a bounded dimension such as machine, agent or operation over high-cardinality query text on a busy account.

Simple Query panels consume the same variable through an item filter:

{"field":"machine","operator":"==","variable":"machine"}

Add this object to the Simple item's filters array. Supply either variable or a literal value. The capability page uses one Machine selector for both its Simple Query and Kusto examples.

When calling a query API directly, send typed selections alongside query:

{
  "query": "logship.agent.traces | where $__machine(machine) | take 20",
  "variables": [
    {"name":"machine","type":"string","values":["host-a","host-b"],"isAll":false}
  ]
}

For All, send "values":[] and "isAll":true. For agent selections, use "type":"guid". Dashboard time helpers similarly need datetime bindings when submitting queries directly; alternatively use concrete ago(6h) and now() expressions for CLI checks.

Filters apply only where their fields exist. Raw query execution events contain query text, while latency histogram summaries do not; filtering the former cannot filter the latter by query text. Page introductions list the scope of each shared filter. Synthetic tutorial examples stay independent of live machine selectors. If a combination returns no rows, reset selectors to All; options from different sources need not describe matching records, and a time-window change can make an earlier selection empty.

Find the right data

Use the CLI with an explicit account:

logsh account list
logsh --account default schema list
logsh --account default schema describe logship.backend.admin.schema
logsh --account default query -q '$metadata.schema.tables | take 20'

Account telemetry is isolated. Executor memory, host API metrics and platform health belong in Administration; account ingestion, account storage and account query activity can be shown in a tenant account. A table available in Administration may not exist in Default. Use schema list in the destination account before copying a panel.

An empty successful result means no matching events were returned. A missing-table error means the query cannot execute. Do not replace either condition with synthetic zero values that imply healthy telemetry.

Dashboard HTTP API

Authenticate with Authorization: Bearer <token>. The CLI currently manages queries and schemas; use the dashboard HTTP API to export and update dashboards:

MethodPathPurpose
GET/dashboards/{accountId}List dashboard metadata
GET/dashboards/{accountId}/dashboard/{dashboardId}Export a complete dashboard
PUT/dashboards/{accountId}/dashboard/{dashboardId}Create or replace a dashboard

Save the GET response before editing. PUT the complete document, preserving chart IDs and variables. A dashboard document contains metadata (accountId, id, name), variables, and charts. Each chart has an itemId, coordinate (x, y), dimension (width, height), and content. Layout uses four columns; x + width must not exceed four. Avoid overlapping rectangles.

Content examples:

{"type":"Markdown","markdown":{"content":"## Ingestion\n\nRecords per five-minute bucket."}}
{"type":"Kusto","kusto":{"query":"$metadata.schema.tables | take 20 | render table"}}
{"type":"Simple","simple":{"title":"Mean published ingestion count","visualization":"line","items":[{"name":"Records","table":"logship.backend.metrics.inflow.count","fields":[{"field":"sum","aggregation":"avg"}],"groupBy":["account"],"filters":[]}]}}

Validate Kusto panels with POST /search/{accountId}/kusto and a JSON body containing query. For CLI validation, replace dashboard time functions with concrete expressions such as ago(6h) and now(). Inspect returned rows and render metadata; a successful query alone does not verify browser rendering. Check chart labels, legends, empty states, narrow screens and both themes in the frontend.

Provisioning

Repository dashboard definitions live in provisioning/dashboards for the configured default account and provisioning/administration/dashboards for Administration. Administration definitions are copied into the host output and created automatically when their dashboard IDs do not already exist. Existing Administration dashboards are preserved across restarts; editing a JSON file does not migrate them. Apply intentional updates through the HTTP API.

Default-account dashboards are explicitly registered in backend.services.setupService.dashboards in the host's appsettings.json. Each registration supplies accountName, dashboardId and filePath. Account names are deployment-specific: the sample configuration creates Default Account; a production deployment may call it default. Configured provisioning writes the specified dashboard, so keep provisioning files synchronized with intentional live edits.

Keep stable dashboard and chart IDs. Test every query in its destination account before publishing. The capability page labels its inline data as synthetic; operational pages use live telemetry.