mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TraceView: Display event names of a span (#91382)
* Display event name of a span * Clean up * Retrigger the build * Show colon only when there are fields to display * Rollback * Use event name when exporting to OTLP * Allow filtering spans by event name * Show duration as a key/value pair * Update betterer report (we do not translate panels that are planned to be externalized) * Fix tests after changing how duration is rendered * Handle long names * Test handling long names * Make parenthesis gray * Fix a test * Fix linting * Fix tests * Update label
This commit is contained in:
parent
1df622641c
commit
d7d22bbbb8
@ -4096,7 +4096,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"]
|
||||
],
|
||||
"public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
|
@ -13,6 +13,7 @@ export type TraceLog = {
|
||||
// Millisecond epoch time
|
||||
timestamp: number;
|
||||
fields: TraceKeyValuePair[];
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type TraceSpanReference = {
|
||||
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 ? (
|
||||
<Icon name={'angle-down'} className={iconCls} />
|
||||
@ -152,6 +154,8 @@ export default function AccordianKeyValues({
|
||||
};
|
||||
}
|
||||
|
||||
const showDataSummaryFields = data.length > 0 && !isOpen;
|
||||
|
||||
return (
|
||||
<div className={cx(className, styles.container)}>
|
||||
<div
|
||||
@ -165,11 +169,15 @@ export default function AccordianKeyValues({
|
||||
{arrow}
|
||||
<strong data-test={markers.LABEL}>
|
||||
{label}
|
||||
{isOpen || ':'}
|
||||
{showDataSummaryFields && ':'}
|
||||
</strong>
|
||||
{!isOpen && <KeyValuesSummary data={data} />}
|
||||
{showDataSummaryFields && (
|
||||
<span className={css({ marginLeft: '0.7em' })}>
|
||||
<KeyValuesSummary data={data} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isOpen && <KeyValuesTable data={data} linksGetter={linksGetter} />}
|
||||
{isOpen && <KeyValuesTable data={tableFields} linksGetter={linksGetter} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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({
|
||||
</HeaderComponent>
|
||||
{isOpen && (
|
||||
<div className={styles.AccordianLogsContent}>
|
||||
{_sortBy(logs, 'timestamp').map((log, i) => (
|
||||
<AccordianKeyValues
|
||||
// `i` is necessary in the key because timestamps can repeat
|
||||
key={`${log.timestamp}-${i}`}
|
||||
className={i < logs.length - 1 ? styles.AccordianKeyValuesItem : null}
|
||||
data={log.fields || []}
|
||||
highContrast
|
||||
interactive={interactive}
|
||||
isOpen={openedItems ? openedItems.has(log) : false}
|
||||
label={`${formatDuration(log.timestamp - timestamp)}`}
|
||||
linksGetter={linksGetter}
|
||||
onToggle={interactive && onItemToggle ? () => 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 ? (
|
||||
<span>
|
||||
{formattedDuration} <span>({formattedLogName})</span>
|
||||
</span>
|
||||
) : (
|
||||
formattedDuration
|
||||
);
|
||||
return (
|
||||
<AccordianKeyValues
|
||||
// `i` is necessary in the key because timestamps can repeat
|
||||
key={`${log.timestamp}-${i}`}
|
||||
className={i < logs.length - 1 ? styles.AccordianKeyValuesItem : null}
|
||||
data={log.fields || []}
|
||||
logName={truncateLogNameInSummary ? log.name : undefined}
|
||||
highContrast
|
||||
interactive={interactive}
|
||||
isOpen={openedItems ? openedItems.has(log) : false}
|
||||
label={label}
|
||||
linksGetter={linksGetter}
|
||||
onToggle={interactive && onItemToggle ? () => onItemToggle(log) : null}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<small className={styles.AccordianLogsFooter}>
|
||||
Log timestamps are relative to the start time of the full trace.
|
||||
Event timestamps are relative to the start time of the full trace.
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
@ -31,6 +31,7 @@ export type TraceLink = {
|
||||
export type TraceLog = {
|
||||
timestamp: number;
|
||||
fields: TraceKeyValuePair[];
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type TraceProcess = {
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
||||
|
@ -31,6 +31,7 @@ export type TraceLink = {
|
||||
export type TraceLog = {
|
||||
timestamp: number;
|
||||
fields: TraceKeyValuePair[];
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type TraceProcess = {
|
||||
|
@ -14,6 +14,7 @@ export type TraceLink = {
|
||||
export type TraceLog = {
|
||||
timestamp: number;
|
||||
fields: TraceKeyValuePair[];
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type TraceProcess = {
|
||||
|
@ -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({
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user