Skip to content

Commit 8ad9766

Browse files
Defmanaangelisc
andauthored
fix(trace): show status of span in trace panel (#950)
Co-authored-by: Andreas Christou <[email protected]>
1 parent ad0aed4 commit 8ad9766

File tree

7 files changed

+56
-39
lines changed

7 files changed

+56
-39
lines changed

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,16 @@ If using the [Open Telemetry Collector and ClickHouse exporter](https://github.c
166166

167167
```sql
168168
SELECT
169-
TraceId AS traceID,
170-
SpanId AS spanID,
171-
SpanName AS operationName,
172-
ParentSpanId AS parentSpanID,
173-
ServiceName AS serviceName,
174-
Duration / 1000000 AS duration,
175-
Timestamp AS startTime,
176-
arrayMap(key -> map('key', key, 'value', SpanAttributes[key]), mapKeys(SpanAttributes)) AS tags,
177-
arrayMap(key -> map('key', key, 'value', ResourceAttributes[key]), mapKeys(ResourceAttributes)) AS serviceTags
169+
TraceId AS traceID,
170+
SpanId AS spanID,
171+
SpanName AS operationName,
172+
ParentSpanId AS parentSpanID,
173+
ServiceName AS serviceName,
174+
Duration / 1000000 AS duration,
175+
Timestamp AS startTime,
176+
arrayMap(key -> map('key', key, 'value', SpanAttributes[key]), mapKeys(SpanAttributes)) AS tags,
177+
arrayMap(key -> map('key', key, 'value', ResourceAttributes[key]), mapKeys(ResourceAttributes)) AS serviceTags,
178+
if(StatusCode IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) AS statusCode
178179
FROM otel.otel_traces
179180
WHERE TraceId = '61d489320c01243966700e172ab37081'
180181
ORDER BY startTime ASC

src/dashboards/opentelemetry-clickhouse.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@
458458
},
459459
"pluginVersion": "4.0.6",
460460
"queryType": "timeseries",
461-
"rawSql": "SELECT\r\n $__timeInterval(Timestamp) as time,\r\n count(*) as ` `,\r\n ServiceName\r\nFROM otel_traces\r\nWHERE\r\n $__conditionalAll(TraceId IN (${trace_id:singlequote}), $trace_id)\r\n AND $__timeFilter(Timestamp)\r\n AND ServiceName IN (${serviceName:singlequote})\r\n AND StatusCode = 'STATUS_CODE_ERROR'\r\n AND ServiceName != 'loadgenerator' GROUP BY ServiceName, time\r\nORDER BY time ASC\r\nLIMIT 100000",
461+
"rawSql": "SELECT\r\n $__timeInterval(Timestamp) as time,\r\n count(*) as ` `,\r\n ServiceName\r\nFROM otel_traces\r\nWHERE\r\n $__conditionalAll(TraceId IN (${trace_id:singlequote}), $trace_id)\r\n AND $__timeFilter(Timestamp)\r\n AND ServiceName IN (${serviceName:singlequote})\r\n AND StatusCode IN ('Error', 'STATUS_CODE_ERROR')\r\n AND ServiceName != 'loadgenerator' GROUP BY ServiceName, time\r\nORDER BY time ASC\r\nLIMIT 100000",
462462
"refId": "A"
463463
}
464464
],
@@ -728,6 +728,10 @@
728728
{
729729
"hint": "trace_service_tags",
730730
"name": "ResourceAttributes"
731+
},
732+
{
733+
"hint": "trace_status_code",
734+
"name": "StatusCode"
731735
}
732736
],
733737
"database": "default",
@@ -977,4 +981,4 @@
977981
"uid": "8klBUGfVk",
978982
"version": 2,
979983
"weekStart": ""
980-
}
984+
}

src/data/sqlGenerator.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ describe('SQL Generator', () => {
196196
{ name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },
197197
{ name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },
198198
{ name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },
199+
{ name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },
199200
],
200201
filters: [],
201202
meta: {
@@ -215,7 +216,8 @@ describe('SQL Generator', () => {
215216
'multiply("Duration", 0.000001) as duration,',
216217
`arrayMap(key -> map('key', key, 'value',"SpanAttributes"[key]),`,
217218
`mapKeys("SpanAttributes")) as tags,`,
218-
`arrayMap(key -> map('key', key, 'value',"ResourceAttributes"[key]), mapKeys("ResourceAttributes")) as serviceTags`,
219+
`arrayMap(key -> map('key', key, 'value',"ResourceAttributes"[key]), mapKeys("ResourceAttributes")) as serviceTags,`,
220+
`if("StatusCode" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`,
219221
`FROM "default"."otel_traces" WHERE traceID = 'abcdefg'`,
220222
'LIMIT 1000'
221223
];
@@ -239,6 +241,7 @@ describe('SQL Generator', () => {
239241
{ name: 'Duration', type: 'Int64', hint: ColumnHint.TraceDurationTime },
240242
{ name: 'SpanAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceTags },
241243
{ name: 'ResourceAttributes', type: 'Map(LowCardinality(String), String)', hint: ColumnHint.TraceServiceTags },
244+
{ name: 'StatusCode', type: 'LowCardinality(String)', hint: ColumnHint.TraceStatusCode },
242245
],
243246
filters: [],
244247
meta: {
@@ -260,7 +263,8 @@ describe('SQL Generator', () => {
260263
'multiply("Duration", 0.000001) as duration,',
261264
`arrayMap(key -> map('key', key, 'value',"SpanAttributes"[key]),`,
262265
`mapKeys("SpanAttributes")) as tags,`,
263-
`arrayMap(key -> map('key', key, 'value',"ResourceAttributes"[key]), mapKeys("ResourceAttributes")) as serviceTags`,
266+
`arrayMap(key -> map('key', key, 'value',"ResourceAttributes"[key]), mapKeys("ResourceAttributes")) as serviceTags,`,
267+
`if("StatusCode" IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`,
264268
`FROM "default"."otel_traces" WHERE traceID = trace_id AND "Timestamp" >= trace_start AND "Timestamp" <= trace_end`,
265269
'LIMIT 1000'
266270
];

src/data/sqlGenerator.ts

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const generateSql = (options: QueryBuilderOptions): string => {
2828
*/
2929
const generateTraceSearchQuery = (options: QueryBuilderOptions): string => {
3030
const { database, table } = options;
31-
31+
3232
const queryParts: string[] = [];
3333

3434
// TODO: these columns could be a map or some other convenience function
@@ -37,28 +37,28 @@ const generateTraceSearchQuery = (options: QueryBuilderOptions): string => {
3737
if (traceId !== undefined) {
3838
selectParts.push(`${escapeIdentifier(traceId.name)} as traceID`);
3939
}
40-
40+
4141
const traceServiceName = getColumnByHint(options, ColumnHint.TraceServiceName);
4242
if (traceServiceName !== undefined) {
4343
selectParts.push(`${escapeIdentifier(traceServiceName.name)} as serviceName`);
4444
}
45-
45+
4646
const traceOperationName = getColumnByHint(options, ColumnHint.TraceOperationName);
4747
if (traceOperationName !== undefined) {
4848
selectParts.push(`${escapeIdentifier(traceOperationName.name)} as operationName`);
4949
}
50-
50+
5151
const traceStartTime = getColumnByHint(options, ColumnHint.Time);
5252
if (traceStartTime !== undefined) {
5353
selectParts.push(`${escapeIdentifier(traceStartTime.name)} as startTime`);
5454
}
55-
55+
5656
const traceDurationTime = getColumnByHint(options, ColumnHint.TraceDurationTime);
5757
if (traceDurationTime !== undefined) {
5858
const timeUnit = options.meta?.traceDurationUnit;
5959
selectParts.push(getTraceDurationSelectSql(escapeIdentifier(traceDurationTime.name), timeUnit));
6060
}
61-
61+
6262
const selectPartsSql = selectParts.join(', ');
6363

6464
queryParts.push('SELECT');
@@ -93,7 +93,7 @@ const generateTraceSearchQuery = (options: QueryBuilderOptions): string => {
9393
*/
9494
const generateTraceIdQuery = (options: QueryBuilderOptions): string => {
9595
const { database, table } = options;
96-
96+
9797
const queryParts: string[] = [];
9898

9999
// TODO: these columns could be a map or some other convenience function
@@ -102,48 +102,53 @@ const generateTraceIdQuery = (options: QueryBuilderOptions): string => {
102102
if (traceId !== undefined) {
103103
selectParts.push(`${escapeIdentifier(traceId.name)} as traceID`);
104104
}
105-
105+
106106
const traceSpanId = getColumnByHint(options, ColumnHint.TraceSpanId);
107107
if (traceSpanId !== undefined) {
108108
selectParts.push(`${escapeIdentifier(traceSpanId.name)} as spanID`);
109109
}
110-
110+
111111
const traceParentSpanId = getColumnByHint(options, ColumnHint.TraceParentSpanId);
112112
if (traceParentSpanId !== undefined) {
113113
selectParts.push(`${escapeIdentifier(traceParentSpanId.name)} as parentSpanID`);
114114
}
115-
115+
116116
const traceServiceName = getColumnByHint(options, ColumnHint.TraceServiceName);
117117
if (traceServiceName !== undefined) {
118118
selectParts.push(`${escapeIdentifier(traceServiceName.name)} as serviceName`);
119119
}
120-
120+
121121
const traceOperationName = getColumnByHint(options, ColumnHint.TraceOperationName);
122122
if (traceOperationName !== undefined) {
123123
selectParts.push(`${escapeIdentifier(traceOperationName.name)} as operationName`);
124124
}
125-
125+
126126
const traceStartTime = getColumnByHint(options, ColumnHint.Time);
127127
if (traceStartTime !== undefined) {
128128
selectParts.push(`${convertTimeFieldToMilliseconds(escapeIdentifier(traceStartTime.name))} as startTime`);
129129
}
130-
130+
131131
const traceDurationTime = getColumnByHint(options, ColumnHint.TraceDurationTime);
132132
if (traceDurationTime !== undefined) {
133133
const timeUnit = options.meta?.traceDurationUnit;
134134
selectParts.push(getTraceDurationSelectSql(escapeIdentifier(traceDurationTime.name), timeUnit));
135135
}
136-
136+
137137
// TODO: for tags and serviceTags, consider the column type. They might not require mapping, they could already be JSON.
138138
const traceTags = getColumnByHint(options, ColumnHint.TraceTags);
139139
if (traceTags !== undefined) {
140140
selectParts.push(`arrayMap(key -> map('key', key, 'value',${escapeIdentifier(traceTags.name)}[key]), mapKeys(${escapeIdentifier(traceTags.name)})) as tags`);
141141
}
142-
142+
143143
const traceServiceTags = getColumnByHint(options, ColumnHint.TraceServiceTags);
144144
if (traceServiceTags !== undefined) {
145145
selectParts.push(`arrayMap(key -> map('key', key, 'value',${escapeIdentifier(traceServiceTags.name)}[key]), mapKeys(${escapeIdentifier(traceServiceTags.name)})) as serviceTags`);
146146
}
147+
148+
const traceStatusCode = getColumnByHint(options, ColumnHint.TraceStatusCode);
149+
if (traceStatusCode !== undefined) {
150+
selectParts.push(`if(${escapeIdentifier(traceStatusCode.name)} IN ('Error', 'STATUS_CODE_ERROR'), 2, 0) as statusCode`);
151+
}
147152
const selectPartsSql = selectParts.join(', ');
148153

149154
// Optimize trace ID filtering for OTel enabled trace lookups
@@ -207,14 +212,14 @@ const generateTraceIdQuery = (options: QueryBuilderOptions): string => {
207212
* Generates logs query with columns that fit Grafana's Logs panel
208213
* Column aliases follow this structure:
209214
* https://grafana.com/developers/plugin-tools/tutorials/build-a-logs-data-source-plugin#logs-data-frame-format
210-
*
215+
*
211216
* note: column order seems to matter as well as alias name
212217
*/
213218
const generateLogsQuery = (_options: QueryBuilderOptions): string => {
214219
// Copy columns so column aliases can be safely mutated
215220
const options = { ..._options, columns: _options.columns?.map(c => ({ ...c })) };
216221
const { database, table } = options;
217-
222+
218223
const queryParts: string[] = [];
219224

220225
// TODO: these columns could be a map or some other convenience function
@@ -263,7 +268,7 @@ const generateLogsQuery = (_options: QueryBuilderOptions): string => {
263268
queryParts.push('FROM');
264269
queryParts.push(getTableIdentifier(database, table));
265270

266-
271+
267272
const filterParts = getFilters(options);
268273
const hasLogMessageFilter = logMessage && options.meta?.logMessageLike;
269274

@@ -304,7 +309,7 @@ const generateSimpleTimeSeriesQuery = (_options: QueryBuilderOptions): string =>
304309
// Copy columns so column aliases can be safely mutated
305310
const options = { ..._options, columns: _options.columns?.map(c => ({ ...c })) };
306311
const { database, table } = options;
307-
312+
308313
const queryParts: string[] = [];
309314

310315
const selectParts: string[] = [];
@@ -341,7 +346,7 @@ const generateSimpleTimeSeriesQuery = (_options: QueryBuilderOptions): string =>
341346

342347
// (v3) aggregate selections go AFTER group by
343348
aggregateSelectParts.forEach(a => selectParts.push(a));
344-
349+
345350
const selectPartsSql = selectParts.join(', ');
346351

347352
queryParts.push('SELECT');
@@ -354,7 +359,7 @@ const generateSimpleTimeSeriesQuery = (_options: QueryBuilderOptions): string =>
354359
queryParts.push('WHERE');
355360
queryParts.push(filterParts);
356361
}
357-
362+
358363
const hasAggregates = (options.aggregates?.length || 0 > 0);
359364
const hasGroupBy = (options.groupBy?.length || 0 > 0);
360365
if (hasAggregates || hasGroupBy) {
@@ -389,7 +394,7 @@ const generateAggregateTimeSeriesQuery = (_options: QueryBuilderOptions): string
389394
// Copy columns so column aliases can be safely mutated
390395
const options = { ..._options, columns: _options.columns?.map(c => ({ ...c })) };
391396
const { database, table } = options;
392-
397+
393398
const queryParts: string[] = [];
394399
const selectParts: string[] = [];
395400

@@ -407,7 +412,7 @@ const generateAggregateTimeSeriesQuery = (_options: QueryBuilderOptions): string
407412
const name = `${agg.aggregateType}(${agg.column})`;
408413
selectParts.push(`${name}${alias}`);
409414
});
410-
415+
411416
const selectPartsSql = selectParts.join(', ');
412417

413418
queryParts.push('SELECT');
@@ -477,7 +482,7 @@ const generateTableQuery = (options: QueryBuilderOptions): string => {
477482
// selectParts.push(g)
478483
});
479484
}
480-
485+
481486
const selectPartsSql = selectParts.join(', ');
482487

483488
queryParts.push('SELECT');
@@ -528,7 +533,7 @@ export const getColumnsByHints = (options: QueryBuilderOptions, hints: readonly
528533

529534
const getColumnIdentifier = (col: SelectedColumn): string => {
530535
let colName = col.name;
531-
536+
532537
// allow for functions like count()
533538
if (colName.includes('(') || colName.includes(')') || colName.includes('"') || colName.includes('"') || colName.includes(' as ')) {
534539
colName = col.name
@@ -690,7 +695,7 @@ const getFilters = (options: QueryBuilderOptions): string => {
690695
if (operator) {
691696
filterParts.push(operator);
692697
}
693-
698+
694699
if (isNullFilter(filter.operator)) {
695700
// empty
696701
} else if (filter.operator === FilterOperator.IsEmpty) {

src/labels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ export default {
422422
[ColumnHint.TraceDurationTime]: 'Duration Time',
423423
[ColumnHint.TraceTags]: 'Tags',
424424
[ColumnHint.TraceServiceTags]: 'Service Tags',
425+
[ColumnHint.TraceStatusCode]: 'Status Code',
425426
}
426427
}
427428
}

src/otel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const otel129: OtelVersion = {
4848
[ColumnHint.TraceDurationTime, 'Duration'],
4949
[ColumnHint.TraceTags, 'SpanAttributes'],
5050
[ColumnHint.TraceServiceTags, 'ResourceAttributes'],
51+
[ColumnHint.TraceStatusCode, 'StatusCode'],
5152
]),
5253
traceDurationUnit: TimeUnit.Nanoseconds,
5354
};

src/types/queryBuilder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export enum ColumnHint {
132132
TraceDurationTime = 'trace_duration_time',
133133
TraceTags = 'trace_tags',
134134
TraceServiceTags = 'trace_service_tags',
135+
TraceStatusCode = 'trace_status_code',
135136
}
136137

137138
/**

0 commit comments

Comments
 (0)