diff --git a/.betterer.results b/.betterer.results index bafa487b8df..2f9307b0639 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4096,7 +4096,9 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "7"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"] ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], diff --git a/packages/grafana-data/src/types/trace.ts b/packages/grafana-data/src/types/trace.ts index 27b16a9f73c..8346f07dfb9 100644 --- a/packages/grafana-data/src/types/trace.ts +++ b/packages/grafana-data/src/types/trace.ts @@ -13,6 +13,7 @@ export type TraceLog = { // Millisecond epoch time timestamp: number; fields: TraceKeyValuePair[]; + name?: string; }; export type TraceSpanReference = { diff --git a/pkg/tsdb/tempo/trace_transform.go b/pkg/tsdb/tempo/trace_transform.go index 0c3bf0321c2..a7d0f3c2dea 100644 --- a/pkg/tsdb/tempo/trace_transform.go +++ b/pkg/tsdb/tempo/trace_transform.go @@ -22,6 +22,7 @@ type TraceLog struct { // Millisecond epoch time Timestamp float64 `json:"timestamp"` Fields []*KeyValue `json:"fields"` + Name string `json:"name,omitempty"` } type TraceReference struct { @@ -260,12 +261,6 @@ func spanEventsToLogs(events ptrace.SpanEventSlice) []*TraceLog { for i := 0; i < events.Len(); i++ { event := events.At(i) fields := make([]*KeyValue, 0, event.Attributes().Len()+1) - if event.Name() != "" { - fields = append(fields, &KeyValue{ - Key: TagMessage, - Value: event.Name(), - }) - } event.Attributes().Range(func(key string, attr pcommon.Value) bool { fields = append(fields, &KeyValue{Key: key, Value: getAttributeVal(attr)}) return true @@ -273,6 +268,7 @@ func spanEventsToLogs(events ptrace.SpanEventSlice) []*TraceLog { logs = append(logs, &TraceLog{ Timestamp: float64(event.Timestamp()) / 1_000_000, Fields: fields, + Name: event.Name(), }) } diff --git a/pkg/tsdb/tempo/trace_transform_test.go b/pkg/tsdb/tempo/trace_transform_test.go index 8b901c6c41c..8253220655a 100644 --- a/pkg/tsdb/tempo/trace_transform_test.go +++ b/pkg/tsdb/tempo/trace_transform_test.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -57,7 +58,30 @@ func TestTraceToFrame(t *testing.T) { require.Equal(t, json.RawMessage("[{\"value\":\"loki-all\",\"key\":\"service.name\"},{\"value\":\"Jaeger-Go-2.25.0\",\"key\":\"opencensus.exporterversion\"},{\"value\":\"4d019a031941\",\"key\":\"host.hostname\"},{\"value\":\"172.18.0.6\",\"key\":\"ip\"},{\"value\":\"4b19ace06df8e4de\",\"key\":\"client-uuid\"}]"), span["serviceTags"]) require.Equal(t, 1616072924072.852, span["startTime"]) require.Equal(t, 0.094, span["duration"]) - require.Equal(t, "[{\"timestamp\":1616072924072.856,\"fields\":[{\"value\":\"test event\",\"key\":\"message\"},{\"value\":1,\"key\":\"chunks requested\"}]},{\"timestamp\":1616072924072.9448,\"fields\":[{\"value\":1,\"key\":\"chunks fetched\"}]}]", string(span["logs"].(json.RawMessage))) + expectedLogs := ` + [ + { + "timestamp": 1616072924072.856, + "name": "test event", + "fields": [ + { + "value": 1, + "key": "chunks requested" + } + ] + }, + { + "timestamp": 1616072924072.9448, + "fields": [ + { + "value": 1, + "key": "chunks fetched" + } + ] + } + ] + ` + assert.JSONEq(t, expectedLogs, string(span["logs"].(json.RawMessage))) }) t.Run("should transform correct traceID", func(t *testing.T) { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx index f2d0a4d9be2..29399e29552 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx @@ -67,7 +67,6 @@ export const getStyles = (theme: GrafanaTheme2) => { summaryItem: css` label: summaryItem; display: inline; - margin-left: 0.7em; padding-right: 0.5rem; border-right: 1px solid ${autoColor(theme, '#ddd')}; &:last-child { @@ -90,10 +89,11 @@ export const getStyles = (theme: GrafanaTheme2) => { export type AccordianKeyValuesProps = { className?: string | TNil; data: TraceKeyValuePair[]; + logName?: string; highContrast?: boolean; interactive?: boolean; isOpen: boolean; - label: string; + label: string | React.ReactNode; linksGetter?: ((pairs: TraceKeyValuePair[], index: number) => TraceLink[]) | TNil; onToggle?: null | (() => void); }; @@ -127,6 +127,7 @@ export function KeyValuesSummary({ data = null }: KeyValuesSummaryProps) { export default function AccordianKeyValues({ className = null, data, + logName, highContrast = false, interactive = true, isOpen, @@ -134,11 +135,12 @@ export default function AccordianKeyValues({ linksGetter, onToggle = null, }: AccordianKeyValuesProps) { - const isEmpty = !Array.isArray(data) || !data.length; + const isEmpty = (!Array.isArray(data) || !data.length) && !logName; const styles = useStyles2(getStyles); const iconCls = cx(alignIcon, { [styles.emptyIcon]: isEmpty }); let arrow: React.ReactNode | null = null; let headerProps: {} | null = null; + const tableFields = logName ? [{ key: 'event name', value: logName }, ...data] : data; if (interactive) { arrow = isOpen ? ( @@ -152,6 +154,8 @@ export default function AccordianKeyValues({ }; } + const showDataSummaryFields = data.length > 0 && !isOpen; + return (
{label} - {isOpen || ':'} + {showDataSummaryFields && ':'} - {!isOpen && } + {showDataSummaryFields && ( + + + + )}
- {isOpen && } + {isOpen && }
); } diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx index 8e5db8dcebd..f69f3c8808f 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.test.tsx @@ -30,6 +30,7 @@ const logs = [ { key: 'message', value: 'oh the next log message' }, { key: 'more', value: 'stuff' }, ], + name: 'foo event name', }, ]; @@ -72,4 +73,47 @@ describe('AccordianLogs tests', () => { expect(screen.getByText(/^something$/)).toBeInTheDocument(); expect(screen.getByText(/^else$/)).toBeInTheDocument(); }); + + it('shows log entries and long event name when expanded', () => { + const longNameLog = { + timestamp: 20, + name: 'This is a very very very very very very very long name', + fields: [{ key: 'foo', value: 'test' }], + }; + + setup({ + isOpen: true, + logs: [longNameLog], + openedItems: new Set([longNameLog]), + } as AccordianLogsProps); + + expect( + screen.getByRole('switch', { + name: '15μs (This is a very very ...)', + }) + ).toBeInTheDocument(); + + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.queryAllByRole('cell')).toHaveLength(6); + expect(screen.getByText(/^event name$/)).toBeInTheDocument(); + expect(screen.getByText(/This is a very very very very very very very long name/)).toBeInTheDocument(); + }); + + it('renders event name and duration when events list is closed', () => { + setup({ isOpen: true, openedItems: new Set() } as AccordianLogsProps); + expect( + screen.getByRole('switch', { + name: '15μs (foo event name) : message = oh the next log message more = stuff', + }) + ).toBeInTheDocument(); + expect( + screen.getByRole('switch', { name: '5μs: message = oh the log message something = else' }) + ).toBeInTheDocument(); + }); + + it('renders event name and duration when events list is open', () => { + setup({ isOpen: true, openedItems: new Set(logs) } as AccordianLogsProps); + expect(screen.getByRole('switch', { name: '15μs (foo event name)' })).toBeInTheDocument(); + expect(screen.getByRole('switch', { name: '5μs' })).toBeInTheDocument(); + }); }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx index 9e8cefccd8c..6ecfec0f80d 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx @@ -59,6 +59,9 @@ const getStyles = (theme: GrafanaTheme2) => { AccordianKeyValuesItem: css({ marginBottom: theme.spacing(0.5), }), + parenthesis: css({ + color: `${autoColor(theme, '#777')}`, + }), }; }; @@ -108,22 +111,35 @@ export default function AccordianLogs({ {isOpen && (
- {_sortBy(logs, 'timestamp').map((log, i) => ( - onItemToggle(log) : null} - /> - ))} + {_sortBy(logs, 'timestamp').map((log, i) => { + const formattedDuration = formatDuration(log.timestamp - timestamp); + const truncateLogNameInSummary = log.name && log.name.length > 20; + const formattedLogName = log.name && truncateLogNameInSummary ? log.name.slice(0, 20) + '...' : log.name; + const label = formattedLogName ? ( + + {formattedDuration} ({formattedLogName}) + + ) : ( + formattedDuration + ); + return ( + onItemToggle(log) : null} + /> + ); + })} - Log timestamps are relative to the start time of the full trace. + Event timestamps are relative to the start time of the full trace.
)} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx index 005d3af52ea..92b0731d467 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx @@ -48,7 +48,7 @@ export const getStyles = (theme: GrafanaTheme2) => { row: css` label: row; & > td { - padding: 0rem 0.5rem; + padding: 0.5rem 0.5rem; height: 30px; } &:nth-child(2n) > td { @@ -63,6 +63,7 @@ export const getStyles = (theme: GrafanaTheme2) => { color: ${autoColor(theme, '#888')}; white-space: pre; width: 125px; + vertical-align: top; `, copyColumn: css` label: copyColumn; diff --git a/public/app/features/explore/TraceView/components/types/trace.ts b/public/app/features/explore/TraceView/components/types/trace.ts index 0507287b987..bee45e059b4 100644 --- a/public/app/features/explore/TraceView/components/types/trace.ts +++ b/public/app/features/explore/TraceView/components/types/trace.ts @@ -31,6 +31,7 @@ export type TraceLink = { export type TraceLog = { timestamp: number; fields: TraceKeyValuePair[]; + name?: string; }; export type TraceProcess = { diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts index 1cf04d8424a..4bae04a74a2 100644 --- a/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.test.ts @@ -55,6 +55,7 @@ describe('filterSpans', () => { ], logs: [ { + name: 'logName0', fields: [ { key: 'logFieldKey0', @@ -316,6 +317,10 @@ describe('filterSpans', () => { ).toEqual(new Set([spanID0])); }); + it('it should return logs have a name which matches the filter', () => { + expect(filterSpans({ ...defaultFilters, query: 'logName0' }, spans)).toEqual(new Set([spanID0])); + }); + it('should return no spans when logs is null', () => { const nullSpan = { ...span0, logs: null }; expect( diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx index 942a13a476b..9f6d908d52e 100644 --- a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx +++ b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx @@ -90,7 +90,8 @@ export function getQueryMatches(query: string, spans: TraceSpan[] | TNil) { (span.instrumentationLibraryName && isTextInQuery(queryParts, span.instrumentationLibraryName)) || (span.instrumentationLibraryVersion && isTextInQuery(queryParts, span.instrumentationLibraryVersion)) || (span.traceState && isTextInQuery(queryParts, span.traceState)) || - (span.logs !== null && span.logs.some((log) => isTextInKeyValues(log.fields))) || + (span.logs !== null && + span.logs.some((log) => (log.name && isTextInQuery(queryParts, log.name)) || isTextInKeyValues(log.fields))) || isTextInKeyValues(span.process.tags) || queryParts.some((queryPart) => queryPart === span.spanID); diff --git a/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts b/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts index 0507287b987..bee45e059b4 100644 --- a/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts +++ b/public/app/plugins/datasource/jaeger/_importedDependencies/types/trace.ts @@ -31,6 +31,7 @@ export type TraceLink = { export type TraceLog = { timestamp: number; fields: TraceKeyValuePair[]; + name?: string; }; export type TraceProcess = { diff --git a/public/app/plugins/datasource/jaeger/types.ts b/public/app/plugins/datasource/jaeger/types.ts index fe6778957f6..3fc5f8de320 100644 --- a/public/app/plugins/datasource/jaeger/types.ts +++ b/public/app/plugins/datasource/jaeger/types.ts @@ -14,6 +14,7 @@ export type TraceLink = { export type TraceLog = { timestamp: number; fields: TraceKeyValuePair[]; + name?: string; }; export type TraceProcess = { diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index 592b043307b..b41458590d1 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -132,7 +132,7 @@ function getLogs(span: collectorTypes.opentelemetryProto.trace.v1.Span) { fields.push({ key: attribute.key, value: getAttributeValue(attribute.value) }); } } - logs.push({ fields, timestamp: event.timeUnixNano / 1000000 }); + logs.push({ fields, timestamp: event.timeUnixNano / 1000000, name: event.name }); } } @@ -364,7 +364,7 @@ function getOTLPEvents(logs: TraceLog[]): collectorTypes.opentelemetryProto.trac timeUnixNano: log.timestamp * 1000000, attributes: [], droppedAttributesCount: 0, - name: '', + name: log.name || '', }; for (const field of log.fields) { event.attributes!.push({ diff --git a/public/app/plugins/datasource/tempo/test/testResponse.ts b/public/app/plugins/datasource/tempo/test/testResponse.ts index 493cb97501a..765e55cf08c 100644 --- a/public/app/plugins/datasource/tempo/test/testResponse.ts +++ b/public/app/plugins/datasource/tempo/test/testResponse.ts @@ -1920,7 +1920,7 @@ export const otlpDataFrameFromResponse = new MutableDataFrame({ name: 'logs', type: FieldType.other, config: {}, - values: [[]], + values: [[{ name: 'DNSDone', fields: [{ key: 'addr', value: '172.18.0.6' }] }]], }, { name: 'references', @@ -2138,7 +2138,20 @@ export const otlpDataFrameToResponse = new MutableDataFrame({ name: 'logs', type: FieldType.other, config: {}, - values: [[]], + values: [ + [ + { + fields: [ + { + key: 'addr', + value: '172.18.0.6', + }, + ], + timestamp: 1627471657255.809, + name: 'DNSDone', + }, + ], + ], state: { displayName: 'logs', }, @@ -2240,6 +2253,14 @@ export const otlpResponse = { { key: 'http.url', value: { stringValue: '/' } }, { key: 'component', value: { stringValue: 'net/http' } }, ], + events: [ + { + name: 'DNSDone', + attributes: [{ key: 'addr', value: { stringValue: '172.18.0.6' } }], + droppedAttributesCount: 0, + timeUnixNano: 1627471657255809000, + }, + ], links: [ { spanId: 'spanId',