Skip to content

Commit 4afdc57

Browse files
Add field config to trace search results for better default display (#1733)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a64fe45 commit 4afdc57

File tree

3 files changed

+170
-1
lines changed

3 files changed

+170
-1
lines changed

cspell.config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"networkidle",
5656
"nofile",
5757
"nolint",
58+
"operationname",
5859
"Nonproxy",
5960
"openfeature",
6061
"oper",
@@ -69,6 +70,7 @@
6970
"regexes",
7071
"schemads",
7172
"sdkproxy",
73+
"servicename",
7274
"shopspring",
7375
"singlequote",
7476
"slvrtrn",

src/data/utils.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ColumnHint, QueryBuilderOptions, QueryType } from 'types/queryBuilder';
22
import {
3+
applyTraceSearchFieldConfig,
34
columnLabelToPlaceholder,
45
dataFrameHasLogLabelWithName,
56
isBuilderOptionsRunnable,
@@ -110,6 +111,116 @@ describe('columnLabelToPlaceholder', () => {
110111
});
111112
});
112113

114+
describe('applyTraceSearchFieldConfig', () => {
115+
const buildTraceSearchRequestResponse = (
116+
fields: Field[],
117+
builderOptions: Partial<QueryBuilderOptions> = {}
118+
): [DataQueryRequest<CHQuery>, DataQueryResponse] => {
119+
const inputQuery: CHBuilderQuery = {
120+
refId: 'A',
121+
editorType: EditorType.Builder,
122+
builderOptions: {
123+
database: 'default',
124+
table: 'otel_traces',
125+
queryType: QueryType.Traces,
126+
...builderOptions,
127+
},
128+
pluginVersion: '',
129+
rawSql: '',
130+
};
131+
132+
const request: DataQueryRequest<CHQuery> = {
133+
requestId: '',
134+
interval: '',
135+
intervalMs: 0,
136+
range: {} as any,
137+
scopedVars: {} as any,
138+
targets: [inputQuery],
139+
timezone: '',
140+
app: CoreApp.Explore,
141+
startTime: 0,
142+
};
143+
144+
const data: DataFrame[] = [{
145+
fields,
146+
length: 1,
147+
refId: 'A',
148+
}];
149+
const response: DataQueryResponse = { data };
150+
151+
return [request, response];
152+
};
153+
154+
it('applies field configs to trace search result fields', () => {
155+
const fields: Field[] = [
156+
{ name: 'traceID', type: FieldType.string, config: {}, values: [] },
157+
{ name: 'serviceName', type: FieldType.string, config: {}, values: [] },
158+
{ name: 'operationName', type: FieldType.string, config: {}, values: [] },
159+
{ name: 'startTime', type: FieldType.time, config: {}, values: [] },
160+
{ name: 'duration', type: FieldType.number, config: {}, values: [] },
161+
];
162+
163+
const [request, response] = buildTraceSearchRequestResponse(fields);
164+
applyTraceSearchFieldConfig(request, response);
165+
166+
expect(response.data[0].fields[4].config.unit).toBe('ms');
167+
expect(response.data[0].fields[4].config.displayName).toBe('Duration');
168+
expect(response.data[0].fields[0].config.displayName).toBe('Trace ID');
169+
expect(response.data[0].fields[1].config.displayName).toBe('Service Name');
170+
expect(response.data[0].fields[2].config.displayName).toBe('Operation Name');
171+
expect(response.data[0].fields[3].config.displayName).toBe('Start Time');
172+
});
173+
174+
it('does not apply field configs to trace ID mode queries', () => {
175+
const fields: Field[] = [
176+
{ name: 'duration', type: FieldType.number, config: {}, values: [] },
177+
];
178+
179+
const [request, response] = buildTraceSearchRequestResponse(fields, {
180+
meta: { isTraceIdMode: true, traceId: 'abc123' },
181+
});
182+
applyTraceSearchFieldConfig(request, response);
183+
184+
expect(response.data[0].fields[0].config.unit).toBeUndefined();
185+
});
186+
187+
it('does not apply field configs to non-trace queries', () => {
188+
const fields: Field[] = [
189+
{ name: 'duration', type: FieldType.number, config: {}, values: [] },
190+
];
191+
192+
const [request, response] = buildTraceSearchRequestResponse(fields, {
193+
queryType: QueryType.Table,
194+
});
195+
applyTraceSearchFieldConfig(request, response);
196+
197+
expect(response.data[0].fields[0].config.unit).toBeUndefined();
198+
});
199+
200+
it('preserves existing field config properties', () => {
201+
const fields: Field[] = [
202+
{ name: 'duration', type: FieldType.number, config: { decimals: 2 }, values: [] },
203+
];
204+
205+
const [request, response] = buildTraceSearchRequestResponse(fields);
206+
applyTraceSearchFieldConfig(request, response);
207+
208+
expect(response.data[0].fields[0].config.unit).toBe('ms');
209+
expect(response.data[0].fields[0].config.decimals).toBe(2);
210+
});
211+
212+
it('does not modify fields that have no matching config', () => {
213+
const fields: Field[] = [
214+
{ name: 'customColumn', type: FieldType.string, config: {}, values: [] },
215+
];
216+
217+
const [request, response] = buildTraceSearchRequestResponse(fields);
218+
applyTraceSearchFieldConfig(request, response);
219+
220+
expect(response.data[0].fields[0].config).toEqual({});
221+
});
222+
});
223+
113224
describe('transformQueryResponseWithTraceAndLogLinks', () => {
114225
const buildTestRequestResponse = (
115226
builderOptions: Partial<QueryBuilderOptions>

src/data/utils.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CoreApp, DataFrame, DataQueryRequest, DataQueryResponse } from '@grafana/data';
1+
import { CoreApp, DataFrame, DataQueryRequest, DataQueryResponse, FieldConfig } from '@grafana/data';
22
import {
33
ColumnHint,
44
FilterOperator,
@@ -121,6 +121,60 @@ export const tryApplyColumnHints = (columns: SelectedColumn[], hintsToColumns?:
121121
*/
122122
export const columnLabelToPlaceholder = (label: string) => label.toLowerCase().replace(/ /g, '_');
123123

124+
/**
125+
* Field config map for trace search result columns.
126+
* Maps column name (lowercase) to Grafana FieldConfig for better default display.
127+
*/
128+
const traceSearchFieldConfigs: Record<string, FieldConfig> = {
129+
duration: {
130+
unit: 'ms',
131+
displayName: 'Duration',
132+
},
133+
starttime: {
134+
displayName: 'Start Time',
135+
},
136+
servicename: {
137+
displayName: 'Service Name',
138+
},
139+
operationname: {
140+
displayName: 'Operation Name',
141+
},
142+
traceid: {
143+
displayName: 'Trace ID',
144+
},
145+
};
146+
147+
/**
148+
* Applies field configs to trace search result frames for better default display.
149+
* Trace search results are table-format frames from trace queries (non-traceIdMode).
150+
*/
151+
export const applyTraceSearchFieldConfig = (req: DataQueryRequest<CHQuery>, res: DataQueryResponse): void => {
152+
res.data.forEach((frame: DataFrame) => {
153+
const originalQuery = req.targets.find((t) => t.refId === frame.refId) as CHBuilderQuery;
154+
if (!originalQuery) {
155+
return;
156+
}
157+
158+
const isTraceSearch = originalQuery.editorType === EditorType.Builder &&
159+
originalQuery.builderOptions.queryType === QueryType.Traces &&
160+
!originalQuery.builderOptions.meta?.isTraceIdMode;
161+
162+
if (!isTraceSearch) {
163+
return;
164+
}
165+
166+
frame.fields.forEach((field) => {
167+
const fieldConfig = traceSearchFieldConfigs[field.name.toLowerCase()];
168+
if (fieldConfig) {
169+
field.config = {
170+
...field.config,
171+
...fieldConfig,
172+
};
173+
}
174+
});
175+
});
176+
};
177+
124178
/**
125179
* Mutates the DataQueryResponse to include trace/log links on the traceID field.
126180
* The link will open a second query editor in split view
@@ -133,6 +187,8 @@ export const transformQueryResponseWithTraceAndLogLinks = (
133187
req: DataQueryRequest<CHQuery>,
134188
res: DataQueryResponse
135189
): DataQueryResponse => {
190+
applyTraceSearchFieldConfig(req, res);
191+
136192
res.data.forEach((frame: DataFrame) => {
137193
const originalQuery = req.targets.find((t) => t.refId === frame.refId) as CHBuilderQuery;
138194
if (!originalQuery) {

0 commit comments

Comments
 (0)