| --- | |
| { | |
| "title": "Dashboards", | |
| "sidebar_label": "Dashboard", | |
| "language": "en-US", | |
| "description": "How to build observability dashboards on Apache Doris in Grafana? This article covers importing prebuilt dashboards, the OTel data model, Counter rate calculation, template variables, and custom panel construction.", | |
| "keywords": [ | |
| "Grafana Dashboard", | |
| "Doris Dashboard", | |
| "OpenTelemetry", | |
| "MySQL data source", | |
| "template variables", | |
| "Counter rate", | |
| "Time Series", | |
| "Stat panel", | |
| "Table panel" | |
| ] | |
| } | |
| --- | |
| <!-- Knowledge type: capability definition + operational steps --> | |
| <!-- Applicable scenario: observability platform / dashboard construction and customization --> | |
| ## **Overview** | |
| This document explains how to use DOG Stack observability data in Grafana. | |
| DOG Stack uses Doris as the unified storage backend. Doris is MySQL-protocol compatible, so a single MySQL data source in Grafana can query all logs, traces, and metrics. The data path is: | |
| ```Plain | |
| Apps / Infrastructure → OpenTelemetry Collector → Doris Exporter → Doris → Grafana (MySQL DataSource) | |
| ``` | |
| This document covers: | |
| - Importing prebuilt dashboards: use the ready-made dashboards we provide. | |
| - Understanding the data model: how OTel data is stored in Doris. | |
| - Creating custom dashboards: build panels, queries, and variables from scratch. | |
| - Reference: schema and syntax cheat sheet. | |
| ## **Prerequisites** | |
| Before you start, make sure: | |
| - DOG Stack is deployed, and both Grafana and Doris are running. | |
| - A MySQL-type data source connecting to Doris is configured in Grafana (the Grafana shipped with DOG Stack already has a pre-configured Doris data source). | |
| If you have not configured a data source yet, follow these steps: | |
| 1. In Grafana's left-side menu, click **Connections** > **Data sources** > **Add data source**. | |
| 2. Select **MySQL**. | |
| 1. Fill in the connection information: | |
| \- **Host**: `<Doris-fe-host>:9030` | |
| \- **Database**: `otel` | |
| \- **User**: `root` (or the user you configured) | |
| \- **Password**: leave empty (if no password is set) | |
| 1. Click **Save & test** to confirm the connection works. | |
|  | |
| ## **Importing prebuilt dashboards** | |
| We provide 4 [prebuilt dashboards](https://github.com/velodb/ai-observe-stack/tree/master/grafana-dashboard) covering common observability scenarios: | |
| | Dashboard | File | What it monitors | | |
| | ------------------ | ----------------------------------- | ---------------------------------------------- | | |
| | Host Metrics | `host_metrics_dashboard.json` | CPU, memory, disk, network, system load | | |
| | JVM Monitoring | `jvm_metrics_dashboard.json` | Heap memory, GC, threads, CPU utilization | | |
| | K8s Observability | `k8s_kubelet_dashboard.json` | Pod / Node / Namespace resource usage | | |
| | Nginx Logs | `nginx_logs_dashboard.json` | Requests, status codes, top URLs, error logs | | |
| | PostgreSQL Metrics | `postgresql-metrics-dashboard.json` | Connections, transactions, DB size, Checkpoint, BGWriter | | |
| To import a prebuilt dashboard: | |
| 1. In Grafana's left-side menu, click **Dashboards** > **New** > **Import**. | |
| 2. Click **Upload dashboard JSON file** and choose the JSON file under `grafana-dashboard/`. | |
| 1. On the import page, set the data source to your configured Doris (MySQL) data source. | |
| 2. Click **Import**. | |
| 1. Repeat the steps above for the remaining JSON files. | |
| Once imported, the variable selectors at the top of the dashboard (such as Service, Namespace) automatically query their candidate values from Doris. | |
| ### Per-dashboard panel details | |
| **Host Metrics Dashboard** | |
| - Overview: CPU usage, memory usage, 1-minute load, root disk usage | |
| - CPU: usage trend, usage by Mode | |
| - Memory: memory details (Total / Available / Free / Cached / Buffers), usage trend | |
| - System Load: 1 / 5 / 15-minute load, open file descriptors | |
| - Disk: disk space usage table, read/write throughput | |
| - Network: ingress/egress traffic, errors and packet drops | |
|  | |
| **JVM Monitoring Dashboard** | |
| - Overview: heap usage, Full GC count, CPU usage, thread count | |
| - Memory: heap / non-heap trends; details for Old Gen / Eden / Survivor pools | |
| - GC: time (Young / Old) and counts | |
| - Thread & CPU: thread count trend, CPU utilization | |
|  | |
| **K8s Observability Dashboard** | |
| - Pods: CPU (millicores), memory, Limit utilization, status table (with restart count and uptime) | |
| - Nodes: CPU / memory trends, status table | |
| - Namespaces: CPU / memory aggregates, status table | |
|  | |
| **Nginx Logs Dashboard** | |
| - Overview: requests, errors, 5xx / 4xx stats, status-code pie chart | |
| - Trends: request volume / error log trends | |
| - Analysis: Top URL, Top IP, HTTP method distribution, top error messages | |
| - Recent Logs: access / error log details | |
|  | |
| **PostgreSQL Metrics Dashboard** | |
| - Connections: active connections (split by database), max connections | |
| - Transactions: commit rate, rollback count | |
| - Storage: database size (split by database), number of databases, number of tables | |
| - BGWriter: checkpoint count (scheduled / requested), buffer writes, BGWriter time | |
|  | |
| ## **Understanding the data model** | |
| Before creating a custom dashboard, you need to know how OTel data is stored in Doris. | |
| ### **Tables** | |
| Unlike Prometheus, in Doris you cannot query by metric name alone — you also need to know which table to query. OTel data is stored across multiple tables by signal type and metric type: | |
| | Table | Content | Value field | Query approach | | |
| | ------------------------ | -------------------- | ------------------------------- | ------------------------------------ | | |
| | `otel_metrics_gauge` | Instantaneous metrics| `value` | Aggregate directly, e.g. `AVG(value)`| | |
| | `otel_metrics_sum` | Cumulative counters | `value` (monotonically increasing) | Compute rate with `LAG()` | | |
| | `otel_metrics_histogram` | Histogram | `count`, `sum`, `bucket_counts` | Compute deltas with `LAG()` | | |
| | `otel_logs` | Logs | `body`, `severity_text` | `COUNT` aggregation or detail queries| | |
| | `otel_traces` | Traces | `duration`, `span_name` | Sort or aggregate by duration | | |
| To determine which table holds a given metric, run: | |
| ```SQL | |
| SELECT 'gauge' AS type, metric_name FROM otel.otel_metrics_gauge WHERE metric_name = 'your_metric_name' LIMIT 1 | |
| UNION ALL | |
| SELECT 'sum', metric_name FROM otel.otel_metrics_sum WHERE metric_name = 'your_metric_name' LIMIT 1 | |
| UNION ALL | |
| SELECT 'histogram', metric_name FROM otel.otel_metrics_histogram WHERE metric_name = 'your_metric_name' LIMIT 1; | |
| ``` | |
| To browse all available metrics, run the following on each table: | |
| ```SQL | |
| SELECT DISTINCT metric_name FROM otel.otel_metrics_gauge ORDER BY metric_name; | |
| ``` | |
| ### **Attribute fields** | |
| In Prometheus, all labels are accessed uniformly as `{key="value"}`. In Doris, attributes are spread across several `variant` (JSON) columns and accessed with bracket syntax: | |
| | Table | Field | Content | Example | | |
| | ------------ | --------------------- | ------------------------------------ | ------------------------------------ | | |
| | metrics | `attributes` | Metric dimensions (Prometheus labels)| `attributes['mode']` | | |
| | metrics | `resource_attributes` | Resource info (K8s pod/node, etc.) | `resource_attributes['k8s.pod.name']`| | |
| | logs | `log_attributes` | Log fields | `log_attributes['status']` | | |
| | traces | `span_attributes` | Span fields | `span_attributes['http.method']` | | |
| Note: `service_name` and `service_instance_id` are top-level columns, not part of `attributes`. Use them directly, e.g. `WHERE service_name = '...'`. | |
| When using `SELECT`, `GROUP BY`, `PARTITION BY`, or `LIKE`, you need to use `CAST` for type conversion: | |
| ```SQL | |
| -- Extract a value in SELECT | |
| CAST(attributes['device'] AS VARCHAR) AS device | |
| -- Pattern match in WHERE | |
| CAST(attributes['device'] AS VARCHAR) NOT LIKE 'veth%' | |
| -- Numeric comparison in WHERE | |
| CAST(log_attributes['status'] AS INT) >= 500 | |
| -- Use in GROUP BY | |
| GROUP BY CAST(resource_attributes['k8s.pod.name'] AS VARCHAR) | |
| ``` | |
| For simple equality comparisons in `WHERE`, `CAST` is usually unnecessary: | |
| ```SQL | |
| WHERE attributes['mode'] = 'idle' | |
| ``` | |
| Caution: Using a wrong attribute field name (for example, `log_attributes` on a metrics table) does not raise an error — it silently returns NULL, causing empty query results. | |
| ### **Counter metrics and rate calculation** | |
| For Gauge metrics, `value` directly represents the current value and can be aggregated with `AVG` or `MAX`. | |
| For Counter / Sum metrics, `value` is a monotonically increasing cumulative value (such as total CPU seconds or total network bytes). To get the rate, you need the delta between adjacent data points. Prometheus's `rate()` is implemented in Doris with the `LAG()` window function. | |
| Here is a generic rate template: | |
| ```SQL | |
| SELECT | |
| t.timestamp AS time, | |
| t.<dimension_field> AS metric, | |
| CASE | |
| WHEN UNIX_TIMESTAMP(t.timestamp) > UNIX_TIMESTAMP(t.prev_ts) | |
| AND t.value >= t.prev_value | |
| THEN (t.value - t.prev_value) / (UNIX_TIMESTAMP(t.timestamp) - UNIX_TIMESTAMP(t.prev_ts)) | |
| ELSE NULL | |
| END AS value | |
| FROM ( | |
| SELECT timestamp, value, <dimension_field>, | |
| LAG(value) OVER (PARTITION BY <dimension_field> ORDER BY timestamp) AS prev_value, | |
| LAG(timestamp) OVER (PARTITION BY <dimension_field> ORDER BY timestamp) AS prev_ts | |
| FROM otel.otel_metrics_sum | |
| WHERE metric_name = '<metric_name>' | |
| AND $__timeFilter(timestamp) | |
| ) t | |
| WHERE t.prev_ts IS NOT NULL | |
| ORDER BY time | |
| ``` | |
| To use this template, replace the following placeholders: | |
| - `<dimension_field>`: the field that splits the series, e.g. `CAST(attributes['device'] AS VARCHAR)`. | |
| - `<metric_name>`: the metric name, e.g. `node_network_receive_bytes_total`. | |
| The template includes three layers of safety: | |
| | Condition | Purpose | | |
| | --------------------------------------------------------- | ------------------------------------------------------ | | |
| | `WHERE t.prev_ts IS NOT NULL` | Skips the first row of each partition (`LAG` returns NULL) | | |
| | `UNIX_TIMESTAMP(t.timestamp) > UNIX_TIMESTAMP(t.prev_ts)` | Prevents divide-by-zero from duplicate timestamps | | |
| | `t.value >= t.prev_value` | Prevents negative deltas caused by counter resets | | |
| The choice of `PARTITION BY` directly determines whether the rate is correct. Choosing wrong causes data from different dimensions to be mixed. Here are the choices used in the prebuilt dashboards: | |
| | Scenario | PARTITION BY | | |
| | --------------------------------- | --------------------------------------------------------- | | |
| | CPU usage (multi-core aggregation)| `service_instance_id, CAST(attributes['cpu'] AS VARCHAR)` | | |
| | Disk I/O | `CAST(attributes['device'] AS VARCHAR)` | | |
| | Network traffic | `CAST(attributes['device'] AS VARCHAR)` | | |
| | GC time | `CAST(attributes['jvm.gc.name'] AS VARCHAR)` | | |
| ## **Creating a custom dashboard** | |
| This section walks through, in practice, creating a dashboard with multiple panels and variables from scratch. | |
| ### **Create a Dashboard and a Time Series panel** | |
| We will create a Time Series panel that shows the CPU usage trend of a K8s pod. This example uses a Gauge metric and shows the complete thought process of writing the SQL. | |
| 1. In Grafana's left-side menu, click **Dashboards** > **New** > **New dashboard**. | |
| 2. Click **Add visualization**. | |
| 1. From the data source dropdown, choose your configured MySQL (Doris) data source. | |
| 2. In the top-right of the query editor, switch to **Code** mode. | |
| Now we write the SQL query. The following builds it up step by step. | |
| **Write the base query.** The goal is to monitor pod CPU; the metric name is `k8s.pod.cpu.usage`. It is a Gauge metric stored in `otel_metrics_gauge`, where `value` can be aggregated directly: | |
| ```SQL | |
| SELECT timestamp, value | |
| FROM otel.otel_metrics_gauge | |
| WHERE metric_name = 'k8s.pod.cpu.usage' | |
| ``` | |
| **Add time filtering.** Grafana's MySQL data source provides two ways to filter on time: | |
| ```SQL | |
| -- Option A: use the $__timeFilter macro (recommended) | |
| WHERE metric_name = 'k8s.pod.cpu.usage' | |
| AND $__timeFilter(timestamp) | |
| -- Option B: use $__from and $__to (more flexible inside subqueries or JOINs) | |
| WHERE metric_name = 'k8s.pod.cpu.usage' | |
| AND timestamp >= FROM_UNIXTIME($__from/1000) | |
| AND timestamp < FROM_UNIXTIME($__to/1000) | |
| ``` | |
| Note: `$__from` and `$__to` are millisecond timestamps — divide by 1000 before using them in `FROM_UNIXTIME()`. | |
| **Add time bucketing.** Raw data points are too dense; aggregate them at a fixed interval. Use `FLOOR(UNIX_TIMESTAMP(timestamp) / N) * N` to round time to N-second buckets: | |
| ```SQL | |
| SELECT | |
| FLOOR(UNIX_TIMESTAMP(timestamp) / 20) * 20 AS time, | |
| AVG(value) AS value | |
| FROM otel.otel_metrics_gauge | |
| WHERE metric_name = 'k8s.pod.cpu.usage' | |
| AND timestamp >= FROM_UNIXTIME($__from/1000) | |
| AND timestamp < FROM_UNIXTIME($__to/1000) | |
| GROUP BY time | |
| ORDER BY time | |
| ``` | |
| **Split into multiple series by dimension.** For the Time Series format, Grafana's MySQL data source requires three columns: | |
| | Column | Purpose | | |
| | ----------------- | ---------------------------------------------------------------------- | | |
| | `time` or `time_sec` | X-axis time. Either a `datetime` value or a UNIX timestamp in seconds. | | |
| | `metric` | Series name. Grafana splits into multiple lines by distinct values of this column. | | |
| | `value` | Y-axis value. | | |
| Add the pod name as the `metric` column for the complete query: | |
| ```SQL | |
| SELECT | |
| FLOOR(UNIX_TIMESTAMP(timestamp) / 20) * 20 AS time, | |
| CAST(resource_attributes['k8s.pod.name'] AS VARCHAR) AS metric, | |
| AVG(value) AS value | |
| FROM otel.otel_metrics_gauge | |
| WHERE metric_name = 'k8s.pod.cpu.usage' | |
| AND timestamp >= FROM_UNIXTIME($__from/1000) | |
| AND timestamp < FROM_UNIXTIME($__to/1000) | |
| GROUP BY time, metric | |
| ORDER BY time | |
| ``` | |
| To monitor a different Gauge metric, change: | |
| - `metric_name`: the target metric name. | |
| - The `metric` column: the dimension used to split series, chosen from `attributes` or `resource_attributes`. | |
| 1. Paste the complete SQL into the query editor. | |
| 2. Below the editor, set **Format** to **Time series**. | |
| 1. Click the panel title to set a name. | |
| 2. In the right-side panel settings, find **Standard options** > **Unit** and choose an appropriate unit. | |
| 3. Click **Apply** at the top right. | |
| ### **Add a Counter metric panel** | |
| This section creates a Time Series panel showing the rate of a Counter metric. A Counter's `value` is a monotonically increasing cumulative value — use the [generic rate template](#counter-metrics-and-rate-calculation) to compute the rate. | |
| 1. On the dashboard, click **Add** > **Visualization**. | |
| 2. Choose the data source and switch to **Code** mode. | |
| Replace the placeholders in the rate template with concrete values. The example below computes the network receive byte rate, split by network device: | |
| ```SQL | |
| SELECT | |
| t.timestamp AS time, | |
| t.device AS metric, | |
| CASE | |
| WHEN UNIX_TIMESTAMP(t.timestamp) > UNIX_TIMESTAMP(t.prev_ts) | |
| AND t.value >= t.prev_value | |
| THEN (t.value - t.prev_value) / (UNIX_TIMESTAMP(t.timestamp) - UNIX_TIMESTAMP(t.prev_ts)) | |
| ELSE NULL | |
| END AS value | |
| FROM ( | |
| SELECT timestamp, CAST(attributes['device'] AS VARCHAR) AS device, value, | |
| LAG(value) OVER (PARTITION BY CAST(attributes['device'] AS VARCHAR) ORDER BY timestamp) AS prev_value, | |
| LAG(timestamp) OVER (PARTITION BY CAST(attributes['device'] AS VARCHAR) ORDER BY timestamp) AS prev_ts | |
| FROM otel.otel_metrics_sum | |
| WHERE metric_name = 'node_network_receive_bytes_total' | |
| AND $__timeFilter(timestamp) | |
| ) t | |
| WHERE t.prev_ts IS NOT NULL | |
| ORDER BY time | |
| ``` | |
| To use a different Counter metric, change two things: | |
| - `metric_name`: the target metric name. | |
| - The dimension field in `PARTITION BY` and `SELECT`: the dimension used to split that metric (such as `attributes['cpu']`, `attributes['gc_name']`, etc.). | |
| - Paste the SQL and set **Format** to **Time series**. | |
| - In **Standard options** > **Unit**, choose an appropriate unit. | |
| - Click **Apply**. | |
| ### **Add a Stat panel** | |
| A Stat panel shows a single value, such as the current utilization or the latest count. The SQL just needs to return one value. | |
| 1. Click **Add** > **Visualization** and switch to **Code** mode. | |
| The following query gets the latest metric value within the time range: | |
| ```SQL | |
| SELECT value | |
| FROM otel.otel_metrics_gauge | |
| WHERE metric_name = 'node_memory_MemAvailable_bytes' | |
| AND $__timeFilter(timestamp) | |
| ORDER BY timestamp DESC | |
| LIMIT 1 | |
| ``` | |
| If you need to compute on multiple metrics (such as a ratio), see "Querying multiple metrics in one SQL" in [Optimization tips](#optimization-tips). | |
| 2. Paste the SQL and set **Format** to **Table** (Stat panels use Table format). | |
| 3. In the right-side panel settings, change the panel type to **Stat**. | |
| 4. In **Standard options** > **Unit**, choose an appropriate unit. | |
| 5. Click **Apply**. | |
| ### **Add a Table panel** | |
| The Table panel is suitable for multi-row, multi-column data; each column alias in the SQL becomes a column header. | |
| 1. Click **Add** > **Visualization** and switch to **Code** mode. | |
| The following query shows recent log details: | |
| ```SQL | |
| SELECT | |
| timestamp, | |
| service_name, | |
| severity_text, | |
| body, | |
| CAST(log_attributes['your_key'] AS VARCHAR) AS your_key | |
| FROM otel.otel_logs | |
| WHERE $__timeFilter(timestamp) | |
| ORDER BY timestamp DESC | |
| LIMIT 100 | |
| ``` | |
| Replace `your_key` with the actual log attribute field name. To see the available attributes, run: | |
| ```SQL | |
| SELECT log_attributes FROM otel.otel_logs LIMIT 1; | |
| ``` | |
| 2. Paste the SQL and set **Format** to **Table**. | |
| 3. Click **Apply**. | |
| ### **Add template variables** | |
| Template variables add interactive dropdown filters to a dashboard, letting users filter data without modifying the SQL. | |
| **Single-select variable:** | |
| 1. Click the gear icon at the top right of the dashboard, then go to **Settings** > **Variables** > **New variable**. | |
| 1. Fill in the configuration: | |
| \- **Name**: `service_name` | |
| \- **Type**: **Query** | |
| \- **Data source**: choose your Doris data source | |
| \- **Query**: | |
| `sql SELECT DISTINCT service_name FROM otel.otel_metrics_gauge WHERE service_name != '' AND service_name IS NOT NULL AND $__timeFilter(timestamp) ORDER BY service_name ` | |
| 1. Click **Apply**. | |
| In a panel's SQL, reference a single-select variable with the `$variable` syntax: | |
| ```SQL | |
| AND service_name = '$service_name' | |
| ``` | |
| **Multi-select variable:** | |
| 1. Create a new variable with the following configuration: | |
| \- **Name**: `namespace` | |
| \- **Type**: **Query** | |
| \- **Multi-value**: checked | |
| \- **Include All option**: checked | |
| \- **Query**: | |
| `sql SELECT DISTINCT CAST(resource_attributes['k8s.namespace.name'] AS VARCHAR) AS __text FROM otel.otel_metrics_gauge WHERE metric_name = 'k8s.pod.phase' AND timestamp >= NOW() - INTERVAL 1 HOUR ORDER BY 1 ` | |
| 1. Click **Apply**. | |
| Note: The column alias `__text` is a Grafana convention used to control the display text of the dropdown. | |
| In a panel's SQL, reference a multi-select variable with the `${variable:sqlstring}` syntax combined with `IN()`: | |
| ```SQL | |
| AND CAST(resource_attributes['k8s.namespace.name'] AS VARCHAR) IN (${namespace:sqlstring}) | |
| ``` | |
| Caution: Multi-select variables produce a SQL syntax error if `:sqlstring` is omitted. | |
| **Cascading variables:** | |
| The candidate values of one variable can depend on another. For example, the following Pod variable filters its candidates by the currently selected Namespace: | |
| ```SQL | |
| SELECT DISTINCT CAST(resource_attributes['k8s.pod.name'] AS VARCHAR) AS __text | |
| FROM otel.otel_metrics_gauge | |
| WHERE metric_name = 'k8s.pod.phase' | |
| AND timestamp >= NOW() - INTERVAL 1 HOUR | |
| AND CAST(resource_attributes['k8s.namespace.name'] AS VARCHAR) IN (${namespace:sqlstring}) | |
| ORDER BY 1 | |
| ``` | |
| ### **Optimization tips** | |
| The following are common optimization techniques used in the prebuilt dashboards. | |
| **Adjust the time-bucket interval.** Choose a bucket size that matches the time range. Modify N in `FLOOR(UNIX_TIMESTAMP(timestamp) / N) * N`: | |
| - 20 seconds: suited to short-window real-time monitoring. | |
| - 60 seconds: suited to hour-level overviews. | |
| - 300 seconds: suited to day-level trends. | |
| **Set meaningful series names.** Use `CONCAT()` to combine multiple fields: | |
| ```SQL | |
| CONCAT(device, ' read') AS metric | |
| ``` | |
| Use `CASE WHEN` to map numeric values to readable text: | |
| ```SQL | |
| CASE WHEN value = 2 THEN 'Running' | |
| WHEN value = 1 THEN 'Pending' | |
| WHEN value = 3 THEN 'Succeeded' | |
| WHEN value = 4 THEN 'Failed' | |
| ELSE 'Unknown' | |
| END AS status | |
| ``` | |
| **Handle divide-by-zero and NULLs.** Use `NULLIF` to guard against divide-by-zero and `COALESCE` to provide default values: | |
| ```SQL | |
| / NULLIF(SUM(...), 0) | |
| COALESCE(restarts, 0) | |
| ``` | |
| **Query multiple metrics in one SQL.** Use `CASE WHEN metric_name` instead of JOIN: | |
| ```SQL | |
| SELECT timestamp AS time, | |
| SUM(CASE WHEN metric_name = 'node_memory_MemAvailable_bytes' THEN value END) AS available, | |
| SUM(CASE WHEN metric_name = 'node_memory_MemTotal_bytes' THEN value END) AS total | |
| FROM otel.otel_metrics_gauge | |
| WHERE metric_name IN ('node_memory_MemTotal_bytes', 'node_memory_MemAvailable_bytes') | |
| GROUP BY timestamp | |
| ``` | |
| **Filter out noise data.** Exclude virtual network interfaces: | |
| ```SQL | |
| AND CAST(attributes['device'] AS VARCHAR) NOT LIKE 'veth%' | |
| AND CAST(attributes['device'] AS VARCHAR) NOT LIKE 'br-%' | |
| AND CAST(attributes['device'] AS VARCHAR) != 'lo' | |
| ``` | |
| Keep only real filesystems: | |
| ```SQL | |
| AND CAST(attributes['fstype'] AS VARCHAR) IN ('ext4', 'xfs', 'btrfs') | |
| ``` | |
| ## **Reference** | |
| ### **OTel table schema** | |
| **Metrics tables (gauge / sum / histogram — shared fields)** | |
| | Field | Type | Description | | |
| | --------------------- | -------------- | --------------------- | | |
| | `service_name` | varchar(200) | Service name | | |
| | `timestamp` | datetime(6) | Data timestamp | | |
| | `service_instance_id` | varchar(200) | Service instance ID | | |
| | `metric_name` | varchar(200) | Metric name | | |
| | `metric_description` | text | Metric description | | |
| | `metric_unit` | text | Metric unit | | |
| | `attributes` | variant (JSON) | Metric dimensions | | |
| | `resource_attributes` | variant (JSON) | Resource attributes | | |
| | `scope_name` | text | Collector name | | |
| gauge-only field: `value` (double) | |
| sum extra fields: `value` (double), `aggregation_temporality` (text), `is_monotonic` (boolean) | |
| histogram extra fields: `count` (bigint), `sum` (double), `bucket_counts` (array\<bigint\>), `explicit_bounds` (array\<double\>), `min` (double), `max` (double) | |
| **Logs table** | |
| | Field | Type | Description | | |
| | --------------------- | -------------- | --------------------------------- | | |
| | `timestamp` | datetime(6) | Log timestamp | | |
| | `service_name` | varchar(200) | Service name | | |
| | `service_instance_id` | varchar(200) | Service instance ID | | |
| | `trace_id` | varchar(200) | Associated trace ID | | |
| | `span_id` | text | Associated span ID | | |
| | `severity_number` | int | Severity number | | |
| | `severity_text` | text | Severity text (INFO / WARN / ERROR) | | |
| | `body` | text | Log body | | |
| | `resource_attributes` | variant (JSON) | Resource attributes | | |
| | `log_attributes` | variant (JSON) | Log attributes | | |
| **Traces table** | |
| | Field | Type | Description | | |
| | --------------------- | -------------- | ------------------------------------------ | | |
| | `timestamp` | datetime(6) | Span start time | | |
| | `service_name` | varchar(200) | Service name | | |
| | `trace_id` | varchar(200) | Trace ID | | |
| | `span_id` | text | Span ID | | |
| | `parent_span_id` | text | Parent span ID | | |
| | `span_name` | text | Span name | | |
| | `span_kind` | text | Span kind (CLIENT / SERVER / INTERNAL) | | |
| | `end_time` | datetime(6) | Span end time | | |
| | `duration` | bigint | Duration (nanoseconds) | | |
| | `span_attributes` | variant (JSON) | Span attributes | | |
| | `events` | array | Span events | | |
| | `links` | array | Span links | | |
| | `status_code` | text | Status code (OK / ERROR / UNSET) | | |
| | `status_message` | text | Status message | | |
| | `resource_attributes` | variant (JSON) | Resource attributes | | |
| ### **Syntax cheat sheet** | |
| | Purpose | Syntax | | |
| | -------------------------------- | ------------------------------------------------------------ | | |
| | Time filter (macro) | `$__timeFilter(timestamp)` | | |
| | Time filter (manual) | `timestamp >= FROM_UNIXTIME($__from/1000)` | | |
| | Time bucket (20 seconds) | `FLOOR(UNIX_TIMESTAMP(timestamp) / 20) * 20 AS time` | | |
| | Time bucket (1 minute) | `UNIX_TIMESTAMP(DATE_FORMAT(timestamp, '%Y-%m-%d %H:%i:00')) * 1000 AS time` | | |
| | Attribute access | `attributes['key']` | | |
| | Attribute CAST | `CAST(attributes['key'] AS VARCHAR)` | | |
| | Single-select variable | `service_name = '$service_name'` | | |
| | Multi-select variable | `IN (${namespace:sqlstring})` | | |
| | Divide-by-zero guard | `/ NULLIF(..., 0)` | | |
| | Default value | `COALESCE(..., 0)` | | |
| | Series naming | `CONCAT(device, ' read') AS metric` | | |
| | Status mapping | `CASE WHEN value = 2 THEN 'Running' ... END` | | |
| | URL stripping query string | `SUBSTRING_INDEX(url, '?', 1)` | | |
| | Filter out virtual network cards | `NOT LIKE 'veth%'` + `NOT LIKE 'br-%'` + `!= 'lo'` | |