Files
Pranjul KalsiandGitHub f50d2b6f5a feat: add create/update dashboard tools to MCP server (#23) (#43)
This PR fullfills both requirement of the issue (#23)

### Summary
- In **internal/handler/tools/handler.go** - Adds create_dashboard MCP
tool. Validates input, decodes JSON into types.Dashboard, calls SigNoz
API, returns result or error.
- In **pkg/types** - Adds dashboard.go which Defines `Dashboard` schema
used by the MCP tool. Contains layout, variables, and widgets with JSON
and jsonschema annotations for validation and UI generation.
- In **internal/client/client.go** - Adds `CreateDashboard` which
Creates a dashboard by POSTing the marshaled `Dashboard` JSON to SigNoz.
Sets auth header, applies timeout, returns raw API response or error.
- Updates **README.md** with necessary details 


### DEMO

#### DEMO 1 


https://github.com/user-attachments/assets/d7e3ab04-741b-4964-a41a-6a55e6abce54

#### DEMO 2


https://github.com/user-attachments/assets/dc914c1f-67e3-4c32-8a31-d2d441c239fe


### Followup 
@pranay01 @pradeepitm12 Let me know if this is satisfactory. If yes,
I’ll add the Update Dashboard tool in the same PR.
2025-12-22 23:40:47 +05:30

1171 lines
47 KiB
Go

package dashboard
const PromqlQuery = `
SigNoz PromQL Guide
Source: https://signoz.io/docs/userguide/write-a-prom-query-with-new-format/
================================================================================
CRITICAL RULES
================================================================================
When writing PromQL queries in SigNoz, you MUST follow these rules:
1. Metric names with dots MUST be quoted and wrapped in curly braces
2. Label names with dots MUST be quoted
3. Metric name MUST be the first parameter inside curly braces (without a label operator)
4. Use double quotes for all quoted strings
5. Traditional metric names (letters, numbers, underscores only) can use old or new format, but prefer new format for consistency
================================================================================
SYNTAX PATTERN
================================================================================
New format syntax:
{<metric_name>, <label_filters>}
With dots in metric name:
{"metric.name.with.dots", label="value"}
With dots in label name:
{"metric.name", "label.name.with.dots"="value"}
================================================================================
KEY EXAMPLES
================================================================================
Example 1: Container CPU Utilization
sum by ("k8s.pod.name") (rate({"container.cpu.utilization","k8s.namespace.name"="ns"}[5m]))
Example 2: Histogram Quantile
histogram_quantile(0.95, sum by (le) (rate({"request.duration.bucket",job="api"}[5m])))
Example 3: Complex Query with Functions
max_over_time({"foo.bar.total",env=~"prod|stag"}[1h] offset 30m) / ignoring ("instance") group_left () sum without ("pod") (rate({"other.metric"}[5m]))
Example 4: Metrics Without Dots (Both Work)
sum by ("foo_attr") (rate({"foo_bar_total", env=~"prod|stage"}[1h] offset 30m))
================================================================================
EXAMPLES BY METRIC TYPE
================================================================================
COUNTER:
rate({"http_requests_total", job="api", status!~"5.."}[5m])
GAUGE:
avg_over_time({"kube_pod_container_resource_requests_cpu_cores",namespace="prod"}[10m])
HISTOGRAM:
histogram_quantile(0.95, sum by (le) (rate({"http_request_duration_seconds.bucket",job="api"}[5m])))
Note: Histogram metrics use .min, .max, .count, .bucket, .sum suffixes (not _ underscore)
SUMMARY:
sum(rate({"http_request_duration_seconds.sum"}[5m])) / sum(rate({"http_request_duration_seconds.count"}[5m]))
Note: Summary metrics use .count, .quantile, .sum suffixes (not _ underscore)
================================================================================
COMMON PATTERNS
================================================================================
Rate with Filters:
rate({"http.requests.total", job="api", status!~"5.."}[5m])
Aggregation with Grouping:
sum by ("service.name", "environment") (rate({"http.requests"}[5m]))
Multiple Label Filters:
{"metric.name", "label.one"="value1", "label.two"=~"value.*", label_three="value3"}
Using ignoring/without:
sum({"metric.a"}) / ignoring ("instance.id") sum({"metric.b"})
================================================================================
QUICK REFERENCE
================================================================================
Element | Has Dots? | Format
---------------------|-----------|----------------------------------------
Metric name | Yes | {"metric.name"}
Metric name | No | {"metric_name"} or metric_name
Label name | Yes | "label.name"="value"
Label name | No | label_name="value" or "label_name"="value"
Grouping label | Yes | by ("label.name")
Grouping label | No | by ("label_name") or by (label_name)
================================================================================
VALIDATION CHECKLIST
================================================================================
Before submitting a PromQL query, verify:
[ ] Metric name is wrapped in curly braces {}
[ ] Metric name is the first parameter inside {}
[ ] Metric name with dots is quoted: {"metric.name"}
[ ] Label names with dots are quoted: "label.name"="value"
[ ] Grouping labels with dots are quoted: by ("label.name")
[ ] All quotes are double quotes ", not single quotes '
[ ] Histogram/Summary metrics use dot suffixes (.bucket, .sum, .count)
================================================================================
ERROR PREVENTION
================================================================================
Common Mistakes:
WRONG: rate("http.requests.total"[5m]) // Missing curly braces
WRONG: rate({job="api", "http.requests.total"}[5m]) // Metric name not first
WRONG: rate({'http.requests.total'}[5m]) // Single quotes
WRONG: rate({"http.requests.total", service.name="api"}[5m]) // Missing quotes on label with dots
Correct Versions:
RIGHT: rate({"http.requests.total"}[5m]) // Curly braces present
RIGHT: rate({"http.requests.total", job="api"}[5m]) // Metric name first
RIGHT: rate({"http.requests.total"}[5m]) // Double quotes
RIGHT: rate({"http.requests.total", "service.name"="api"}[5m]) // Quoted labels with dots
================================================================================
SUMMARY
================================================================================
Always use the new format for consistency and OpenTelemetry compatibility.
Key takeaway: Wrap everything in curly braces, quote anything with dots, metric name goes first.
`
const ClickhouseSqlQueryForMetrics = `
SigNoz Metrics ClickHouse Query Examples
Source: https://signoz.io/docs/userguide/write-a-metrics-clickhouse-query/
================================================================================
BASIC QUERIES
================================================================================
Fingerprints and Labels:
SELECT DISTINCT fingerprint, labels
FROM signoz_metrics.distributed_time_series_v4
WHERE metric_name = 'signoz_calls_total'
AND temporality = 'Cumulative'
LIMIT 10
Extract Label Value:
SELECT DISTINCT fingerprint,
JSONExtractString(labels, 'service_name') AS service_name
FROM signoz_metrics.distributed_time_series_v4_1day
WHERE metric_name = 'signoz_calls_total'
AND temporality = 'Cumulative'
LIMIT 10
Raw Samples:
SELECT timestamp_ms, value
FROM signoz_metrics.distributed_samples_v4
WHERE metric_name = 'signoz_calls_total'
LIMIT 10
================================================================================
RATE CALCULATION PATTERN
================================================================================
Request Rate (Counter Metric):
SELECT ts, sum(per_series_value) AS value
FROM (
SELECT ts,
If((per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) < 0, nan,
If((ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window) >= 86400, nan,
(per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) /
(ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window))) AS per_series_value
FROM (
SELECT fingerprint,
toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts,
max(value) AS per_series_value
FROM signoz_metrics.distributed_samples_v4
INNER JOIN (
SELECT DISTINCT fingerprint
FROM signoz_metrics.time_series_v4_1day
WHERE metric_name = 'signoz_calls_total'
AND temporality = 'Cumulative'
AND unix_milli >= intDiv({{.start_timestamp_ms}}, 86400000) * 86400000
AND unix_milli < {{.end_timestamp_ms}}
AND JSONExtractString(labels, 'service_name') = 'frontend'
) AS filtered_time_series USING (fingerprint)
WHERE metric_name = 'signoz_calls_total'
AND unix_milli >= {{.start_timestamp_ms}}
AND unix_milli < {{.end_timestamp_ms}}
GROUP BY fingerprint, ts
ORDER BY fingerprint ASC, ts ASC
)
WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint ASC, ts ASC)
)
WHERE isNaN(per_series_value) = 0
GROUP BY ts
ORDER BY ts ASC
Key Pattern:
- Use lagInFrame() to calculate difference between consecutive values
- Handle counter resets (negative differences → nan)
- Handle large time gaps (>= 86400s → nan)
- Filter out nan values before aggregation
================================================================================
ERROR RATE (RATIO OF TWO METRICS)
================================================================================
SELECT A.ts AS ts, (A.value * 100) / B.value AS error_percentage
FROM (
-- Error count rate (status_code = ERROR)
SELECT ts, sum(value) AS value
FROM (
SELECT ts,
if(runningDifference(ts) <= 0, nan,
if(runningDifference(value) < 0, value / runningDifference(ts),
runningDifference(value) / runningDifference(ts))) AS value
FROM (
SELECT fingerprint,
toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts,
max(value) AS value
FROM signoz_metrics.distributed_samples_v4
INNER JOIN (
SELECT DISTINCT fingerprint
FROM signoz_metrics.time_series_v4_1day
WHERE metric_name = 'signoz_calls_total'
AND temporality = 'Cumulative'
AND unix_milli >= intDiv({{.start_timestamp_ms}}, 86400000) * 86400000
AND unix_milli < {{.end_timestamp_ms}}
AND JSONExtractString(labels, 'service_name') = 'redis'
AND JSONExtractString(labels, 'status_code') IN ['STATUS_CODE_ERROR']
) AS filtered_time_series USING (fingerprint)
WHERE metric_name = 'signoz_calls_total'
AND unix_milli >= {{.start_timestamp_ms}}
AND unix_milli <= {{.end_timestamp_ms}}
GROUP BY fingerprint, ts
ORDER BY fingerprint ASC, ts ASC
)
WHERE isNaN(value) = 0
)
GROUP BY ts
ORDER BY ts ASC
) AS A
INNER JOIN (
-- Total count rate (all status codes)
SELECT ts, sum(value) AS value
FROM (
SELECT ts,
if(runningDifference(ts) <= 0, nan,
if(runningDifference(value) < 0, value / runningDifference(ts),
runningDifference(value) / runningDifference(ts))) AS value
FROM (
SELECT fingerprint,
toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts,
max(value) AS value
FROM signoz_metrics.distributed_samples_v4
INNER JOIN (
SELECT DISTINCT fingerprint
FROM signoz_metrics.time_series_v4_1day
WHERE metric_name = 'signoz_calls_total'
AND temporality = 'Cumulative'
AND unix_milli >= intDiv({{.start_timestamp_ms}}, 86400000) * 86400000
AND unix_milli < {{.end_timestamp_ms}}
AND JSONExtractString(labels, 'service_name') = 'redis'
) AS filtered_time_series USING (fingerprint)
WHERE metric_name = 'signoz_calls_total'
AND unix_milli >= {{.start_timestamp_ms}}
AND unix_milli <= {{.end_timestamp_ms}}
GROUP BY fingerprint, ts
ORDER BY fingerprint ASC, ts ASC
)
WHERE isNaN(value) = 0
)
GROUP BY ts
ORDER BY ts ASC
) AS B ON A.ts = B.ts
Key Pattern:
- Calculate rate for numerator (errors)
- Calculate rate for denominator (total)
- Join on timestamp and compute ratio
================================================================================
HISTOGRAM QUANTILE (LATENCY PERCENTILES)
================================================================================
SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)),
groupArray(value), 0.99) AS p99_latency
FROM (
SELECT le, ts, sum(value) AS value
FROM (
SELECT le, ts,
if(runningDifference(ts) <= 0, nan,
if(runningDifference(value) < 0, value / runningDifference(ts),
runningDifference(value) / runningDifference(ts))) AS value
FROM (
SELECT fingerprint, le,
toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts,
max(value) AS value
FROM signoz_metrics.distributed_samples_v4
INNER JOIN (
SELECT DISTINCT JSONExtractString(labels, 'le') AS le, fingerprint
FROM signoz_metrics.time_series_v4_1day
WHERE metric_name = 'signoz_latency_bucket'
AND temporality = 'Cumulative'
AND unix_milli >= intDiv({{.start_timestamp_ms}}, 86400000) * 86400000
AND unix_milli < {{.end_timestamp_ms}}
AND JSONExtractString(labels, 'service_name') = 'frontend'
) AS filtered_time_series USING (fingerprint)
WHERE metric_name = 'signoz_latency_bucket'
AND unix_milli >= {{.start_timestamp_ms}}
AND unix_milli <= {{.end_timestamp_ms}}
GROUP BY fingerprint, le, ts
ORDER BY fingerprint ASC, le ASC, ts ASC
)
WHERE isNaN(value) = 0
)
GROUP BY le, ts
HAVING isNaN(value) = 0
ORDER BY le ASC, ts ASC
)
GROUP BY ts
ORDER BY ts ASC
Key Pattern:
- Extract le (less than or equal) bucket labels
- Calculate rate for each bucket
- Use histogramQuantile() to compute percentile (0.99 = P99)
- Group buckets by timestamp
================================================================================
KEY PATTERNS
================================================================================
Time Series Tables by Granularity:
- distributed_time_series_v4 - 1 hour granularity
- distributed_time_series_v4_6hrs - 6 hour granularity
- distributed_time_series_v4_1day - 1 day granularity
Use appropriate table for time range:
- Short queries (< 6h): time_series_v4
- Medium queries (6h-24h): time_series_v4_6hrs
- Long queries (> 24h): time_series_v4_1day
Timestamp Filtering:
Time series table (unix_milli):
unix_milli >= intDiv({{.start_timestamp_ms}}, 86400000) * 86400000
AND unix_milli < {{.end_timestamp_ms}}
Samples table (unix_milli):
unix_milli >= {{.start_timestamp_ms}}
AND unix_milli < {{.end_timestamp_ms}}
Label Filtering:
JSONExtractString(labels, 'label_name') = 'value'
JSONExtractString(labels, 'status_code') IN ['ERROR', 'WARN']
Rate Calculation Methods:
Using lagInFrame() (Recommended):
(per_series_value - lagInFrame(per_series_value, 1, 0) OVER rate_window) /
(ts - lagInFrame(ts, 1, toDate('1970-01-01')) OVER rate_window)
Using runningDifference():
if(runningDifference(value) < 0, value / runningDifference(ts),
runningDifference(value) / runningDifference(ts))
Time Interval Aggregation:
toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) -- 1 minute
toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(300)) -- 5 minutes
Variables:
{{.start_timestamp_ms}} - Start time (milliseconds)
{{.end_timestamp_ms}} - End time (milliseconds)
{{.service_name}} - Custom variable (user-defined)
{{.variable_name}} - Any custom variable
Using Variables:
JSONExtractString(labels, 'service_name') = {{.service_name}}
Common Metric Types:
- Counter (cumulative): signoz_calls_total, use rate calculation
- Histogram (buckets): signoz_latency_bucket, use histogramQuantile()
- Gauge (point-in-time): Direct aggregation (avg, max, min)
Performance Tips:
1. Use appropriate time_series table for query range
2. Filter on metric_name and temporality in time_series table
3. Use DISTINCT fingerprint in subquery for filtering
4. Add time range filters to both tables
5. Order by fingerprint and timestamp for window functions
6. Filter isNaN() values before final aggregation
`
const ClickhouseSqlQueryForLogs = `
SigNoz Logs ClickHouse Query Examples
Source: https://signoz.io/docs/userguide/logs_clickhouse_queries/
================================================================================
ESSENTIAL PATTERNS
================================================================================
Resource Filter Template (CTE):
WITH __resource_filter AS (
SELECT fingerprint
FROM signoz_logs.distributed_logs_v2_resource
WHERE (simpleJSONExtractString(labels, 'service.name') = 'myservice')
AND seen_at_ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
)
SELECT ...
FROM signoz_logs.distributed_logs_v2
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp >= $start_timestamp_nano
AND timestamp <= $end_timestamp_nano
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
Multiple Resource Filters:
WHERE (simpleJSONExtractString(labels, 'service.name') = 'demo')
AND (simpleJSONExtractString(labels, 'environment') = 'production')
================================================================================
TIMESERIES QUERIES
================================================================================
Basic Count Per Minute:
WITH __resource_filter AS (...)
SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM signoz_logs.distributed_logs_v2
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp >= $start_timestamp_nano
AND timestamp <= $end_timestamp_nano
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
GROUP BY ts
ORDER BY ts ASC;
Group By Standard Attribute:
SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
attributes_string['container_name'] as container_name,
toFloat64(count()) AS value
FROM signoz_logs.distributed_logs_v2
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp >= $start_timestamp_nano
AND timestamp <= $end_timestamp_nano
AND mapContains(attributes_string, 'container_name')
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
GROUP BY container_name, ts
ORDER BY ts ASC;
Multiple Filters (Severity + Attribute + Resource):
WITH __resource_filter AS (
SELECT fingerprint
FROM signoz_logs.distributed_logs_v2_resource
WHERE (simpleJSONExtractString(labels, 'service.name') = 'demo')
AND seen_at_ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
)
SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM signoz_logs.distributed_logs_v2
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp >= $start_timestamp_nano
AND timestamp <= $end_timestamp_nano
AND severity_text = 'INFO'
AND attributes_string['method'] = 'GET'
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
GROUP BY ts
ORDER BY ts ASC;
Using Indexed (Selected) Fields:
Replace: attributes_string['method']
With: attribute_string_method
WHERE severity_text = 'INFO'
AND attribute_string_method = 'GET'
================================================================================
VALUE QUERIES (SINGLE AGGREGATED VALUE)
================================================================================
Average from Timeseries:
WITH __resource_filter AS (...)
SELECT avg(value) as value, any(ts) as ts
FROM (
SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM signoz_logs.distributed_logs_v2
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp >= $start_timestamp_nano
AND timestamp <= $end_timestamp_nano
AND severity_text = 'INFO'
AND attributes_string['method'] = 'GET'
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
GROUP BY ts
ORDER BY ts ASC
)
================================================================================
COMMON USE CASES
================================================================================
Logs by Kubernetes Cluster:
SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS ts,
resource.k8s.cluster.name::String as k8s_cluster_name,
toFloat64(count()) AS value
FROM signoz_logs.distributed_logs_v2
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp >= $start_timestamp_nano
AND timestamp <= $end_timestamp_nano
AND k8s_cluster_name IS NOT NULL
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
GROUP BY k8s_cluster_name, ts
ORDER BY ts ASC;
================================================================================
KEY PATTERNS
================================================================================
Timestamp Filtering (Always Include Both):
ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
AND timestamp >= $start_timestamp_nano
AND timestamp <= $end_timestamp_nano
Attribute Access:
Resource Attributes (SELECT/GROUP BY):
resource.<keyname>::String
Examples:
resource.service.name::String
resource.k8s.cluster.name::String
resource.k8s.namespace.name::String
Resource Attributes (WHERE - Non-indexed):
simpleJSONExtractString(labels, 'resource.name')
Resource Attributes (WHERE - Indexed):
resource_string_resource$$name
Example: resource_string_service$$name
Standard Attributes (Non-indexed):
attributes_string['attribute_name']
Check existence: mapContains(attributes_string, 'attribute_name')
Standard Attributes (Indexed/Selected):
attribute_string_attribute_name
Example: attribute_string_method
Common Severity Filters:
severity_text = 'ERROR'
severity_text = 'WARN'
severity_text = 'INFO'
severity_text IN ('ERROR', 'FATAL')
Variables:
$start_timestamp - Unix timestamp (seconds)
$end_timestamp - Unix timestamp (seconds)
$start_timestamp_nano - Unix timestamp (nanoseconds)
$end_timestamp_nano - Unix timestamp (nanoseconds)
Common Fields:
- severity_text - Log severity level
- body - Log message body
- trace_id - Associated trace ID
- span_id - Associated span ID
- resource_fingerprint - Resource fingerprint for filtering
Performance Tips:
1. Always use resource filter CTE for service filtering
2. Include both timestamp filters (ts_bucket_start and timestamp)
3. Use indexed/selected attributes when available
4. Filter on IS NOT NULL for grouped fields
5. Add 1800s buffer to ts_bucket_start for safety
6. Use mapContains() before accessing non-indexed attributes
Indexed vs Non-Indexed Attributes:
When to use indexed (selected) fields:
- Frequently queried attributes
- High cardinality fields
- Performance-critical queries
Syntax comparison:
Non-indexed (slower): attributes_string['method'] = 'GET'
Indexed (faster): attribute_string_method = 'GET'
Check if attribute exists (non-indexed):
mapContains(attributes_string, 'attribute_name')
`
const ClickhouseSqlQueryForTraces = `
SigNoz Traces ClickHouse Query Examples
Source: https://signoz.io/docs/userguide/writing-clickhouse-traces-query/
================================================================================
ESSENTIAL PATTERNS
================================================================================
Resource Filter Template (CTE):
WITH __resource_filter AS (
SELECT fingerprint
FROM signoz_traces.distributed_traces_v3_resource
WHERE (simpleJSONExtractString(labels, 'service.name') = 'myservice')
AND seen_at_ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
)
SELECT ...
FROM signoz_traces.distributed_signoz_index_v3
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
Multiple Resource Filters:
WHERE (simpleJSONExtractString(labels, 'service.name') = 'frontend')
AND (simpleJSONExtractString(labels, 'environment') = 'production')
================================================================================
TIMESERIES QUERIES
================================================================================
Basic Count Per Minute:
WITH __resource_filter AS (...)
SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts,
toFloat64(count()) AS value
FROM signoz_traces.distributed_signoz_index_v3
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
GROUP BY ts
ORDER BY ts ASC;
Custom Interval (100ms):
SELECT fromUnixTimestamp64Milli(intDiv(toUnixTimestamp64Milli(timestamp), 100) * 100) AS interval,
toFloat64(count()) AS count
FROM signoz_traces.distributed_signoz_index_v3
WHERE resource_string_service$$name='frontend'
AND duration_nano >= 50*exp10(6) -- 50ms
AND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
GROUP BY interval
ORDER BY interval ASC;
Group By Standard Attribute:
SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval,
attributes_string['http.method'] AS method,
toFloat64(avg(durationNano)) AS value
FROM signoz_traces.distributed_signoz_index_v3
WHERE attributes_string['http.method'] != ''
AND timestamp > now() - INTERVAL 30 MINUTE
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - toIntervalMinute(30))) - 1800
GROUP BY (method, interval)
ORDER BY (method, interval) ASC;
================================================================================
VALUE QUERIES (SINGLE AGGREGATED VALUE)
================================================================================
Average from Timeseries:
SELECT avg(value) as value, any(ts) as ts
FROM (
SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts,
toFloat64(avg(duration_nano)) AS value
FROM signoz_traces.distributed_signoz_index_v3
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
AND http_method = 'GET'
GROUP BY ts
ORDER BY ts ASC
)
================================================================================
TABLE QUERIES
================================================================================
Group By with Table Format:
SELECT now() as ts,
http_method,
toFloat64(avg(duration_nano)) AS avg_duration_nano
FROM signoz_traces.distributed_signoz_index_v3
WHERE resource_fingerprint GLOBAL IN __resource_filter
AND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}
AND ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
AND http_method IS NOT NULL
AND http_method != ''
GROUP BY http_method, ts
ORDER BY avg_duration_nano DESC;
================================================================================
ADVANCED USE CASES
================================================================================
Extract Span Event Attributes:
WITH arrayFilter(x -> JSONExtractString(x, 'name')='Getting customer', events) AS filteredEvents
SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval,
toFloat64(count()) AS count,
arrayJoin(arrayMap(x -> JSONExtractString(JSONExtractString(x, 'attributeMap'), 'customer_id'),
filteredEvents)) AS customer_id
FROM signoz_traces.distributed_signoz_index_v3
WHERE not empty(filteredEvents)
AND timestamp > toUnixTimestamp(now() - INTERVAL 30 MINUTE)
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - toIntervalMinute(30))) - 1800
GROUP BY (customer_id, interval)
ORDER BY (customer_id, interval) ASC;
Latency Between Spans in Trace:
SELECT interval, round(avg(time_diff), 2) AS avg_latency_ms
FROM (
SELECT interval, traceID,
if(startTime1 != 0 AND startTime2 != 0,
(toUnixTimestamp64Nano(startTime2) - toUnixTimestamp64Nano(startTime1)) / 1000000,
nan) AS time_diff
FROM (
SELECT toStartOfInterval(timestamp, toIntervalMinute(1)) AS interval,
traceID,
minIf(timestamp, resource_string_service$$name='driver' AND name='/driver.DriverService/FindNearest') AS startTime1,
minIf(timestamp, resource_string_service$$name='route' AND name='HTTP GET /route') AS startTime2
FROM signoz_traces.distributed_signoz_index_v3
WHERE timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}
AND ts_bucket_start BETWEEN {{.start_timestamp}} - 1800 AND {{.end_timestamp}}
AND resource_string_service$$name IN ('driver', 'route')
GROUP BY (interval, traceID)
)
)
WHERE isNaN(time_diff) = 0
GROUP BY interval
ORDER BY interval ASC;
Cross-Signal Query (Logs):
SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 1 MINUTE) AS interval,
toFloat64(count()) AS value
FROM signoz_logs.distributed_logs_v2
WHERE timestamp > toUnixTimestamp64Nano(now64() - INTERVAL 30 MINUTE)
AND ts_bucket_start >= toUInt64(toUnixTimestamp(now() - toIntervalMinute(30))) - 1800
GROUP BY interval
ORDER BY interval ASC;
================================================================================
KEY PATTERNS
================================================================================
Timestamp Filtering (Always Include Both):
ts_bucket_start BETWEEN $start_timestamp - 1800 AND $end_timestamp
AND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}
Attribute Access:
Resource Attributes (SELECT/GROUP BY):
resource.<keyname>::String
Example: resource.service.name::String
Resource Attributes (WHERE - Non-indexed):
simpleJSONExtractString(labels, 'resource.name')
Resource Attributes (WHERE - Indexed):
resource_string_resource$$name
Standard Attributes (Non-indexed):
attributes_string['attribute_name']
Standard Attributes (Indexed):
attribute_string_attribute_name
Duration Conversions:
duration_nano >= 50*exp10(6) -- 50ms to nanoseconds
duration_nano / 1000000 -- nanoseconds to milliseconds
Variables:
$start_timestamp - Unix timestamp (seconds)
$end_timestamp - Unix timestamp (seconds)
{{.start_datetime}} - DateTime format
{{.end_datetime}} - DateTime format
Common Fields:
- has_error - Boolean (span error status)
- http_method - String (GET, POST, etc.)
- duration_nano - UInt64 (span duration)
- traceID - String (trace identifier)
- resource_fingerprint - UInt64 (for filtering)
- events - Array (span events)
Performance Tips:
1. Always use resource filter CTE for service filtering
2. Include both timestamp filters
3. Use indexed attributes when available
4. Filter on IS NOT NULL and != '' for string fields
5. Add 1800s buffer to ts_bucket_start for safety
`
const Querybuilder = `
SigNoz Query Builder Reference
Consolidated guide for SigNoz Query Builder, search syntax, operators, and advanced querying.
================================================================================
METRICS NAMING CONVENTIONS (CRITICAL FOR QUERY BUILDER)
================================================================================
When querying OpenTelemetry metrics in Query Builder, you MUST use the correct metric name format:
CRITICAL RULE: OpenTelemetry histogram and summary metrics use DOT NOTATION for suffixes, NOT underscores.
Correct Format (with dots):
- otelcol_connector_received_items.sum
- otelcol_processor_batch_batch_send_size.sum
- signoz_latency.sum
- signoz_latency.count
- signoz_latency.bucket
- http_request_duration_seconds.sum
- http_request_duration_seconds.count
WRONG Format (with underscores - DO NOT USE):
- otelcol_connector_received_items_sum ❌ WRONG
- otelcol_connector_received_items_count ❌ WRONG
- signoz_latency_sum ❌ WRONG
- http_request_duration_seconds_count ❌ WRONG
Common Metric Suffixes (use dots):
.sum - Sum of all values (histogram/summary)
.count - Count of observations (histogram/summary)
.bucket - Histogram bucket (for percentile calculations)
.min - Minimum value
.max - Maximum value
Examples by Metric Type:
Counter Metrics (no suffix needed):
- http_requests_total
- signoz_calls_total
- otelcol_receiver_accepted_spans
Gauge Metrics (no suffix needed):
- system_memory_usage
- process_cpu_seconds_total
Histogram Metrics (use .sum, .count, .bucket):
- signoz_latency.sum (total latency)
- signoz_latency.count (number of requests)
- signoz_latency.bucket (for percentiles)
- http_request_duration.sum
- http_request_duration.count
Summary Metrics (use .sum, .count, .quantile):
- rpc_duration_seconds.sum
- rpc_duration_seconds.count
When selecting metrics in Query Builder:
1. Search for the base metric name (e.g., "otelcol_connector_received_items")
2. Select the appropriate suffix variant (e.g., "otelcol_connector_received_items.sum")
3. NEVER manually append _sum, _count, etc. with underscores
Common OpenTelemetry Collector Metrics:
- otelcol_receiver_accepted_spans.sum
- otelcol_receiver_refused_spans.sum
- otelcol_processor_batch_batch_send_size.sum
- otelcol_processor_batch_batch_send_size.count
- otelcol_exporter_sent_spans.sum
- otelcol_exporter_send_failed_spans.sum
- otelcol_connector_received_items.sum
- otelcol_connector_produced_items.sum
Common SigNoz Metrics:
- signoz_latency.sum
- signoz_latency.count
- signoz_latency.bucket
- signoz_calls_total
================================================================================
QUERY BUILDER CORE FEATURES
================================================================================
Filtering:
Apply filters using operators: =, !=, >, >=, <, <=, IN, NOT IN, LIKE, ILIKE, CONTAINS, EXISTS
Example: service.name = 'api' AND http.status_code >= 400
Aggregation Functions:
Statistical: count(), count_distinct(), sum(), avg(), min(), max()
Percentiles: p50(), p95(), p99()
Rate: rate(), rate_sum(), rate_avg()
Grouping & Result Manipulation:
Group By: Segment data by attributes
Order By: Sort ascending/descending
Limit: Top N results (for time series: limits number of series, not data points)
Having: Filter aggregated results - count() > 1000 AND count() < 5000
Legend Format: Customize labels with {{attribute}}
Missing Values: Records without grouping key → grouped as "". Use field EXISTS to exclude.
Multi-Query Analysis:
Combine queries with formulas:
- Error Rate: (errors / total) * 100
- SLA: (requests_under_200ms / total) * 100
Math Functions: +, -, *, /, log, ln, sqrt, exp, sin, cos, abs, now
Time Aggregation:
Auto Step Interval:
- Traces/Logs: Min 5s, auto (time_range / 300) rounded to 5s
- Metrics: Min 60s, varies by time range
- Max: 1,500 data points per series
Examples:
- 1 hour → 10s steps (360 points)
- 24 hours → 5min steps (300 points)
Trace Operators (V5):
Analyze parent-child relationships:
- => Direct descendant
- -> Indirect descendant
- && AND within trace
- || OR
- NOT Negation
Example: service.name = 'frontend' => service.name = 'customer'
Metrics-Specific Features:
Temporal Aggregation: Rate, Increase
Spatial Aggregation: Across dimensions (service, region, etc.)
Advanced Functions:
- Exclusion: Cut Off Min/Max
- Arithmetic: Absolute, Log2, Log10
- Smoothing: EWMA 3/5/7
- Time Shift: Time Shift 3600 (1 hour)
================================================================================
SEARCH SYNTAX
================================================================================
Basic Structure:
field operator value
Combine: condition1 AND condition2 OR condition3
Precedence: NOT > AND > OR (use parentheses!)
Operators Reference:
Comparison:
=, !=, <>, <, <=, >, >=
String Matching:
- LIKE '%pattern%' - Case-sensitive, wildcards: % (any chars), _ (single char)
- ILIKE '%pattern%' - Case-insensitive
- CONTAINS 'text' - Contains text
- REGEXP 'pattern' - RE2 regex
Range & Set:
- BETWEEN min AND max - Inclusive range
- IN ('val1', 'val2') - Value in list
Existence:
- EXISTS - Field exists
- NOT EXISTS - Field missing
Boolean:
- AND, OR, NOT
Field Existence Behavior:
CRITICAL: Operators behave differently with missing fields!
Positive Operators (Check Existence):
=, >, <, LIKE, ILIKE, BETWEEN, IN, CONTAINS, REGEXP
Example: response_time > 500 only matches records WITH the field.
Negative Operators (Don't Check):
!=, <>, NOT LIKE, NOT ILIKE, NOT BETWEEN, NOT IN, NOT CONTAINS, NOT REGEXP
Example: service.name != 'redis' matches records WITHOUT the field too!
Best Practice:
Ambiguous:
service.name != 'redis' // Includes records without service.name!
Explicit:
service.name EXISTS AND service.name != 'redis'
Or use positive operators:
service.name IN ('api', 'web', 'auth') // Automatically checks existence
Common Pitfalls:
1. Missing quotes: status = 'active' not status = active
2. LIKE wildcards: message LIKE '%error%' not message LIKE 'error'
3. Array syntax: region IN ('us', 'eu') not region IN 'us', 'eu'
4. Precedence: (a OR b) AND c not a OR b AND c
5. Case: LIKE is case-sensitive, ILIKE is case-insensitive
Regular Expression (REGEXP):
RE2 Syntax - Fast, safe, linear time
Common Patterns:
. - Any character
* - Zero or more
+ - One or more
? - Zero or one
[abc] - Character set
\d - Digit
\w - Word character
\s - Whitespace
Examples:
email REGEXP '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
ip_address REGEXP '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
url REGEXP '^https://'
level REGEXP '^(ERROR|WARN|FATAL)$'
Note: RE2 doesn't support lookahead/lookbehind or backreferences.
================================================================================
FULL-TEXT SEARCH (LOGS ONLY)
================================================================================
Searches log body field only.
Quoted vs Unquoted:
Quoted (Exact Phrase):
'failed to connect to database'
Unquoted (All Words):
error database connection // Finds logs with ALL three words
Escaping:
Only escape these two:
- Single quote: \'
- Backslash: \\
Examples:
'user\'s email'
'C:\\Program Files\\App'
'user\'s path: C:\\Users\\John'
No escaping needed for: ", [, ], $, etc.
Regex in Full-Text:
'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' // Email
'^\[SnapshotGenerator id=\d+\]' // Structured logs
Combining with Fields:
'payment failed' AND service.name = 'payment-service' AND severity_text = 'ERROR'
Performance Tips:
1. Narrow time range
2. Add field filters (service.name, severity_text)
3. Avoid leading wildcards
4. Start simple, add complexity incrementally
================================================================================
FUNCTIONS REFERENCE
================================================================================
For JSON body fields only.
hasToken(field, "value"):
Find whole-word token in string (not substring).
Examples:
hasToken(body, "[email protected]")
hasToken(body, "uuid-123")
Matches: uuid123 in "The uuid123 was generated"
Doesn't match: uuid123 in "abcuuid123xyz"
has(field, value):
Check if array contains value.
Examples:
has(body.tags, 'production')
has(body.user_ids, 12345)
has(body.regions, 'us-east')
Requires: Field must be an array in JSON body.
Combining Functions:
has(body.tags, 'production') AND status_code IN (500, 502, 503)
has(body.user_ids, 12345) AND severity_text = 'ERROR'
NOT has(body.tags, 'debug')
Common Errors:
- "unknown function" - Check spelling/case
- "expects key and value" - Provide both parameters
- "supports only body JSON" - Use body. prefix (except hasToken)
================================================================================
ADVANCED SEARCH EXAMPLES
================================================================================
Complex Boolean Logic:
(service.name = 'auth' OR service.name = 'user') AND status_code >= 400
Nested Conditions:
region = 'us-east' AND (
(status = 'error' AND retry_count > 3) OR
(status = 'timeout' AND response_time_ms > 5000)
)
Production Incident Investigation:
deployment.environment = 'production' AND
severity_text IN ('ERROR', 'FATAL') AND (
(error.type = 'DatabaseError' AND body CONTAINS 'connection') OR
(error.type = 'TimeoutError' AND response_time_ms > 30000) OR
(http.status_code >= 500 AND http.url LIKE '%/critical/%')
)
User Experience Monitoring:
user.tier = 'premium' AND
service.name = 'api-gateway' AND (
(http.method = 'GET' AND response_time_ms > 2000) OR
(http.method = 'POST' AND response_time_ms > 5000)
) AND
k8s.namespace.name = 'production'
Security Event Detection:
(
(body CONTAINS 'authentication failed' AND client.ip NOT LIKE '10.%') OR
(http.status_code = 403 AND request_count > 10) OR
(body REGEXP '(sql injection|xss|csrf)' AND severity_text = 'WARNING')
)
Performance Degradation:
service.name = 'database-service' AND (
(query.type = 'SELECT' AND execution_time_ms > 1000 AND
table_name IN ('users', 'orders', 'payments')) OR
(query.type IN ('INSERT', 'UPDATE') AND execution_time_ms > 500 AND
affected_rows > 1000)
) AND deployment.environment = 'production'
Kubernetes Pod Issues:
k8s.pod.name LIKE 'payment-service-%' AND
k8s.namespace.name = 'production' AND (
(severity_text = 'ERROR' AND body CONTAINS 'OOMKilled') OR
(restart_count > 5) OR
(body CONTAINS 'CrashLoopBackOff')
)
Cross-Service Transaction Tracing:
(trace_id = '123e4567-e89b-12d3-a456-426614174000' OR
correlation_id = 'ORDER-2024-001') AND
service.name IN ('order-service', 'payment-service', 'inventory-service')
Feature Flag Analysis:
has(body.feature_flags, 'new-checkout-flow') AND (
(conversion.success = true AND response_time_ms < 3000) OR
(severity_text = 'ERROR' AND body CONTAINS 'feature flag evaluation failed')
) AND deployment.environment = 'production'
================================================================================
BEST PRACTICES
================================================================================
Query Building Workflow:
1. Start broad: service.name = 'payment' AND deployment.environment = 'production'
2. Add severity: ... AND severity_text IN ('ERROR', 'FATAL')
3. Add conditions: ... AND (http.status_code >= 500 OR response_time_ms > 5000)
4. Add context: ... AND user.tier = 'premium'
Performance Optimization:
Query Level:
1. Filter early - Apply restrictive filters first
2. Limit scope - Use specific time ranges
3. Resource attributes - Leverage indexed fields (service.name, severity_text)
4. Aggregation efficiency - Choose appropriate time windows
Dashboard Level:
1. Adjust refresh frequency
2. Optimize time ranges
3. Limit concurrent queries
Common Patterns:
Pattern | Example | Use Case
-----------------------|--------------------------------------|---------------------------
Multi-service | service IN ('a', 'b') | Cross-service analysis
Nested OR | (cond1 OR cond2) AND cond3 | Complex filtering
Range + Pattern | status >= 400 AND url LIKE '%api%' | API error tracking
Regex + Field | body REGEXP 'pattern' AND service='x'| Pattern detection
Array + Field | has(body.tags, 'x') AND status = 'y' | Tag-based filtering
Troubleshooting:
Slow Queries:
- Check time range
- Use resource attributes in WHERE
- Reduce GROUP BY cardinality
- Increase aggregation intervals
No Results:
- Verify filter syntax
- Check time range includes data
- Confirm field existence
Unexpected Results:
- Review AND vs OR logic
- Check operator precedence (use parentheses!)
- Validate field existence behavior
================================================================================
QUICK REFERENCE TABLES
================================================================================
Operators Summary:
Category | Positive (Checks Exists) | Negative (Doesn't Check)
-------------|----------------------------------|----------------------------------
Equality | = | !=, <>
Comparison | <, <=, >, >= | -
Pattern | LIKE, ILIKE | NOT LIKE, NOT ILIKE
Range | BETWEEN | NOT BETWEEN
Set | IN | NOT IN
Text | CONTAINS | NOT CONTAINS
Regex | REGEXP | NOT REGEXP
Existence | EXISTS | NOT EXISTS
Aggregation Functions:
Function | Syntax | Description
-----------------|-------------------------|--------------------------------
Count | count() | Total occurrences
Count Distinct | count_distinct(field) | Unique values
Sum | sum(field) | Total of values
Average | avg(field) | Mean value
Min/Max | min(field), max(field) | Range boundaries
Percentiles | p50(), p95(), p99() | Distribution analysis
Rate | rate() | Occurrences per time unit
Rate Sum | rate_sum(field) | Change in totals
Rate Avg | rate_avg(field) | Change in averages
Common Query Patterns:
Operation | Syntax | Example
-----------------|-----------------------------|---------------------------------
Equality | field = 'value' | service.name = 'api'
Comparison | field > value | duration > 1000
Pattern | field LIKE '%pattern%' | message LIKE '%error%'
Case-insensitive | field ILIKE '%pattern%' | path ILIKE '%/api/%'
In list | field IN (val1, val2) | region IN ('us', 'eu')
Exists | field EXISTS | user_id EXISTS
Full-text | 'exact phrase' | 'connection timeout'
Regex | field REGEXP 'pattern' | email REGEXP '^[a-z]+@'
Array check | has(field, value) | has(body.tags, 'prod')
Token search | hasToken(field, "token") | hasToken(body, "uuid123")
`