Skip to content

Commit 6d6231c

Browse files
authored
feat: full trace visualization support (#8)
- Add Variant/Array/Map/Tuple/JSON converters to output data.FieldTypeJSON - Transform trace attribute JSON objects to [{key,value}] format for Grafana - Convert startTime to epoch milliseconds via TO_UNIX_TIMESTAMP() * 1000 - Add tags, serviceTags, statusCode, statusMessage, kind fields to trace query - Add config settings and UI for new trace columns - Add TraceTags, TraceServiceTags, TraceStatusCode, TraceStatusMessage, TraceKind hints
1 parent cc5433f commit 6d6231c

8 files changed

Lines changed: 207 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "datasource-databend",
3-
"version": "1.3.2",
3+
"version": "1.4.0",
44
"description": "Grafana datasource plugin for Databend",
55
"scripts": {
66
"build": "webpack -c ./.config/webpack/webpack.config.ts --env production",

pkg/converters/converters.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package converters
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"math/big"
67
"reflect"
@@ -25,6 +26,8 @@ var matchRegexes = map[string]*regexp.Regexp{
2526
"Nullable(Date)": regexp.MustCompile(`^Nullable\(Date\(?`),
2627
"Nullable(Decimal)": regexp.MustCompile(`^Nullable\(Decimal`),
2728
"Nullable(String)": regexp.MustCompile(`Nullable\(String`),
29+
"Variant": regexp.MustCompile(`^(Variant|Array|Map|Tuple|JSON)`),
30+
"Nullable(Variant)": regexp.MustCompile(`^Nullable\((Variant|Array|Map|Tuple|JSON)`),
2831
}
2932

3033
var Converters = map[string]Converter{
@@ -185,6 +188,18 @@ var Converters = map[string]Converter{
185188
matchRegex: matchRegexes["Nullable(Decimal)"],
186189
scanType: reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(decimal.Decimal{}))),
187190
},
191+
"Variant": {
192+
convert: variantConvert,
193+
fieldType: data.FieldTypeJSON,
194+
matchRegex: matchRegexes["Variant"],
195+
scanType: reflect.PointerTo(reflect.TypeOf("")),
196+
},
197+
"Nullable(Variant)": {
198+
convert: variantNullableConvert,
199+
fieldType: data.FieldTypeNullableJSON,
200+
matchRegex: matchRegexes["Nullable(Variant)"],
201+
scanType: reflect.PointerTo(reflect.PointerTo(reflect.TypeOf(""))),
202+
},
188203
}
189204

190205
func DatabendConverters() []sqlutil.Converter {
@@ -288,3 +303,29 @@ func bigIntNullableConvert(in interface{}) (interface{}, error) {
288303
f, _ := new(big.Float).SetInt(**v).Float64()
289304
return &f, nil
290305
}
306+
307+
func variantConvert(in interface{}) (interface{}, error) {
308+
if in == nil {
309+
return json.RawMessage("null"), nil
310+
}
311+
v, ok := in.(*string)
312+
if !ok {
313+
return nil, fmt.Errorf("invalid variant - %v", in)
314+
}
315+
return json.RawMessage(*v), nil
316+
}
317+
318+
func variantNullableConvert(in interface{}) (interface{}, error) {
319+
if in == nil {
320+
return (*json.RawMessage)(nil), nil
321+
}
322+
v, ok := in.(**string)
323+
if !ok {
324+
return nil, fmt.Errorf("invalid variant - %v", in)
325+
}
326+
if *v == nil {
327+
return (*json.RawMessage)(nil), nil
328+
}
329+
raw := json.RawMessage(**v)
330+
return &raw, nil
331+
}

pkg/plugin/driver.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ func (d *Databend) MutateResponse(_ context.Context, res data.Frames) (data.Fram
7979
if frame.Meta == nil {
8080
continue
8181
}
82+
83+
if frame.Meta.PreferredVisualization == data.VisTypeTrace {
84+
transformTraceAttributes(frame)
85+
continue
86+
}
87+
8288
// For non-logs/traces/table visualizations, convert JSON fields to string
8389
if shouldConvertJSONFields(frame.Meta.PreferredVisualization) {
8490
convertJSONFieldsToString(frame)
@@ -111,3 +117,56 @@ func convertJSONFieldsToString(frame *data.Frame) {
111117
}
112118
}
113119
}
120+
121+
// transformTraceAttributes transforms JSON object fields (tags, serviceTags) in trace frames
122+
// from {"k1":"v1","k2":"v2"} to [{"key":"k1","value":"v1"},{"key":"k2","value":"v2"}]
123+
// which is the format Grafana's trace panel expects.
124+
func transformTraceAttributes(frame *data.Frame) {
125+
for i, field := range frame.Fields {
126+
if field.Type() != data.FieldTypeJSON {
127+
continue
128+
}
129+
name := field.Name
130+
if name != "tags" && name != "serviceTags" {
131+
continue
132+
}
133+
for j := 0; j < field.Len(); j++ {
134+
val := field.At(j)
135+
if val == nil {
136+
field.Set(j, json.RawMessage("[]"))
137+
continue
138+
}
139+
raw := val.(json.RawMessage)
140+
transformed := transformObjectToKeyValueArray(raw)
141+
frame.Fields[i].Set(j, transformed)
142+
}
143+
}
144+
}
145+
146+
// transformObjectToKeyValueArray converts a JSON object like {"k":"v",...}
147+
// into [{"key":"k","value":"v"},...] for Grafana's trace panel.
148+
func transformObjectToKeyValueArray(raw json.RawMessage) json.RawMessage {
149+
var obj map[string]interface{}
150+
if err := json.Unmarshal(raw, &obj); err != nil {
151+
return raw
152+
}
153+
154+
kvPairs := make([]map[string]string, 0, len(obj))
155+
for k, v := range obj {
156+
var strVal string
157+
switch val := v.(type) {
158+
case string:
159+
strVal = val
160+
default:
161+
b, _ := json.Marshal(val)
162+
strVal = string(b)
163+
}
164+
kvPairs = append(kvPairs, map[string]string{"key": k, "value": strVal})
165+
}
166+
167+
result, err := json.Marshal(kvPairs)
168+
if err != nil {
169+
return raw
170+
}
171+
return json.RawMessage(result)
172+
}

src/components/ConfigEditor.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,66 @@ export const ConfigEditor: React.FC<Props> = (props) => {
252252
placeholder="timestamp"
253253
/>
254254
</Field>
255+
<Field label="Parent Span ID Column">
256+
<Input
257+
name="tracesParentSpanIdColumn"
258+
width={40}
259+
value={jsonData.tracesParentSpanIdColumn || ''}
260+
onChange={onUpdateDatasourceJsonDataOption(props, 'tracesParentSpanIdColumn')}
261+
aria-label="Parent Span ID Column"
262+
placeholder="parent_span_id"
263+
/>
264+
</Field>
265+
<Field label="Tags Column" description="Span attributes column (Variant/JSON)">
266+
<Input
267+
name="tracesTagsColumn"
268+
width={40}
269+
value={jsonData.tracesTagsColumn || ''}
270+
onChange={onUpdateDatasourceJsonDataOption(props, 'tracesTagsColumn')}
271+
aria-label="Tags Column"
272+
placeholder="span_attributes"
273+
/>
274+
</Field>
275+
<Field label="Service Tags Column" description="Resource attributes column (Variant/JSON)">
276+
<Input
277+
name="tracesServiceTagsColumn"
278+
width={40}
279+
value={jsonData.tracesServiceTagsColumn || ''}
280+
onChange={onUpdateDatasourceJsonDataOption(props, 'tracesServiceTagsColumn')}
281+
aria-label="Service Tags Column"
282+
placeholder="resource_attributes"
283+
/>
284+
</Field>
285+
<Field label="Status Code Column">
286+
<Input
287+
name="tracesStatusCodeColumn"
288+
width={40}
289+
value={jsonData.tracesStatusCodeColumn || ''}
290+
onChange={onUpdateDatasourceJsonDataOption(props, 'tracesStatusCodeColumn')}
291+
aria-label="Status Code Column"
292+
placeholder="status_code"
293+
/>
294+
</Field>
295+
<Field label="Status Message Column">
296+
<Input
297+
name="tracesStatusMessageColumn"
298+
width={40}
299+
value={jsonData.tracesStatusMessageColumn || ''}
300+
onChange={onUpdateDatasourceJsonDataOption(props, 'tracesStatusMessageColumn')}
301+
aria-label="Status Message Column"
302+
placeholder="status_message"
303+
/>
304+
</Field>
305+
<Field label="Kind Column" description="Span kind column">
306+
<Input
307+
name="tracesKindColumn"
308+
width={40}
309+
value={jsonData.tracesKindColumn || ''}
310+
onChange={onUpdateDatasourceJsonDataOption(props, 'tracesKindColumn')}
311+
aria-label="Kind Column"
312+
placeholder="span_kind"
313+
/>
314+
</Field>
255315
</ConfigSubSection>
256316
<ConfigSubSection title="Data Links">
257317
<Field label="Show Trace Links" description="Show clickable trace visualization links on TraceId fields">

src/data/sqlGenerator.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ function generateTraceIdSql(options: QueryBuilderOptions): string {
235235
const operationCol = columns.find((c) => c.hint === ColumnHint.TraceOperationName);
236236
const durationCol = columns.find((c) => c.hint === ColumnHint.TraceDurationTime);
237237
const startTimeCol = columns.find((c) => c.hint === ColumnHint.Time);
238+
const tagsCol = columns.find((c) => c.hint === ColumnHint.TraceTags);
239+
const serviceTagsCol = columns.find((c) => c.hint === ColumnHint.TraceServiceTags);
240+
const statusCodeCol = columns.find((c) => c.hint === ColumnHint.TraceStatusCode);
241+
const statusMessageCol = columns.find((c) => c.hint === ColumnHint.TraceStatusMessage);
242+
const kindCol = columns.find((c) => c.hint === ColumnHint.TraceKind);
238243

239244
const durationUnit = (options.meta?.traceDurationUnit as string) || 'ms';
240245
const traceId = (options.meta?.traceId as string) || '';
@@ -258,11 +263,34 @@ function generateTraceIdSql(options: QueryBuilderOptions): string {
258263
selectParts.push(`${operationCol.name} AS operationName`);
259264
}
260265
if (startTimeCol) {
261-
selectParts.push(`${startTimeCol.name} AS startTime`);
266+
selectParts.push(`TO_UNIX_TIMESTAMP(${startTimeCol.name}) * 1000 AS startTime`);
262267
}
263268
if (durationCol) {
264269
selectParts.push(`${convertDuration(durationCol.name, durationUnit)} AS duration`);
265270
}
271+
if (tagsCol) {
272+
selectParts.push(`${tagsCol.name} AS tags`);
273+
} else {
274+
selectParts.push(`'[]' AS tags`);
275+
}
276+
if (serviceTagsCol) {
277+
selectParts.push(`${serviceTagsCol.name} AS serviceTags`);
278+
} else {
279+
selectParts.push(`'[]' AS serviceTags`);
280+
}
281+
if (statusCodeCol) {
282+
selectParts.push(`CASE WHEN ${statusCodeCol.name} IN ('Error', 'STATUS_CODE_ERROR') THEN 2 ELSE 0 END AS statusCode`);
283+
} else {
284+
selectParts.push(`0 AS statusCode`);
285+
}
286+
if (statusMessageCol) {
287+
selectParts.push(`${statusMessageCol.name} AS statusMessage`);
288+
} else {
289+
selectParts.push(`'' AS statusMessage`);
290+
}
291+
if (kindCol) {
292+
selectParts.push(`${kindCol.name} AS kind`);
293+
}
266294

267295
const parts: string[] = [];
268296
parts.push(`SELECT ${selectParts.join(', ')}`);

src/data/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,16 @@ export const transformQueryResponseWithTraceAndLogLinks = (
121121
columns: [
122122
{ name: traceIdColumn, hint: ColumnHint.TraceId },
123123
{ name: settings.tracesSpanIdColumn || 'span_id', hint: ColumnHint.TraceSpanId },
124+
{ name: settings.tracesParentSpanIdColumn || 'parent_span_id', hint: ColumnHint.TraceParentSpanId },
124125
{ name: settings.tracesOperationNameColumn || 'operation_name', hint: ColumnHint.TraceOperationName },
125126
{ name: settings.tracesServiceNameColumn || 'service_name', hint: ColumnHint.TraceServiceName },
126127
{ name: settings.tracesDurationColumn || 'duration', hint: ColumnHint.TraceDurationTime },
127128
{ name: settings.tracesStartTimeColumn || 'timestamp', hint: ColumnHint.Time },
129+
{ name: settings.tracesTagsColumn || 'span_attributes', hint: ColumnHint.TraceTags },
130+
{ name: settings.tracesServiceTagsColumn || 'resource_attributes', hint: ColumnHint.TraceServiceTags },
131+
{ name: settings.tracesStatusCodeColumn || 'status_code', hint: ColumnHint.TraceStatusCode },
132+
{ name: settings.tracesStatusMessageColumn || 'status_message', hint: ColumnHint.TraceStatusMessage },
133+
{ name: settings.tracesKindColumn || 'span_kind', hint: ColumnHint.TraceKind },
128134
],
129135
filters: [],
130136
orderBy: [],

src/types/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ export interface DatabendConfig extends DataSourceJsonData {
1818
tracesTable?: string;
1919
tracesTraceIdColumn?: string;
2020
tracesSpanIdColumn?: string;
21+
tracesParentSpanIdColumn?: string;
2122
tracesOperationNameColumn?: string;
2223
tracesServiceNameColumn?: string;
2324
tracesDurationColumn?: string;
2425
tracesDurationUnit?: string;
2526
tracesStartTimeColumn?: string;
27+
tracesTagsColumn?: string;
28+
tracesServiceTagsColumn?: string;
29+
tracesStatusCodeColumn?: string;
30+
tracesStatusMessageColumn?: string;
31+
tracesKindColumn?: string;
2632
showTraceLinks?: boolean;
2733

2834
// Ad-hoc filters

src/types/queryBuilder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export enum ColumnHint {
1414
TraceServiceName = 'trace_service_name',
1515
TraceOperationName = 'trace_operation_name',
1616
TraceDurationTime = 'trace_duration_time',
17+
TraceTags = 'trace_tags',
18+
TraceServiceTags = 'trace_service_tags',
19+
TraceStatusCode = 'trace_status_code',
20+
TraceStatusMessage = 'trace_status_message',
21+
TraceKind = 'trace_kind',
1722
}
1823

1924
export enum OrderByDirection {

0 commit comments

Comments
 (0)