Visualizing Better Stack logs and metrics in Grafana
Better Stack stores all your telemetry data in a ClickHouse-powered warehouse you can query from outside the platform using plain SQL. Any tool that speaks to a ClickHouse HTTP endpoint can become a front end for your logs and metrics, including Grafana.
This guide walks through connecting Grafana to Better Stack via the SQL API, defining a user-defined metric from your logs, and building three panels: a metrics-backed time-series chart, a request volume chart from wide events, and a log table.
Prerequisites
To start, you need a Telemetry source and an SQL API connection. Here is how to set up each:
Then, create an SQL API connection in Telemetry โ Integrations โ SQL API. When creating the connection, select the Cluster that matches your source's Data region. Once created, copy the Host, Username, and Password from the success banner. The password is hidden permanently once you navigate away.
Make sure to select the same data region
The data region you pick when creating a source determines which cluster it lands on. For example, selecting Europe may assign the source to eu-fsn-3. When you create a SQL API connection, you must select the same cluster. Check the Data region section on your source's settings page to confirm the exact cluster name.
If your source is on a different cluster than the SQL API connection, queries return no results and the sample queries list appears empty.
Better Stack uses two storage tiers relevant to the queries below.
Wide events are every structured log line stored as JSON in object storage, great for ad-hoc filtering but slower to scan.
Go to Sources โ your source โ Configure and click the Extract Metrics tab.
Click + Metric and fill in:
Metric:duration
JSON dot notation:duration (Better Stack generates the extraction expression automatically)
Aggregations:Avg, Min, Max
Click Create metric and when prompted, click Apply changes to new data only.
User-defined metrics only capture data from the point of definition onwards. Logs sent before you created the metric will not appear in the metrics table. After saving, send a fresh batch of logs before querying.
Once defined, any query against remote(t123456_your_source_metrics) resolves that metric in milliseconds because it reads from compressed columnar data on NVMe rather than scanning object storage.
The practical implication for dashboarding: use the metrics table for time-series charts that refresh every 30 seconds or cover a 90-day window. Use the wide events table for log tables, one-off investigations, or attributes you have not yet promoted to a metric.
Connect Grafana to Better Stack
Install the ClickHouse plugin
In your Grafana instance, go to Administration โ Plugins and data โ Plugins, search for ClickHouse, select the Grafana ClickHouse data source plugin, and click Install.
Go to Connections โ Data sources and click Add new data source.
Search for and select ClickHouse.
Fill in the Server section. Set Server address to your cluster hostname only, no port (e.g. eu-fsn-3-connect.betterstackdata.com). Set Server port to 443. Set Protocol to HTTP (the default is Native and must be switched). Enable Secure connection (it is off by default).
Scroll down to Credentials and enter your username and password from the Better Stack success banner.
Leave Default database blank.
Click Save & test. You should see Data source is working.
Troubleshoot connection errors
If Save & test returns an error, the table below covers the most common causes.
Error
Cause
Fix
context deadline exceeded
Protocol is still set to Native
Switch Protocol to HTTP and enable Secure connection
failed to create ClickHouse client
Protocol/Secure mismatch or wrong password
Verify Protocol is HTTP and Secure is on; re-enter the password via the Reset button
Host not in allowlist
You added specific IPs to the allowlist but are connecting from a different IP
Add your current outbound IP (curl https://ifconfig.me) to the connection's allowlist, or leave it blank to allow all IPs
Authentication failed
Credentials from a different connection
Create a fresh connection and copy all three values at the same time from the success banner
Build your first panel: average request duration
This time-series chart reads from the pre-aggregated metrics table. Because the aggregation states are computed at ingest time, this query is fast even over long time ranges.
Create a new dashboard and add a panel.
Select your Better Stack data source.
Set Query Type to Table.
Switch to the SQL editor, paste the query below, and set the visualization to Time series.
Grafana SQL editor
Copied!
SELECT
toStartOfMinute(dt) AS time,
avgMerge(value_avg) AS avg_duration,
minMerge(value_min) AS min_duration,
maxMerge(value_max) AS max_duration
FROM remote(t123456_your_source_metrics)
WHERE name = 'duration'
AND dt >= $__fromTime
AND dt < $__toTime
GROUP BY time
ORDER BY time ASC
Replace t123456_your_source with your actual source identifier.
Can't see the metric?
If no data appears, the metric may not have any logs since its creation. Run a quick check via the SQL API to confirm the metrics table has rows for your metric name. NULL name rows are not errors; they represent events that arrived before the metric was defined. Send a fresh batch of logs and wait a minute before retrying.
The $__fromTime and $__toTime macros are injected by the Grafana ClickHouse plugin and map to the dashboard time range picker. The Merge() functions resolve the partial aggregation states stored in the metrics table; use them whenever querying the metrics table.
Always set Query Type to Table when querying Better Stack through the ClickHouse plugin. The Time series query type applies extra processing that conflicts with ClickHouse's aggregate merge functions and may return no data.
If you see Unknown expression or function identifier '$__fromTime', your plugin version may use a different macro name. Try toDateTime($__from / 1000) and toDateTime($__to / 1000) as an alternative.
You will get three lines (average, minimum, and maximum duration) in a single panel backed by sub-second queries.
Build a second panel: request volume by status code
Not every signal warrants a permanently defined metric. This chart counts requests per minute directly from the raw logs table, using the wide events tier. It is slower for long windows but useful for attributes you are still exploring.
Add another panel and set Query Type to Table.
Paste the query below into the SQL editor. The UNION ALL combines hot storage for recent data with cold S3 storage for historical records.
Grafana SQL editor: full history
Copied!
SELECT
toStartOfMinute(dt) AS time,
JSONExtract(raw, 'status', 'Nullable(UInt16)') AS status_code,
count(*) AS requests
FROM (
SELECT dt, raw
FROM remote(t123456_your_source_logs)
UNION ALL
SELECT dt, raw
FROM s3Cluster(primary, t123456_your_source_s3)
WHERE _row_type = 1
)
WHERE dt >= $__fromTime
AND dt < $__toTime
AND status_code IS NOT NULL
GROUP BY time, status_code
ORDER BY time ASC
For time ranges under 24 hours, drop the s3Cluster union to keep latency low:
Grafana SQL editor: recent data only
Copied!
SELECT
toStartOfMinute(dt) AS time,
JSONExtract(raw, 'status', 'Nullable(UInt16)') AS status_code,
count(*) AS requests
FROM remote(t123456_your_source_logs)
WHERE dt >= $__fromTime
AND dt < $__toTime
GROUP BY time, status_code
ORDER BY time ASC
Set the visualization to Time series. Grafana splits the results into separate lines per status code automatically. Set Stack series to Normal for a stacked area chart.
The SQL API supports up to 4 concurrent queries for logs and 20 for metrics on the Standard plan. Always include a LIMIT and a tight WHERE dt range to avoid hitting these limits. See Query limits for details.
Build a log table
A table panel is the right choice for surfacing individual log entries alongside parsed fields.
Add a third panel and set the visualization to Table.
Set Query Type to Table.
Paste the query below into the SQL editor.
Grafana SQL editor
Copied!
SELECT
dt AS time,
JSONExtract(raw, 'level', 'Nullable(String)') AS level,
JSONExtract(raw, 'route', 'Nullable(String)') AS route,
JSONExtract(raw, 'status', 'Nullable(UInt16)') AS status,
JSONExtract(raw, 'duration', 'Nullable(Float64)') AS duration_ms,
JSONExtract(raw, 'message', 'Nullable(String)') AS message
FROM remote(t123456_your_source_logs)
WHERE dt >= $__fromTime
AND dt < $__toTime
AND JSONExtract(raw, 'level', 'Nullable(String)') IN ('error', 'warn')
ORDER BY dt DESC
LIMIT 200
The LIMIT 200 guard prevents the panel from hitting concurrent query limits. For longer time windows, tighten the WHERE clause or use S3 sampling:
Grafana SQL editor: S3 sampling
Copied!
FROM s3Cluster(primary, t123456_your_source_s3, sampling = 0.1)
WHERE _row_type = 1
A sampling factor of 0.1 reads 10% of stored S3 files, usually enough to surface representative error patterns without scanning the full dataset.
In the Transformations tab, add Organize fields by name to rename and reorder columns (for example, changing duration_ms to Duration (ms)). Add Filter data by values to let users drill down by level or route without touching the SQL.
Arrange the three panels on the dashboard canvas like this: