mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-24 13:20:27 +00:00
Compare commits
61 Commits
fix/waterf
...
nv/4172
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3980e084c | ||
|
|
4319dd9cef | ||
|
|
92a5e9b9c9 | ||
|
|
408a914129 | ||
|
|
0e304b1d40 | ||
|
|
58a9be24d3 | ||
|
|
adf439fcf1 | ||
|
|
a1a54c4bb2 | ||
|
|
3c1961d3fc | ||
|
|
c3efa0660b | ||
|
|
183dd09082 | ||
|
|
a351373c49 | ||
|
|
8e7653b90d | ||
|
|
5c40d6b68b | ||
|
|
31115df41c | ||
|
|
869c3dccb2 | ||
|
|
c5d7a7ef8c | ||
|
|
544b87b254 | ||
|
|
e885fb98e5 | ||
|
|
be227eec43 | ||
|
|
13263c1f25 | ||
|
|
ccbf410d15 | ||
|
|
03b98ff824 | ||
|
|
2cdba0d11c | ||
|
|
84d2885530 | ||
|
|
b82dcc6138 | ||
|
|
a14d5847b9 | ||
|
|
d184746142 | ||
|
|
c335e17e1d | ||
|
|
433dd0b2d0 | ||
|
|
05e97e246a | ||
|
|
bddfe30f6c | ||
|
|
7a01a5250d | ||
|
|
09c98c830d | ||
|
|
0fbb90cc91 | ||
|
|
15f0787610 | ||
|
|
22ebc7732c | ||
|
|
cff18edf6e | ||
|
|
cb49c0bf3b | ||
|
|
1cb6f94d21 | ||
|
|
68155f374b | ||
|
|
696524509f | ||
|
|
705cdab38c | ||
|
|
ae9b881413 | ||
|
|
05f4e15d07 | ||
|
|
1653c6d725 | ||
|
|
070b4b7061 | ||
|
|
7f4c06edd6 | ||
|
|
6bed20b5b9 | ||
|
|
033bd3c9b8 | ||
|
|
d4c9a923fd | ||
|
|
387dcb529f | ||
|
|
7a4da7bcc5 | ||
|
|
b152fae3fa | ||
|
|
2ed766726c | ||
|
|
8767f6a57d | ||
|
|
22d8c7599b | ||
|
|
1019264272 | ||
|
|
c950d7e784 | ||
|
|
1e279e6193 | ||
|
|
d3a278c43e |
@@ -62,6 +62,9 @@ export const getVolumeQueryPayload = (
|
||||
const k8sPVCNameKey = dotMetricsEnabled
|
||||
? 'k8s.persistentvolumeclaim.name'
|
||||
: 'k8s_persistentvolumeclaim_name';
|
||||
const legendTemplate = dotMetricsEnabled
|
||||
? '{{k8s.namespace.name}}-{{k8s.pod.name}}'
|
||||
: '{{k8s_namespace_name}}-{{k8s_pod_name}}';
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -133,7 +136,7 @@ export const getVolumeQueryPayload = (
|
||||
functions: [],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: 'Available',
|
||||
legend: legendTemplate,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
@@ -225,7 +228,7 @@ export const getVolumeQueryPayload = (
|
||||
functions: [],
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: 'Capacity',
|
||||
legend: legendTemplate,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
@@ -316,7 +319,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: 'Inodes Used',
|
||||
legend: legendTemplate,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
@@ -408,7 +411,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: 'Total Inodes',
|
||||
legend: legendTemplate,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
@@ -500,7 +503,7 @@ export const getVolumeQueryPayload = (
|
||||
},
|
||||
groupBy: [],
|
||||
having: [],
|
||||
legend: 'Inodes Free',
|
||||
legend: legendTemplate,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
queryName: 'A',
|
||||
|
||||
115
perses/common/common.cue
Normal file
115
perses/common/common.cue
Normal file
@@ -0,0 +1,115 @@
|
||||
package common
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Shared types
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
#TelemetryFieldKey: {
|
||||
name: string
|
||||
key?: string
|
||||
description?: string
|
||||
unit?: string
|
||||
signal?: string
|
||||
fieldContext?: string
|
||||
fieldDataType?: string
|
||||
materialized?: bool
|
||||
isIndexed?: bool
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Panel types
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
#ContextLinkProps: {
|
||||
url: string
|
||||
label: string
|
||||
}
|
||||
|
||||
#TimePreference: *"globalTime" | "last5Min" | "last15Min" | "last30Min" | "last1Hr" | "last6Hr" | "last1Day" | "last3Days" | "last1Week" | "last1Month"
|
||||
|
||||
#PrecisionOption: *2 | 0 | 1 | 3 | 4 | "full"
|
||||
|
||||
#Axes: {
|
||||
softMin?: number | *null
|
||||
softMax?: number | *null
|
||||
isLogScale?: bool | *false
|
||||
}
|
||||
|
||||
#LegendPosition: *"bottom" | "right"
|
||||
|
||||
#ThresholdWithLabel: {
|
||||
value: number
|
||||
unit?: string
|
||||
color: string
|
||||
format: "Text" | "Background"
|
||||
label?: string
|
||||
}
|
||||
|
||||
#ComparisonThreshold: {
|
||||
value: number
|
||||
operator: ">" | "<" | ">=" | "<=" | "="
|
||||
unit?: string
|
||||
color: string
|
||||
format: "Text" | "Background"
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Query types
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
#QueryName: =~"^[A-Za-z][A-Za-z0-9_]*$"
|
||||
|
||||
#Limit: int & >=0 & <=10000
|
||||
|
||||
#Offset: int & >=0
|
||||
|
||||
#ReduceTo: "sum" | "count" | "avg" | "min" | "max" | "last" | "median"
|
||||
|
||||
#MetricAggregation: close({
|
||||
metricName: string & !=""
|
||||
timeAggregation: "latest" | "sum" | "avg" | "min" | "max" | "count" | "rate" | "increase"
|
||||
spaceAggregation: "sum" | "avg" | "min" | "max" | "count" | "p50" | "p75" | "p90" | "p95" | "p99"
|
||||
reduceTo?: #ReduceTo
|
||||
temporality?: "delta" | "cumulative" | "unspecified"
|
||||
})
|
||||
|
||||
#ExpressionAggregation: close({
|
||||
expression: string & !=""
|
||||
alias?: string
|
||||
})
|
||||
|
||||
#Aggregation: #MetricAggregation | #ExpressionAggregation
|
||||
|
||||
#FilterExpression: close({
|
||||
expression: string
|
||||
})
|
||||
|
||||
#GroupByItem: close({
|
||||
name: string & !=""
|
||||
fieldDataType?: string
|
||||
fieldContext?: string
|
||||
})
|
||||
|
||||
#OrderByItem: close({
|
||||
columnName: string & !=""
|
||||
order: "asc" | "desc"
|
||||
})
|
||||
|
||||
#HavingExpression: close({
|
||||
expression: string
|
||||
})
|
||||
|
||||
#Function: close({
|
||||
name: "cutOffMin" | "cutOffMax" | "clampMin" | "clampMax" |
|
||||
"absolute" | "runningDiff" | "log2" | "log10" |
|
||||
"cumulativeSum" | "ewma3" | "ewma5" | "ewma7" |
|
||||
"median3" | "median5" | "median7" | "timeShift" |
|
||||
"anomaly" | "fillZero"
|
||||
args?: [...close({value: number | string | bool})]
|
||||
})
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Variable types
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
#VariableSortOrder: *"disabled" | "asc" | "desc"
|
||||
4
perses/cue.mod/module.cue
Normal file
4
perses/cue.mod/module.cue
Normal file
@@ -0,0 +1,4 @@
|
||||
module: "github.com/signoz"
|
||||
language: {
|
||||
version: "v0.12.0"
|
||||
}
|
||||
2117
perses/examples/current.json
Normal file
2117
perses/examples/current.json
Normal file
File diff suppressed because it is too large
Load Diff
868
perses/examples/perses.json
Normal file
868
perses/examples/perses.json
Normal file
@@ -0,0 +1,868 @@
|
||||
{
|
||||
"kind": "Dashboard",
|
||||
"metadata": {
|
||||
"name": "the-everything-dashboard",
|
||||
"project": "signoz"
|
||||
},
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "The everything dashboard",
|
||||
"description": "Trying to cover as many concepts here as possible"
|
||||
},
|
||||
"duration": "1h",
|
||||
"datasources": {
|
||||
"SigNozDatasource": {
|
||||
"default": true,
|
||||
"plugin": {
|
||||
"kind": "SigNozDatasource",
|
||||
"spec": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": [
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"name": "serviceName",
|
||||
"display": {
|
||||
"name": "serviceName"
|
||||
},
|
||||
"allowAllValue": true,
|
||||
"allowMultiple": false,
|
||||
"plugin": {
|
||||
"kind": "SigNozDynamicVariable",
|
||||
"spec": {
|
||||
"name": "service.name",
|
||||
"source": "Metrics",
|
||||
"sort": "disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"name": "statusCodesFromQuery",
|
||||
"display": {
|
||||
"name": "statusCodesFromQuery"
|
||||
},
|
||||
"allowAllValue": true,
|
||||
"allowMultiple": true,
|
||||
"plugin": {
|
||||
"kind": "SigNozQueryVariable",
|
||||
"spec": {
|
||||
"queryValue": "SELECT JSONExtractString(labels, 'http.status_code') AS status_code FROM signoz_metrics.distributed_time_series_v4_1day WHERE status_code != '' GROUP BY status_code",
|
||||
"sort": "asc"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "ListVariable",
|
||||
"spec": {
|
||||
"name": "limit",
|
||||
"display": {
|
||||
"name": "limit"
|
||||
},
|
||||
"allowAllValue": false,
|
||||
"allowMultiple": false,
|
||||
"plugin": {
|
||||
"kind": "SigNozCustomVariable",
|
||||
"spec": {
|
||||
"customValue": "1,10,20,40,80,160,200",
|
||||
"sort": "disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"panels": {
|
||||
"24e2697b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "total resp size",
|
||||
"description": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozTimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"fillSpans": true
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "By",
|
||||
"decimalPrecision": 3
|
||||
},
|
||||
"axes": {
|
||||
"softMax": 800,
|
||||
"isLogScale": true
|
||||
},
|
||||
"legend": {
|
||||
"position": "right",
|
||||
"customColors": {
|
||||
"{service.name=\"sampleapp-gateway\"}": "#9ea5f7"
|
||||
}
|
||||
},
|
||||
"contextLinks": [
|
||||
{
|
||||
"label": "View service details",
|
||||
"url": "http://localhost:8080/{{_service.name}}?dfddf=%7B%7Blimit%7D%7D"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 1024,
|
||||
"unit": "By",
|
||||
"color": "Red",
|
||||
"format": "Text",
|
||||
"label": "upper limit"
|
||||
},
|
||||
{
|
||||
"value": 100,
|
||||
"unit": "By",
|
||||
"color": "Orange",
|
||||
"format": "Text",
|
||||
"label": "kinda bad"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "http.server.response.body.size.sum",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "http.response.status_code IN $statusCodesFromQuery"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "tag"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ff2f72f1": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "fraction of calls",
|
||||
"description": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozTimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"fillSpans": true
|
||||
},
|
||||
"formatting": {
|
||||
"decimalPrecision": 1
|
||||
},
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 1,
|
||||
"color": "Blue",
|
||||
"format": "Background",
|
||||
"label": "max possible"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozCompositeQuery",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"expression": "A",
|
||||
"disabled": true,
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "signoz_calls_total",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name IN $serviceName AND http.status_code IN $statusCodesFromQuery"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "B",
|
||||
"signal": "metrics",
|
||||
"expression": "B",
|
||||
"disabled": true,
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "signoz_calls_total",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name in $serviceName"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_formula",
|
||||
"spec": {
|
||||
"name": "F1",
|
||||
"expression": "A / B"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"011605e7": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "total resp size"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozBarChartPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"stackedBarChart": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "By"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "http.server.response.body.size.sum",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "http.response.status_code IN $statusCodesFromQuery"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"fieldDataType": "string",
|
||||
"fieldContext": "tag"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"e23516fc": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "num traces for service"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozNumberPanel",
|
||||
"spec": {
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": 1
|
||||
},
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 1200000,
|
||||
"operator": ">",
|
||||
"unit": "none",
|
||||
"color": "Red",
|
||||
"format": "Text"
|
||||
},
|
||||
{
|
||||
"value": 1200000,
|
||||
"operator": "<=",
|
||||
"unit": "none",
|
||||
"color": "Green",
|
||||
"format": "Text"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count() "
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name = $serviceName "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"130c8d6b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "num logs for service"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozNumberPanel",
|
||||
"spec": {
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count() "
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name = $serviceName "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"246f7c6d": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "num traces for service per resp code"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozPieChartPanel",
|
||||
"spec": {
|
||||
"formatting": {
|
||||
"decimalPrecision": 1
|
||||
},
|
||||
"legend": {
|
||||
"customColors": {
|
||||
"\"201\"": "#2bc051",
|
||||
"\"400\"": "#cc462e",
|
||||
"\"500\"": "#ff0000"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count() "
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name = $serviceName isEntryPoint = 'true'"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"name": "http.response.status_code",
|
||||
"fieldDataType": "float64",
|
||||
"fieldContext": "tag"
|
||||
}
|
||||
],
|
||||
"legend": "\"{{http.response.status_code}}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"21f7d4d0": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "average latency per service"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozTablePanel",
|
||||
"spec": {
|
||||
"formatting": {
|
||||
"columnUnits": {
|
||||
"A": "s"
|
||||
}
|
||||
},
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 1,
|
||||
"operator": ">",
|
||||
"unit": "min",
|
||||
"color": "Red",
|
||||
"format": "Text",
|
||||
"tableOptions": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozClickHouseSQL",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "WITH\n __spatial_aggregation_cte AS\n (\n SELECT\n toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts,\n `service.name`,\n le,\n sum(value) / 60 AS value\n FROM signoz_metrics.distributed_samples_v4 AS points\n INNER JOIN\n (\n SELECT\n fingerprint,\n JSONExtractString(labels, 'service.name') AS `service.name`,\n JSONExtractString(labels, 'le') AS le\n FROM signoz_metrics.time_series_v4\n WHERE (metric_name IN ('signoz_latency.bucket')) AND (LOWER(temporality) LIKE LOWER('delta')) AND (__normalized = 0)\n GROUP BY\n fingerprint,\n `service.name`,\n le\n ) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint\n WHERE metric_name IN ('signoz_latency.bucket')\n GROUP BY\n ts,\n `service.name`,\n le\n ),\n __histogramCTE AS\n (\n SELECT\n ts,\n `service.name`,\n histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.9) AS value\n FROM __spatial_aggregation_cte\n GROUP BY\n `service.name`,\n ts\n ORDER BY\n `service.name` ASC,\n ts ASC\n )\nSELECT\n `service.name` AS service,\n avg(value) AS A\nFROM __histogramCTE\nGROUP BY `service.name`"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"ad5fd556": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "logs from service"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozListPanel",
|
||||
"spec": {
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "log",
|
||||
"dataType": ""
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"type": "log",
|
||||
"dataType": ""
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"type": "",
|
||||
"dataType": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "LogQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count() "
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name = $serviceName"
|
||||
},
|
||||
"groupBy": [],
|
||||
"order": [
|
||||
{
|
||||
"columnName": "timestamp",
|
||||
"order": "desc"
|
||||
},
|
||||
{
|
||||
"columnName": "id",
|
||||
"order": "desc"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"f07b59ee": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "response size buckets"
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozHistogramPanel",
|
||||
"spec": {
|
||||
"histogramBuckets": {
|
||||
"bucketCount": 60,
|
||||
"mergeAllActiveQueries": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "http.server.response.body.size.bucket",
|
||||
"reduceTo": "avg",
|
||||
"spaceAggregation": "p90",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"e1a41831": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "trace operator",
|
||||
"description": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozTimeSeriesPanel",
|
||||
"spec": {
|
||||
"legend": {
|
||||
"position": "right"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozCompositeQuery",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count() "
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name = 'sampleapp-gateway' "
|
||||
},
|
||||
"legend": "Gateway"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "B",
|
||||
"signal": "traces",
|
||||
"expression": "B",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count() "
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "http.response.status_code = 200"
|
||||
},
|
||||
"legend": "$serviceName"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_trace_operator",
|
||||
"spec": {
|
||||
"name": "T1",
|
||||
"expression": "A -> B ",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()",
|
||||
"alias": "request_count"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"f0d70491": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "no results in this promql",
|
||||
"description": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozTimeSeriesPanel",
|
||||
"spec": {}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozCompositeQuery",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "promql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "sum(rate(flask_exporter_info[5m]))"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "promql",
|
||||
"spec": {
|
||||
"name": "B",
|
||||
"query": "sum(increase(flask_exporter_info[5m]))"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"0e6eb4ca": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": "no results in this promql",
|
||||
"description": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "SigNozTimeSeriesPanel",
|
||||
"spec": {}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "SigNozPromQLQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "sum(rate(flask_exporter_info[5m]))"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/24e2697b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/ff2f72f1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/011605e7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 6,
|
||||
"width": 6,
|
||||
"height": 3,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/e23516fc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 9,
|
||||
"width": 6,
|
||||
"height": 3,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/130c8d6b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/246f7c6d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 12,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/21f7d4d0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 18,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/ad5fd556"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 18,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/f07b59ee"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 24,
|
||||
"width": 12,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/e1a41831"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 0,
|
||||
"y": 30,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/f0d70491"
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 30,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/0e6eb4ca"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
140
perses/howToDefineAPanel.md
Normal file
140
perses/howToDefineAPanel.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Adding a new panel plugin
|
||||
|
||||
This guide is for developers adding a new panel kind to the Perses schema. It covers the file structure you need to create, the shared types available in `common.cue`, and how to compose them into your panel's spec.
|
||||
|
||||
---
|
||||
|
||||
## 1. File structure
|
||||
|
||||
Create a new directory under `schemas-wrapper/schemas/` named after your panel:
|
||||
|
||||
```
|
||||
schemas-wrapper/schemas/your-panel-name/
|
||||
├── your-panel-name.cue # Schema definition
|
||||
└── example.json # Example JSON that validates against the schema
|
||||
```
|
||||
|
||||
The CUE file must use `package model` and declare a `kind` and `spec`:
|
||||
|
||||
```cue
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "YourPanelName"
|
||||
spec: close({
|
||||
// your fields here
|
||||
})
|
||||
```
|
||||
|
||||
Register the panel in `schemas-wrapper/schemas/package.json` by adding an entry to the `plugins` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "YourPanelName"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Available shared types from `common.cue`
|
||||
|
||||
These types are defined in `common/common.cue` and can be used via `import "github.com/signoz/common"`. Use them instead of redefining the same structures in your panel.
|
||||
|
||||
## 3. Defining your spec
|
||||
|
||||
Your spec is a `close({})` block containing the fields relevant to your panel. Pick from the shared types above and add panel-specific types as needed. Every field should be optional (suffixed with `?`) since a panel should be valid with just defaults.
|
||||
|
||||
### Typical patterns
|
||||
|
||||
**Panel with graphs** (has axes, legend, thresholds as reference lines):
|
||||
|
||||
```cue
|
||||
spec: close({
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
axes?: common.#Axes
|
||||
legend?: #Legend
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
thresholds?: [...common.#ThresholdWithLabel]
|
||||
})
|
||||
```
|
||||
|
||||
**Panel with a single value** (no axes/legend, thresholds as conditional formatting):
|
||||
|
||||
```cue
|
||||
spec: close({
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
thresholds?: [...common.#ComparisonThreshold]
|
||||
})
|
||||
```
|
||||
|
||||
**Panel with custom data structure** (panel-specific fields only):
|
||||
|
||||
```cue
|
||||
spec: close({
|
||||
yourCustomConfig?: #YourCustomConfig
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
})
|
||||
```
|
||||
|
||||
### Defining panel-local types
|
||||
|
||||
Types that are specific to your panel (not reusable) should be defined in your CUE file, not in common. For example, `#Visualization` varies per panel because each panel has different rendering options:
|
||||
|
||||
```cue
|
||||
#Visualization: {
|
||||
timePreference?: common.#TimePreference
|
||||
yourCustomFlag?: bool | *false
|
||||
}
|
||||
```
|
||||
|
||||
If your panel needs a threshold type that extends a common one, embed it:
|
||||
|
||||
```cue
|
||||
#YourThreshold: {
|
||||
common.#ComparisonThreshold
|
||||
extraField: string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Writing the example JSON
|
||||
|
||||
Create an `example.json` that exercises all fields in your spec. This file is used for validation — `./validate.sh` will check it against your schema.
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "YourPanelName",
|
||||
"spec": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also add at least one panel using your new kind to `examples/perses.json` so it gets validated as part of a full dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 5. Validation
|
||||
|
||||
Run `./validate.sh` from the `perses/` directory. It lints `examples/perses.json` against all registered schemas. If your schema or example has issues, the error will point to the specific field.
|
||||
|
||||
---
|
||||
|
||||
## Quick reference: existing panels and what they use
|
||||
|
||||
| Field | Time-series | Bar-chart | Number | Pie | Table | Histogram | List |
|
||||
|-------|:-----------:|:---------:|:------:|:---:|:-----:|:---------:|:----:|
|
||||
| `visualization` | yes | yes | yes | yes | yes | — | — |
|
||||
| `formatting` | yes | yes | yes | yes | yes | — | — |
|
||||
| `axes` | yes | yes | — | — | — | — | — |
|
||||
| `legend` | yes | yes | — | yes | — | yes | — |
|
||||
| `contextLinks` | yes | yes | yes | yes | yes | yes | — |
|
||||
| `thresholds` | yes | yes | yes | — | yes | — | — |
|
||||
9
perses/schemas-wrapper/schemas/mf-manifest.json
Normal file
9
perses/schemas-wrapper/schemas/mf-manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "signoz",
|
||||
"name": "signoz",
|
||||
"metaData": {
|
||||
"buildInfo": {
|
||||
"buildVersion": "0.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
130
perses/schemas-wrapper/schemas/package.json
Normal file
130
perses/schemas-wrapper/schemas/package.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"name": "@signoz/signoz-schemas",
|
||||
"version": "0.0.1",
|
||||
"description": "SigNoz schema plugins",
|
||||
"perses": {
|
||||
"schemasPath": ".",
|
||||
"plugins": [
|
||||
{
|
||||
"kind": "Datasource",
|
||||
"spec": {
|
||||
"name": "SigNozDatasource"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozTimeSeriesPanel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozBarChartPanel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozNumberPanel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozPieChartPanel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozTablePanel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozHistogramPanel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozListPanel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"name": "SigNozIgnoredFieldsChart"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"name": "SigNozBuilderQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"name": "SigNozClickHouseSQL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"name": "SigNozPromQLQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"name": "SigNozCompositeQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"name": "SigNozTraceOperator"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"name": "SigNozFormula"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "LogQuery",
|
||||
"spec": {
|
||||
"name": "SigNozBuilderQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TraceQuery",
|
||||
"spec": {
|
||||
"name": "SigNozBuilderQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Variable",
|
||||
"spec": {
|
||||
"name": "SigNozCustomVariable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Variable",
|
||||
"spec": {
|
||||
"name": "SigNozDynamicVariable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Variable",
|
||||
"spec": {
|
||||
"name": "SigNozQueryVariable"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"kind": "SigNozBarChartPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "globalTime",
|
||||
"fillSpans": false,
|
||||
"stackedBarChart": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "By",
|
||||
"decimalPrecision": 2
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 1000,
|
||||
"isLogScale": false
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom",
|
||||
"customColors": {
|
||||
"series-A": "#9ea5f7"
|
||||
}
|
||||
},
|
||||
"contextLinks": [
|
||||
{
|
||||
"label": "View details",
|
||||
"url": "http://localhost:8080/{{_service.name}}"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 500,
|
||||
"unit": "By",
|
||||
"color": "Red",
|
||||
"format": "Text",
|
||||
"label": "upper limit"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozBarChartPanel"
|
||||
spec: close({
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
axes?: common.#Axes
|
||||
legend?: #Legend
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
thresholds?: [...common.#ThresholdWithLabel]
|
||||
})
|
||||
|
||||
#Visualization: {
|
||||
timePreference?: common.#TimePreference
|
||||
fillSpans?: bool | *false
|
||||
stackedBarChart?: bool | *true
|
||||
}
|
||||
|
||||
#Formatting: {
|
||||
unit?: string | *""
|
||||
decimalPrecision?: common.#PrecisionOption
|
||||
}
|
||||
|
||||
#Legend: {
|
||||
position?: common.#LegendPosition
|
||||
customColors?: [string]: string
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// Source: pkg/types/querybuildertypes/querybuildertypesv5/builder_query.go — QueryBuilderQuery
|
||||
kind: "SigNozBuilderQuery"
|
||||
spec: close({
|
||||
name: common.#QueryName
|
||||
signal: "metrics" | "logs" | "traces"
|
||||
expression: string
|
||||
disabled?: bool | *false
|
||||
|
||||
aggregations?: [...common.#Aggregation]
|
||||
filter?: common.#FilterExpression
|
||||
groupBy?: [...common.#GroupByItem]
|
||||
order?: [...common.#OrderByItem]
|
||||
selectFields?: [...common.#TelemetryFieldKey]
|
||||
limit?: common.#Limit
|
||||
limitBy?: #LimitBy
|
||||
offset?: common.#Offset
|
||||
cursor?: string
|
||||
having?: common.#HavingExpression
|
||||
// secondaryAggregations not added — not yet implemented.
|
||||
functions?: [...common.#Function]
|
||||
legend?: string
|
||||
stepInterval?: number
|
||||
reduceTo?: common.#ReduceTo
|
||||
pageSize?: int & >=1
|
||||
source?: string
|
||||
})
|
||||
|
||||
#LimitBy: close({
|
||||
keys: [...string]
|
||||
value: string
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"kind": "SigNozBuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "redis_keyspace_hits",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "sum"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "host_name IN $host_name"
|
||||
},
|
||||
"groupBy": [],
|
||||
"order": [],
|
||||
"disabled": false,
|
||||
"legend": "Hit/s across all hosts",
|
||||
"stepInterval": 60
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// Source: pkg/types/querybuildertypes/querybuildertypesv5/clickhouse_query.go — ClickHouseQuery
|
||||
kind: "SigNozClickHouseSQL"
|
||||
spec: close({
|
||||
name: common.#QueryName
|
||||
query: string & !=""
|
||||
disabled?: bool | *false
|
||||
legend?: string
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"kind": "SigNozClickHouseSQL",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts, count() AS total FROM signoz_logs.distributed_logs GROUP BY ts ORDER BY ts",
|
||||
"disabled": false,
|
||||
"legend": "Log count"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
bq "github.com/signoz/schemas-wrapper/schemas/signoz-builder-query:model"
|
||||
f "github.com/signoz/schemas-wrapper/schemas/signoz-formula:model"
|
||||
to "github.com/signoz/schemas-wrapper/schemas/signoz-trace-operator:model"
|
||||
pql "github.com/signoz/schemas-wrapper/schemas/signoz-promql:model"
|
||||
ch "github.com/signoz/schemas-wrapper/schemas/signoz-clickhouse-sql:model"
|
||||
)
|
||||
|
||||
// Source: pkg/types/querybuildertypes/querybuildertypesv5/req.go — CompositeQuery
|
||||
// SigNozCompositeQuery groups multiple query plugins into a single
|
||||
// query request. Each entry is a typed envelope whose spec is
|
||||
// validated by the corresponding plugin schema.
|
||||
|
||||
// this is to be used when there are multiple queries in a panel
|
||||
// in most cases, there will be only one query, and there it is a better idea to
|
||||
// use the corresponding kind for that query instead of this composite query
|
||||
kind: "SigNozCompositeQuery"
|
||||
spec: close({
|
||||
queries: [...#QueryEnvelope]
|
||||
})
|
||||
|
||||
// QueryEnvelope wraps a single query plugin with a type discriminator.
|
||||
#QueryEnvelope:
|
||||
close({
|
||||
type: "builder_query",
|
||||
spec: bq.spec
|
||||
}) |
|
||||
close({
|
||||
type: "builder_formula",
|
||||
spec: f.spec
|
||||
}) |
|
||||
close({
|
||||
type: "builder_trace_operator",
|
||||
spec: to.spec
|
||||
}) |
|
||||
close({
|
||||
type: "promql",
|
||||
spec: pql.spec
|
||||
}) |
|
||||
close({
|
||||
type: "clickhouse_sql",
|
||||
spec: ch.spec
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"kind": "SigNozCompositeQuery",
|
||||
"spec": {
|
||||
"queries": [
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"expression": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "redis_keyspace_hits",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "sum"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "host_name IN $host_name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_query",
|
||||
"spec": {
|
||||
"name": "B",
|
||||
"signal": "metrics",
|
||||
"expression": "B",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "redis_keyspace_misses",
|
||||
"timeAggregation": "rate",
|
||||
"spaceAggregation": "sum",
|
||||
"reduceTo": "sum"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "host_name IN $host_name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "builder_formula",
|
||||
"spec": {
|
||||
"name": "F1",
|
||||
"expression": "A / (A + B) * 100",
|
||||
"legend": "Hit rate %"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// defaultValue lives on the Perses ListVariable wrapper (spec level).
|
||||
kind: "SigNozCustomVariable"
|
||||
spec: close({
|
||||
customValue: =~"^[^,]+(,[^,]+)*$"
|
||||
sort?: common.#VariableSortOrder
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"kind": "SigNozCustomVariable",
|
||||
"spec": {
|
||||
"customValue": "production,staging,development",
|
||||
"sort": "disabled"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
kind: "SigNozDatasource"
|
||||
|
||||
// SigNoz has a single built-in backend — the frontend already knows
|
||||
// the API endpoint, so there is no connection config to validate.
|
||||
// Add fields here if SigNoz ever supports multiple backends or
|
||||
// configurable API versions.
|
||||
spec: close({})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"kind": "SigNozDatasource",
|
||||
"spec": {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// defaultValue lives on the Perses ListVariable wrapper (spec level).
|
||||
kind: "SigNozDynamicVariable"
|
||||
spec: close({
|
||||
name: string
|
||||
source: string
|
||||
sort?: common.#VariableSortOrder
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"kind": "SigNozDynamicVariable",
|
||||
"spec": {
|
||||
"name": "host_name",
|
||||
"source": "metrics",
|
||||
"sort": "asc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
// Fields from IBaseWidget that are NOT in any dedicated panel CUE file.
|
||||
// This file exists as a reference only and will be removed in the end.
|
||||
// Needs to be removed from package.json as well.
|
||||
|
||||
kind: "SigNozIgnoredFieldsChart"
|
||||
spec: close({
|
||||
opacity?: string // not wired to chart rendering
|
||||
nullZeroValues?: string // not wired to chart rendering
|
||||
stepSize?: number
|
||||
columnWidths?: [string]: number // not a config choice — persisted user-resized column widths
|
||||
// "Chart Appearance" section in UI — could not find this section in the app.
|
||||
// These 4 fields are gated behind panelTypeVs* constants (TIME_SERIES only).
|
||||
lineInterpolation?: #LineInterpolation
|
||||
showPoints?: bool
|
||||
lineStyle?: #LineStyle
|
||||
fillMode?: #FillMode
|
||||
})
|
||||
|
||||
#LineInterpolation: "linear" | "spline" | "stepAfter" | "stepBefore"
|
||||
|
||||
#LineStyle: "solid" | "dashed"
|
||||
|
||||
#FillMode: "solid" | "gradient" | "none"
|
||||
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// Source: pkg/types/querybuildertypes/querybuildertypesv5/formula.go — QueryBuilderFormula
|
||||
kind: "SigNozFormula"
|
||||
spec: close({
|
||||
name: common.#QueryName
|
||||
expression: string
|
||||
disabled?: bool | *false
|
||||
legend?: string
|
||||
limit?: common.#Limit
|
||||
having?: common.#HavingExpression
|
||||
stepInterval?: number
|
||||
order?: [...common.#OrderByItem]
|
||||
functions?: [...common.#Function]
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"kind": "SigNozFormula",
|
||||
"spec": {
|
||||
"name": "F1",
|
||||
"expression": "A / B * 100",
|
||||
"legend": "Hit rate %"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"kind": "SigNozHistogramPanel",
|
||||
"spec": {
|
||||
"histogramBuckets": {
|
||||
"bucketCount": 60,
|
||||
"bucketWidth": 100,
|
||||
"mergeAllActiveQueries": true
|
||||
},
|
||||
"legend": {
|
||||
"customColors": {
|
||||
"series-A": "#9ea5f7"
|
||||
}
|
||||
},
|
||||
"contextLinks": [
|
||||
{
|
||||
"label": "View histogram details",
|
||||
"url": "http://localhost:8080/histogram"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozHistogramPanel"
|
||||
spec: close({
|
||||
histogramBuckets?: #HistogramBuckets
|
||||
legend?: #Legend
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
})
|
||||
|
||||
#HistogramBuckets: {
|
||||
bucketCount?: number | *30
|
||||
bucketWidth?: number | *0
|
||||
mergeAllActiveQueries?: bool | *false
|
||||
}
|
||||
|
||||
#Legend: {
|
||||
customColors?: [string]: string
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"kind": "SigNozListPanel",
|
||||
"spec": {
|
||||
"selectedLogFields": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "log",
|
||||
"dataType": ""
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"type": "log",
|
||||
"dataType": ""
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"type": "",
|
||||
"dataType": "string"
|
||||
}
|
||||
],
|
||||
"selectedTracesFields": [
|
||||
{
|
||||
"name": "service.name",
|
||||
"signal": "traces",
|
||||
"fieldContext": "resource",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"signal": "traces",
|
||||
"fieldContext": "span",
|
||||
"fieldDataType": "string"
|
||||
},
|
||||
{
|
||||
"name": "duration_nano",
|
||||
"signal": "traces",
|
||||
"fieldContext": "span",
|
||||
"fieldDataType": ""
|
||||
},
|
||||
{
|
||||
"name": "http_method",
|
||||
"signal": "traces",
|
||||
"fieldContext": "span",
|
||||
"fieldDataType": ""
|
||||
},
|
||||
{
|
||||
"name": "response_status_code",
|
||||
"signal": "traces",
|
||||
"fieldContext": "span",
|
||||
"fieldDataType": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozListPanel"
|
||||
spec: close({
|
||||
selectedLogFields?: [...#LogField]
|
||||
selectedTracesFields?: [...common.#TelemetryFieldKey]
|
||||
})
|
||||
|
||||
#LogField: {
|
||||
name: string
|
||||
type: string
|
||||
dataType: string
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"kind": "SigNozNumberPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "globalTime"
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": 1
|
||||
},
|
||||
"contextLinks": [
|
||||
{
|
||||
"label": "View details",
|
||||
"url": "http://localhost:8080/details"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 1200000,
|
||||
"operator": ">",
|
||||
"unit": "none",
|
||||
"color": "Red",
|
||||
"format": "Text"
|
||||
},
|
||||
{
|
||||
"value": 1200000,
|
||||
"operator": "<=",
|
||||
"unit": "none",
|
||||
"color": "Green",
|
||||
"format": "Text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozNumberPanel"
|
||||
spec: close({
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
thresholds?: [...common.#ComparisonThreshold]
|
||||
})
|
||||
|
||||
#Visualization: {
|
||||
timePreference?: common.#TimePreference
|
||||
}
|
||||
|
||||
#Formatting: {
|
||||
unit?: string | *""
|
||||
decimalPrecision?: common.#PrecisionOption
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"kind": "SigNozPieChartPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "globalTime"
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "none",
|
||||
"decimalPrecision": 1
|
||||
},
|
||||
"legend": {
|
||||
"customColors": {
|
||||
"\"201\"": "#2bc051",
|
||||
"\"400\"": "#cc462e",
|
||||
"\"500\"": "#ff0000"
|
||||
}
|
||||
},
|
||||
"contextLinks": [
|
||||
{
|
||||
"label": "View breakdown",
|
||||
"url": "http://localhost:8080/breakdown"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozPieChartPanel"
|
||||
spec: close({
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
legend?: #Legend
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
})
|
||||
|
||||
#Visualization: {
|
||||
timePreference?: common.#TimePreference
|
||||
}
|
||||
|
||||
#Formatting: {
|
||||
unit?: string | *""
|
||||
decimalPrecision?: common.#PrecisionOption
|
||||
}
|
||||
|
||||
#Legend: {
|
||||
customColors?: [string]: string
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// Source: pkg/types/querybuildertypes/querybuildertypesv5/prom_query.go — PromQuery
|
||||
kind: "SigNozPromQLQuery"
|
||||
spec: close({
|
||||
name: common.#QueryName
|
||||
query: string & !=""
|
||||
disabled?: bool | *false
|
||||
step?: =~"^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" | number
|
||||
stats?: bool
|
||||
legend?: string
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"kind": "SigNozPromQLQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "rate(http_requests_total{status=\"200\"}[5m])",
|
||||
"disabled": false,
|
||||
"legend": "{{method}} {{path}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// defaultValue lives on the Perses ListVariable wrapper (spec level).
|
||||
kind: "SigNozQueryVariable"
|
||||
spec: close({
|
||||
queryValue: string
|
||||
sort?: common.#VariableSortOrder
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"kind": "SigNozQueryVariable",
|
||||
"spec": {
|
||||
"queryValue": "SELECT DISTINCT host_name FROM signoz_metrics.distributed_time_series_v4_1day WHERE metric_name = 'redis_cpu_time'",
|
||||
"sort": "asc"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"kind": "SigNozTablePanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "globalTime"
|
||||
},
|
||||
"formatting": {
|
||||
"columnUnits": {
|
||||
"A": "s"
|
||||
},
|
||||
"decimalPrecision": 2
|
||||
},
|
||||
"contextLinks": [
|
||||
{
|
||||
"label": "View trace",
|
||||
"url": "http://localhost:8080/trace/{{traceID}}"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 1,
|
||||
"operator": ">",
|
||||
"unit": "min",
|
||||
"color": "Red",
|
||||
"format": "Text",
|
||||
"tableOptions": "A"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozTablePanel"
|
||||
spec: close({
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
thresholds?: [...#TableThreshold]
|
||||
})
|
||||
|
||||
#Visualization: {
|
||||
timePreference?: common.#TimePreference
|
||||
}
|
||||
|
||||
#Formatting: {
|
||||
columnUnits?: [string]: string
|
||||
decimalPrecision?: common.#PrecisionOption
|
||||
}
|
||||
|
||||
#TableThreshold: {
|
||||
common.#ComparisonThreshold
|
||||
tableOptions: string
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"kind": "SigNozTimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"timePreference": "globalTime",
|
||||
"fillSpans": true
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "By",
|
||||
"decimalPrecision": 3
|
||||
},
|
||||
"axes": {
|
||||
"softMin": 0,
|
||||
"softMax": 800,
|
||||
"isLogScale": true
|
||||
},
|
||||
"legend": {
|
||||
"position": "right",
|
||||
"customColors": {
|
||||
"{service.name=\"sampleapp-gateway\"}": "#9ea5f7"
|
||||
}
|
||||
},
|
||||
"contextLinks": [
|
||||
{
|
||||
"label": "View service details",
|
||||
"url": "http://localhost:8080/{{_service.name}}?dfddf=%7B%7Blimit%7D%7D"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"value": 1024,
|
||||
"unit": "By",
|
||||
"color": "Red",
|
||||
"format": "Text",
|
||||
"label": "upper limit"
|
||||
},
|
||||
{
|
||||
"value": 100,
|
||||
"unit": "By",
|
||||
"color": "Orange",
|
||||
"format": "Text",
|
||||
"label": "kinda bad"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
kind: "SigNozTimeSeriesPanel"
|
||||
spec: close({
|
||||
visualization?: #Visualization
|
||||
formatting?: #Formatting
|
||||
axes?: common.#Axes
|
||||
legend?: #Legend
|
||||
contextLinks?: [...common.#ContextLinkProps]
|
||||
thresholds?: [...common.#ThresholdWithLabel]
|
||||
})
|
||||
|
||||
#Visualization: {
|
||||
timePreference?: common.#TimePreference
|
||||
fillSpans?: bool | *false
|
||||
}
|
||||
|
||||
#Formatting: {
|
||||
unit?: string | *""
|
||||
decimalPrecision?: common.#PrecisionOption
|
||||
}
|
||||
|
||||
#Legend: {
|
||||
position?: common.#LegendPosition
|
||||
customColors?: [string]: string
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import "github.com/signoz/common"
|
||||
|
||||
// Source: pkg/types/querybuildertypes/querybuildertypesv5/trace_operator.go — QueryBuilderTraceOperator
|
||||
// SigNozTraceOperator composes multiple trace BuilderQueries using
|
||||
// relational operators (=>, ->, &&, ||, NOT) to query trace relationships.
|
||||
// Signal is implicitly "traces" — all referenced queries must be trace queries.
|
||||
kind: "SigNozTraceOperator"
|
||||
spec: close({
|
||||
name: common.#QueryName
|
||||
// Operator expression composing trace queries, e.g. "A => B && C".
|
||||
expression: string & !=""
|
||||
disabled?: bool | *false
|
||||
|
||||
// Which query's spans to return (must be a query referenced in expression).
|
||||
returnSpansFrom?: common.#QueryName
|
||||
|
||||
aggregations?: [...common.#ExpressionAggregation]
|
||||
filter?: common.#FilterExpression
|
||||
groupBy?: [...common.#GroupByItem]
|
||||
order?: [...common.#OrderByItem]
|
||||
limit?: common.#Limit
|
||||
offset?: common.#Offset
|
||||
cursor?: string
|
||||
functions?: [...common.#Function]
|
||||
stepInterval?: number
|
||||
having?: common.#HavingExpression
|
||||
legend?: string
|
||||
selectFields?: [...common.#TelemetryFieldKey]
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"kind": "SigNozTraceOperator",
|
||||
"spec": {
|
||||
"name": "T1",
|
||||
"expression": "A => B",
|
||||
"returnSpansFrom": "A",
|
||||
"aggregations": [
|
||||
{
|
||||
"expression": "count()",
|
||||
"alias": "request_count"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": "service.name = 'frontend'"
|
||||
},
|
||||
"groupBy": [],
|
||||
"order": []
|
||||
}
|
||||
}
|
||||
2
perses/validate.sh
Executable file
2
perses/validate.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
percli lint -f ./examples/perses.json --plugin.path ./schemas-wrapper
|
||||
rm ./schemas-wrapper/plugin-modules.json
|
||||
@@ -67,7 +67,7 @@ func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, un
|
||||
spansFromRootToNode := []string{}
|
||||
|
||||
if node.SpanID == selectedSpanId {
|
||||
if isSelectedSpanIDUnCollapsed && !slices.Contains(uncollapsedSpans, node.SpanID) {
|
||||
if isSelectedSpanIDUnCollapsed {
|
||||
spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
|
||||
}
|
||||
return true, spansFromRootToNode
|
||||
@@ -88,15 +88,7 @@ func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, un
|
||||
return isPresentInSubtreeForTheNode, spansFromRootToNode
|
||||
}
|
||||
|
||||
// traverseOpts holds the traversal configuration that remains constant
|
||||
// throughout the recursion. Per-call state (level, isPartOfPreOrder, etc.)
|
||||
// is passed as direct arguments.
|
||||
type traverseOpts struct {
|
||||
uncollapsedSpans []string
|
||||
selectedSpanID string
|
||||
}
|
||||
|
||||
func traverseTrace(span *model.Span, opts traverseOpts, level uint64, isPartOfPreOrder bool, hasSibling bool) []*model.Span {
|
||||
func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string) []*model.Span {
|
||||
preOrderTraversal := []*model.Span{}
|
||||
|
||||
// sort the children to maintain the order across requests
|
||||
@@ -134,9 +126,8 @@ func traverseTrace(span *model.Span, opts traverseOpts, level uint64, isPartOfPr
|
||||
preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren)
|
||||
}
|
||||
|
||||
isAlreadyUncollapsed := slices.Contains(opts.uncollapsedSpans, span.SpanID)
|
||||
for index, child := range span.Children {
|
||||
_childTraversal := traverseTrace(child, opts, level+1, isPartOfPreOrder && isAlreadyUncollapsed, index != (len(span.Children)-1))
|
||||
_childTraversal := traverseTrace(child, uncollapsedSpans, level+1, isPartOfPreOrder && slices.Contains(uncollapsedSpans, span.SpanID), index != (len(span.Children)-1), selectedSpanId)
|
||||
preOrderTraversal = append(preOrderTraversal, _childTraversal...)
|
||||
nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
|
||||
span.SubTreeNodeCount += child.SubTreeNodeCount + 1
|
||||
@@ -177,11 +168,7 @@ func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoo
|
||||
_, spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, selectedSpanID, updatedUncollapsedSpans, isSelectedSpanIDUnCollapsed)
|
||||
updatedUncollapsedSpans = append(updatedUncollapsedSpans, spansFromRootToNode...)
|
||||
|
||||
opts := traverseOpts{
|
||||
uncollapsedSpans: updatedUncollapsedSpans,
|
||||
selectedSpanID: selectedSpanID,
|
||||
}
|
||||
_preOrderTraversal := traverseTrace(rootNode, opts, 0, true, false)
|
||||
_preOrderTraversal := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID)
|
||||
_selectedSpanIndex := findIndexForSelectedSpanFromPreOrder(_preOrderTraversal, selectedSpanID)
|
||||
|
||||
if _selectedSpanIndex != -1 {
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
// Package tracedetail tests — waterfall
|
||||
//
|
||||
// # Background
|
||||
//
|
||||
// The waterfall view renders a trace as a scrollable list of spans in
|
||||
// pre-order (parent before children, siblings left-to-right). Because a trace
|
||||
// can have thousands of spans, only a window of ~500 is returned per request.
|
||||
// The window is centred on the selected span.
|
||||
//
|
||||
// # Key concepts
|
||||
//
|
||||
// uncollapsedSpans
|
||||
//
|
||||
// The set of span IDs the user has manually expanded in the UI.
|
||||
// Only the direct children of an uncollapsed span are included in the
|
||||
// output; grandchildren stay hidden until their parent is also uncollapsed.
|
||||
// When multiple spans are uncollapsed their children are all visible at once.
|
||||
//
|
||||
// selectedSpanID
|
||||
//
|
||||
// The span currently focused — set when the user clicks a span in the
|
||||
// waterfall or selects one from the flamegraph. The output window is always
|
||||
// centred on this span. The path from the trace root down to the selected
|
||||
// span is automatically uncollapsed so ancestors are visible even if they are
|
||||
// not in uncollapsedSpans.
|
||||
//
|
||||
// isSelectedSpanIDUnCollapsed
|
||||
//
|
||||
// Controls whether the selected span's own children are shown:
|
||||
// true — user expanded the span (click-to-open in waterfall or flamegraph);
|
||||
// direct children of the selected span are included.
|
||||
// false — user selected without expanding;
|
||||
// the span is visible but its children remain hidden.
|
||||
//
|
||||
// traceRoots
|
||||
//
|
||||
// Root spans of the trace — spans with no parent in the current dataset.
|
||||
// Normally one, but multiple roots are common when upstream services are
|
||||
// not instrumented or their spans were not sampled/exported.
|
||||
|
||||
package tracedetail
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Pre-order traversal is preserved: parent before children, siblings left-to-right.
|
||||
func TestGetSelectedSpans_PreOrderTraversal(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{"root", "child1"}, "root", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, []string{"root", "child1", "grandchild", "child2"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// Multiple roots: both trees are flattened into a single pre-order list with
|
||||
// root1's subtree before root2's. Service/entry-point come from the first root.
|
||||
//
|
||||
// root1 svc-a ← selected
|
||||
// └─ child1
|
||||
// root2 svc-b
|
||||
// └─ child2
|
||||
//
|
||||
// Expected output order: root1 → child1 → root2 → child2
|
||||
func TestGetSelectedSpans_MultipleRoots(t *testing.T) {
|
||||
root1 := mkSpan("root1", "svc-a", mkSpan("child1", "svc-a"))
|
||||
root2 := mkSpan("root2", "svc-b", mkSpan("child2", "svc-b"))
|
||||
spanMap := buildSpanMap(root1, root2)
|
||||
|
||||
spans, _, svcName, entryPoint := GetSelectedSpans([]string{"root1", "root2"}, "root1", []*model.Span{root1, root2}, spanMap, false)
|
||||
|
||||
assert.Equal(t, []string{"root1", "child1", "root2", "child2"}, spanIDs(spans), "root1 subtree must precede root2 subtree")
|
||||
assert.Equal(t, "svc-a", svcName, "metadata comes from first root")
|
||||
assert.Equal(t, "root1-op", entryPoint, "metadata comes from first root")
|
||||
}
|
||||
|
||||
// isSelectedSpanIDUnCollapsed=true opens only the selected span's direct children,
|
||||
// not deeper descendants.
|
||||
//
|
||||
// root → selected (expanded)
|
||||
// ├─ child1 ✓
|
||||
// │ └─ grandchild ✗ (only one level opened)
|
||||
// └─ child2 ✓
|
||||
func TestGetSelectedSpans_ExpandedSelectedSpan(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("selected", "svc",
|
||||
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
|
||||
mkSpan("child2", "svc"),
|
||||
),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{}, "selected", []*model.Span{root}, spanMap, true)
|
||||
|
||||
// root and selected are on the auto-uncollapsed path; child1/child2 are direct
|
||||
// children of the expanded selected span; grandchild stays hidden.
|
||||
assert.Equal(t, []string{"root", "selected", "child1", "child2"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// Multiple spans uncollapsed simultaneously: children of all uncollapsed spans
|
||||
// are visible at once.
|
||||
//
|
||||
// root
|
||||
// ├─ childA (uncollapsed) → grandchildA ✓
|
||||
// └─ childB (uncollapsed) → grandchildB ✓
|
||||
func TestGetSelectedSpans_MultipleUncollapsed(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc", mkSpan("grandchildA", "svc")),
|
||||
mkSpan("childB", "svc", mkSpan("grandchildB", "svc")),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{"root", "childA", "childB"}, "root", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, []string{"root", "childA", "grandchildA", "childB", "grandchildB"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// The selected span is always present in the output window.
|
||||
func TestGetSelectedSpans_SelectedSpanAlwaysInWindow(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("A", "svc",
|
||||
mkSpan("B", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{}, "selected", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, []string{"root", "A", "B", "selected"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// Collapsing a span with other uncollapsed spans
|
||||
//
|
||||
// root
|
||||
// ├─ childA (previously expanded — in uncollapsedSpans)
|
||||
// │ ├─ grandchild1 ✓
|
||||
// │ │ └─ greatGrandchild ✗ (grandchild1 not in uncollapsedSpans)
|
||||
// │ └─ grandchild2 ✓
|
||||
// └─ childB ← selected (not expanded)
|
||||
func TestGetSelectedSpans_ManualUncollapse(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("childA", "svc",
|
||||
mkSpan("grandchild1", "svc", mkSpan("greatGrandchild", "svc")),
|
||||
mkSpan("grandchild2", "svc"),
|
||||
),
|
||||
mkSpan("childB", "svc"),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
// childA was expanded in a previous interaction; childB is now selected without expanding
|
||||
spans, _, _, _ := GetSelectedSpans([]string{"childA"}, "childB", []*model.Span{root}, spanMap, false)
|
||||
|
||||
// path to childB auto-uncollpases root → childA and childB appear; childA is in
|
||||
// uncollapsedSpans so its children appear; greatGrandchild stays hidden.
|
||||
assert.Equal(t, []string{"root", "childA", "grandchild1", "grandchild2", "childB"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// A collapsed span hides all children.
|
||||
func TestGetSelectedSpans_CollapsedSpan(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc"),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{}, "root", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, []string{"root"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// Selecting a span auto-uncollpases the path from root to that span so it is visible.
|
||||
//
|
||||
// root → parent → selected
|
||||
func TestGetSelectedSpans_PathToSelectedIsUncollapsed(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
// no manually uncollapsed spans — path should still be opened
|
||||
spans, _, _, _ := GetSelectedSpans([]string{}, "selected", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, []string{"root", "parent", "selected"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// The path-to-selected spans are returned in updatedUncollapsedSpans.
|
||||
func TestGetSelectedSpans_PathReturnedInUncollapsed(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, uncollapsed, _, _ := GetSelectedSpans([]string{}, "selected", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, []string{"root", "parent"}, uncollapsed)
|
||||
assert.Equal(t, []string{"root", "parent", "selected"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// Siblings of ancestors are rendered as collapsed nodes but their subtrees
|
||||
// must NOT be expanded.
|
||||
//
|
||||
// root
|
||||
// ├─ unrelated → unrelated-child (✗)
|
||||
// └─ parent → selected
|
||||
func TestGetSelectedSpans_SiblingsNotExpanded(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
|
||||
mkSpan("parent", "svc",
|
||||
mkSpan("selected", "svc"),
|
||||
),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, uncollapsed, _, _ := GetSelectedSpans([]string{}, "selected", []*model.Span{root}, spanMap, false)
|
||||
|
||||
// children of root sort alphabetically: parent < unrelated; unrelated-child stays hidden
|
||||
assert.Equal(t, []string{"root", "parent", "selected", "unrelated"}, spanIDs(spans))
|
||||
// only the path nodes are tracked as uncollapsed — unrelated is not
|
||||
assert.Equal(t, []string{"root", "parent"}, uncollapsed)
|
||||
}
|
||||
|
||||
// An unknown selectedSpanID must not panic; returns a window from index 0.
|
||||
func TestGetSelectedSpans_UnknownSelectedSpan(t *testing.T) {
|
||||
root := mkSpan("root", "svc", mkSpan("child", "svc"))
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{}, "nonexistent", []*model.Span{root}, spanMap, false)
|
||||
assert.Equal(t, []string{"root"}, spanIDs(spans))
|
||||
}
|
||||
|
||||
// Test to check if Level, HasChildren, HasSiblings, and SubTreeNodeCount are populated correctly.
|
||||
//
|
||||
// root level=0, hasChildren=true, hasSiblings=false, subTree=4
|
||||
// child1 level=1, hasChildren=true, hasSiblings=true, subTree=2
|
||||
// grandchild level=2, hasChildren=false, hasSiblings=false, subTree=1
|
||||
// child2 level=1, hasChildren=false, hasSiblings=false, subTree=1
|
||||
func TestGetSelectedSpans_SpanMetadata(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
|
||||
mkSpan("child2", "svc"),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
spans, _, _, _ := GetSelectedSpans([]string{"root", "child1"}, "root", []*model.Span{root}, spanMap, false)
|
||||
|
||||
byID := map[string]*model.Span{}
|
||||
for _, s := range spans {
|
||||
byID[s.SpanID] = s
|
||||
}
|
||||
|
||||
assert.Equal(t, uint64(0), byID["root"].Level)
|
||||
assert.Equal(t, uint64(1), byID["child1"].Level)
|
||||
assert.Equal(t, uint64(1), byID["child2"].Level)
|
||||
assert.Equal(t, uint64(2), byID["grandchild"].Level)
|
||||
|
||||
assert.True(t, byID["root"].HasChildren)
|
||||
assert.True(t, byID["child1"].HasChildren)
|
||||
assert.False(t, byID["child2"].HasChildren)
|
||||
assert.False(t, byID["grandchild"].HasChildren)
|
||||
|
||||
assert.False(t, byID["root"].HasSiblings, "root has no siblings")
|
||||
assert.True(t, byID["child1"].HasSiblings, "child1 has sibling child2")
|
||||
assert.False(t, byID["child2"].HasSiblings, "child2 is the last child")
|
||||
assert.False(t, byID["grandchild"].HasSiblings, "grandchild has no siblings")
|
||||
|
||||
assert.Equal(t, uint64(4), byID["root"].SubTreeNodeCount)
|
||||
assert.Equal(t, uint64(2), byID["child1"].SubTreeNodeCount)
|
||||
assert.Equal(t, uint64(1), byID["grandchild"].SubTreeNodeCount)
|
||||
assert.Equal(t, uint64(1), byID["child2"].SubTreeNodeCount)
|
||||
}
|
||||
|
||||
// If the selected span is already in uncollapsedSpans AND isSelectedSpanIDUnCollapsed=true,
|
||||
func TestGetSelectedSpans_DuplicateInUncollapsed(t *testing.T) {
|
||||
root := mkSpan("root", "svc",
|
||||
mkSpan("selected", "svc", mkSpan("child", "svc")),
|
||||
)
|
||||
spanMap := buildSpanMap(root)
|
||||
_, uncollapsed, _, _ := GetSelectedSpans(
|
||||
[]string{"selected"}, // already present
|
||||
"selected",
|
||||
[]*model.Span{root}, spanMap,
|
||||
true,
|
||||
)
|
||||
|
||||
count := 0
|
||||
for _, id := range uncollapsed {
|
||||
if id == "selected" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, count, "should appear once")
|
||||
}
|
||||
|
||||
// makeChain builds a linear trace: span0 → span1 → … → span(n-1).
|
||||
// All span IDs are "span0", "span1", … so the caller can reference them by index.
|
||||
func makeChain(n int) (*model.Span, map[string]*model.Span, []string) {
|
||||
spans := make([]*model.Span, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if i == n-1 {
|
||||
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc")
|
||||
} else {
|
||||
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc", spans[i+1])
|
||||
}
|
||||
}
|
||||
uncollapsed := make([]string, n)
|
||||
for i := range spans {
|
||||
uncollapsed[i] = fmt.Sprintf("span%d", i)
|
||||
}
|
||||
return spans[0], buildSpanMap(spans[0]), uncollapsed
|
||||
}
|
||||
|
||||
// The selected span is centred: 200 spans before it, 300 after (0.4 / 0.6 split).
|
||||
func TestGetSelectedSpans_WindowCentredOnSelected(t *testing.T) {
|
||||
root, spanMap, uncollapsed := makeChain(600)
|
||||
spans, _, _, _ := GetSelectedSpans(uncollapsed, "span300", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, 500, len(spans), "window should be 500 spans")
|
||||
// window is [100, 600): span300 lands at position 200 (300 - 100)
|
||||
assert.Equal(t, "span100", spans[0].SpanID, "window starts 200 before selected")
|
||||
assert.Equal(t, "span300", spans[200].SpanID, "selected span at position 200 in window")
|
||||
assert.Equal(t, "span599", spans[499].SpanID, "window ends 300 after selected")
|
||||
}
|
||||
|
||||
// When the selected span is near the start, the window shifts right so no
|
||||
// negative index is used — the result is still 500 spans.
|
||||
func TestGetSelectedSpans_WindowShiftsAtStart(t *testing.T) {
|
||||
root, spanMap, uncollapsed := makeChain(600)
|
||||
spans, _, _, _ := GetSelectedSpans(uncollapsed, "span10", []*model.Span{root}, spanMap, false)
|
||||
|
||||
assert.Equal(t, 500, len(spans))
|
||||
assert.Equal(t, "span0", spans[0].SpanID, "window clamped to start of trace")
|
||||
assert.Equal(t, "span10", spans[10].SpanID, "selected span still in window")
|
||||
}
|
||||
|
||||
func mkSpan(id, service string, children ...*model.Span) *model.Span {
|
||||
return &model.Span{
|
||||
SpanID: id,
|
||||
ServiceName: service,
|
||||
Name: id + "-op",
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
// spanIDs returns SpanIDs in order.
|
||||
func spanIDs(spans []*model.Span) []string {
|
||||
ids := make([]string, len(spans))
|
||||
for i, s := range spans {
|
||||
ids[i] = s.SpanID
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// buildSpanMap indexes every span in a set of trees by SpanID.
|
||||
func buildSpanMap(roots ...*model.Span) map[string]*model.Span {
|
||||
m := map[string]*model.Span{}
|
||||
var walk func(*model.Span)
|
||||
walk = func(s *model.Span) {
|
||||
m[s.SpanID] = s
|
||||
for _, c := range s.Children {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
for _, r := range roots {
|
||||
walk(r)
|
||||
}
|
||||
return m
|
||||
}
|
||||
Reference in New Issue
Block a user