diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx
index 597b6d125c6..908126b649b 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.tsx
@@ -96,7 +96,7 @@ describe('AccordianKeyValues test', () => {
it('renders the summary instead of the table when it is not expanded', () => {
setupAccordian({ isOpen: false } as AccordianKeyValuesProps);
- expect(screen.getByRole('switch', { name: 'test accordian: span.kind client omg mos-def' })).toBeInTheDocument();
+ expect(screen.getByRole('switch', { name: 'test accordian span.kind client omg mos-def' })).toBeInTheDocument();
expect(screen.queryByRole('table')).not.toBeInTheDocument();
expect(screen.queryAllByRole('cell')).toHaveLength(0);
});
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 d8d621cf218..93822458ac7 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianKeyValues.tsx
@@ -17,7 +17,7 @@ import cx from 'classnames';
import * as React from 'react';
import { GrafanaTheme2, TraceKeyValuePair } from '@grafana/data';
-import { Icon, useStyles2 } from '@grafana/ui';
+import { Counter, Icon, useStyles2 } from '@grafana/ui';
import { autoColor } from '../../Theme';
import TNil from '../../types/TNil';
@@ -43,6 +43,10 @@ export const getStyles = (theme: GrafanaTheme2) => {
background: autoColor(theme, '#e8e8e8'),
},
}),
+ headerLabel: css({
+ width: '120px',
+ display: 'inline-block',
+ }),
headerEmpty: css({
label: 'headerEmpty',
background: 'none',
@@ -87,6 +91,9 @@ export type AccordianKeyValuesProps = {
logName?: string;
highContrast?: boolean;
interactive?: boolean;
+ onlyValues?: boolean;
+ showSummary?: boolean;
+ showCountBadge?: boolean;
isOpen: boolean;
label: string | React.ReactNode;
linksGetter?: ((pairs: TraceKeyValuePair[], index: number) => KeyValuesTableLink[]) | TNil;
@@ -127,6 +134,9 @@ export default function AccordianKeyValues({
isOpen,
label,
linksGetter,
+ onlyValues = false,
+ showSummary = true,
+ showCountBadge = false,
onToggle = null,
}: AccordianKeyValuesProps) {
const isEmpty = (!Array.isArray(data) || !data.length) && !logName;
@@ -148,7 +158,7 @@ export default function AccordianKeyValues({
};
}
- const showDataSummaryFields = data.length > 0 && !isOpen;
+ const showDataSummaryFields = showSummary && data.length > 0 && !isOpen;
return (
@@ -161,9 +171,9 @@ export default function AccordianKeyValues({
data-testid="AccordianKeyValues--header"
>
{arrow}
-
+
{label}
- {showDataSummaryFields && ':'}
+ {showCountBadge ? : null}
{showDataSummaryFields && (
@@ -171,7 +181,7 @@ export default function AccordianKeyValues({
)}
- {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 18085f3fdcb..19ff85b2dd7 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
@@ -103,10 +103,10 @@ describe('AccordianLogs tests', () => {
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',
+ 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();
+ 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', () => {
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 5584760b04b..b708b7f3ec8 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx
@@ -32,14 +32,12 @@ const getStyles = (theme: GrafanaTheme2) => {
AccordianLogs: css({
label: 'AccordianLogs',
position: 'relative',
- marginBottom: '0.25rem',
}),
AccordianLogsHeader: css({
label: 'AccordianLogsHeader',
color: 'inherit',
display: 'flex',
alignItems: 'center',
- padding: '0.25rem 0.1em',
'&:hover': {
background: autoColor(theme, '#e8e8e8'),
},
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx
index d7296055bfc..14fa5187396 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.test.tsx
@@ -75,7 +75,7 @@ describe('AccordianReferences tests', () => {
it('renders the correct number of references', () => {
setup();
- expect(screen.getByRole('switch', { name: 'References (3)' })).toBeInTheDocument();
+ expect(screen.getByRole('switch', { name: 'References 3' })).toBeInTheDocument();
});
it('content doesnt show when not expanded', () => {
@@ -88,7 +88,7 @@ describe('AccordianReferences tests', () => {
it('renders the content when it is expanded', () => {
setup({ isOpen: true } as AccordianReferencesProps);
- expect(screen.getByRole('switch', { name: 'References (3)' })).toBeInTheDocument();
+ expect(screen.getByRole('switch', { name: 'References 3' })).toBeInTheDocument();
expect(screen.getAllByRole('link', { name: /^service\d\sop\d/ })).toHaveLength(2);
expect(screen.getByRole('link', { name: /^View\sLinked/ })).toBeInTheDocument();
});
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx
index de60443707e..10c31897ec6 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx
@@ -17,7 +17,7 @@ import * as React from 'react';
import { Field, GrafanaTheme2, LinkModel } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
-import { Icon, useStyles2 } from '@grafana/ui';
+import { Counter, Icon, useStyles2 } from '@grafana/ui';
import { autoColor } from '../../Theme';
import { TraceSpanReference } from '../../types/trace';
@@ -36,16 +36,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
}),
AccordianReferences: css({
label: 'AccordianReferences',
- border: `1px solid ${autoColor(theme, '#d8d8d8')}`,
position: 'relative',
marginBottom: '0.25rem',
}),
AccordianReferencesHeader: css({
label: 'AccordianReferencesHeader',
- background: autoColor(theme, '#e4e4e4'),
color: 'inherit',
display: 'block',
- padding: '0.25rem 0.5rem',
+ padding: '0.25rem 0',
'&:hover': {
background: autoColor(theme, '#dadada'),
},
@@ -223,7 +221,7 @@ const AccordianReferences = ({
References
{' '}
- ({data.length})
+
{isOpen && (
', () => {
- const props = {
- compact: false,
- data: warnings,
- highContrast: false,
- isOpen: false,
- label: 'le-label',
- onToggle: jest.fn(),
- };
-
- it('renders without exploding', () => {
- render();
- expect(() => render()).not.toThrow();
- });
-
- it('renders the label', () => {
- render();
- const { getByText } = within(screen.getByTestId('AccordianText--header'));
- expect(getByText(props.label)).toBeInTheDocument();
- });
-
- it('renders the content when it is expanded', () => {
- props.isOpen = true;
- render();
- warnings.forEach((warning) => {
- expect(screen.getByText(warning)).toBeInTheDocument();
- });
- });
-});
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx
deleted file mode 100644
index f0efc64b10a..00000000000
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/AccordianText.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) 2019 Uber Technologies, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import { css } from '@emotion/css';
-import cx from 'classnames';
-import * as React from 'react';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { Icon, useStyles2 } from '@grafana/ui';
-
-import { autoColor } from '../../Theme';
-import TNil from '../../types/TNil';
-
-import { getStyles as getAccordianKeyValuesStyles } from './AccordianKeyValues';
-import TextList from './TextList';
-
-import { alignIcon } from '.';
-
-const getStyles = (theme: GrafanaTheme2) => ({
- header: css({
- cursor: 'pointer',
- overflow: 'hidden',
- padding: '0.25em 0.1em',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- '&:hover': {
- background: autoColor(theme, '#e8e8e8'),
- },
- }),
-});
-
-type AccordianTextProps = {
- className?: string | TNil;
- headerClassName?: string | TNil;
- data: string[];
- highContrast?: boolean;
- interactive?: boolean;
- isOpen: boolean;
- label: React.ReactNode | string;
- onToggle?: null | (() => void);
- TextComponent?: React.ElementType<{ data: string[] }>;
-};
-
-function DefaultTextComponent({ data }: { data: string[] }) {
- return ;
-}
-
-export default function AccordianText({
- className = null,
- data,
- headerClassName,
- highContrast = false,
- interactive = true,
- isOpen,
- label,
- onToggle = null,
- TextComponent = DefaultTextComponent,
-}: AccordianTextProps) {
- const isEmpty = !Array.isArray(data) || !data.length;
- const accordianKeyValuesStyles = useStyles2(getAccordianKeyValuesStyles);
- const iconCls = cx(alignIcon, { [accordianKeyValuesStyles.emptyIcon]: isEmpty });
- let arrow: React.ReactNode | null = null;
- let headerProps: {} | null = null;
- if (interactive) {
- arrow = isOpen ? (
-
- ) : (
-
- );
- headerProps = {
- 'aria-checked': isOpen,
- onClick: isEmpty ? null : onToggle,
- role: 'switch',
- };
- }
- const styles = useStyles2(getStyles);
- return (
-
-
- {arrow}
- {label} ({data.length})
-
- {isOpen &&
}
-
- );
-}
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 83632c9351d..a6c79b2d2cf 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx
@@ -47,7 +47,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
row: css({
label: 'row',
'& > td': {
- padding: '0.5rem 0.5rem',
+ padding: '0 0.5rem',
height: '30px',
},
'&:nth-child(2n) > td': {
@@ -68,7 +68,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
color: autoColor(theme, '#888'),
whiteSpace: 'pre',
width: '125px',
- verticalAlign: 'top',
}),
copyColumn: css({
label: 'copyColumn',
@@ -118,19 +117,32 @@ export const LinkValue = ({ link, children }: PropsWithChildren)
export type KeyValuesTableProps = {
data: TraceKeyValuePair[];
linksGetter?: ((pairs: TraceKeyValuePair[], index: number) => KeyValuesTableLink[]) | TNil;
+ onlyValues?: boolean;
};
export default function KeyValuesTable(props: KeyValuesTableProps) {
- const { data, linksGetter } = props;
+ const { data, linksGetter, onlyValues } = props;
const styles = useStyles2(getStyles);
return (
{data.map((row, i) => {
- const markup = {
- __html: jsonMarkup(parseIfComplexJson(row.value)),
- };
+ let markup = { __html: '' };
+ if (row.type === 'code') {
+ markup = {
+ __html: `${row.value}
`,
+ };
+ } else if (row.type === 'text') {
+ markup = {
+ __html: `${row.value}`,
+ };
+ } else {
+ markup = {
+ __html: jsonMarkup(parseIfComplexJson(row.value)),
+ };
+ }
+
const jsonTable = ;
const links = linksGetter?.(data, i);
let valueMarkup;
@@ -147,15 +159,17 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
return (
// `i` is necessary in the key because row.key can repeat
-
- {row.key}
- |
+ {!onlyValues && (
+
+ {row.key}
+ |
+ )}
{valueMarkup} |
|
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx
index f59b1fb4421..29ee78fb866 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx
@@ -15,9 +15,10 @@
import { css } from '@emotion/css';
import cx from 'classnames';
+import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
-const getStyles = () => ({
+const getStyles = (theme: GrafanaTheme2) => ({
TextList: css({
maxHeight: '450px',
overflow: 'auto',
@@ -32,7 +33,7 @@ const getStyles = () => ({
padding: '0.25rem 0.5rem',
verticalAlign: 'top',
'&:nth-child(2n)': {
- background: '#f5f5f5',
+ background: theme.colors.background.secondary,
},
}),
});
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx
index 67a6043f397..627e8fc2ec4 100644
--- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx
+++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx
@@ -29,11 +29,11 @@ import {
PluginExtensionPoints,
IconName,
} from '@grafana/data';
-import { Trans, t } from '@grafana/i18n';
+import { t } from '@grafana/i18n';
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend';
import { usePluginLinks } from '@grafana/runtime';
import { TimeZone } from '@grafana/schema';
-import { TextArea, useStyles2 } from '@grafana/ui';
+import { useStyles2 } from '@grafana/ui';
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink';
import { autoColor } from '../../Theme';
@@ -46,7 +46,6 @@ import { formatDuration } from '../utils';
import AccordianKeyValues from './AccordianKeyValues';
import AccordianLogs from './AccordianLogs';
import AccordianReferences from './AccordianReferences';
-import AccordianText from './AccordianText';
import DetailState from './DetailState';
import { ShareSpanButton } from './ShareSpanButton';
import { getSpanDetailLinkButtons } from './SpanDetailLinkButtons';
@@ -99,6 +98,9 @@ const getStyles = (theme: GrafanaTheme2) => {
gap: '0 1rem',
marginBottom: '0.25rem',
}),
+ content: css({
+ fontSize: theme.typography.bodySmall.fontSize,
+ }),
listWrapper: css({
overflow: 'hidden',
flexGrow: 1,
@@ -340,7 +342,7 @@ export default function SpanDetail(props: SpanDetailProps) {
{linksComponent}
-
+
)}
+
{warnings && warnings.length > 0 && (
-
- Warnings
-
- }
- data={warnings}
+ ({
+ key: '',
+ value: warning,
+ type: 'text',
+ }))}
+ showSummary={false}
+ showCountBadge={true}
isOpen={isWarningsOpen}
+ onlyValues={true}
onToggle={() => warningsToggle(spanID)}
+ label={t('explore.span-detail.warnings', 'Warnings')}
/>
)}
+
{stackTraces?.length ? (
- ({
+ key: '',
+ value: stackTrace,
+ type: 'code',
+ }))}
+ onlyValues={true}
+ showSummary={false}
+ showCountBadge={true}
isOpen={isStackTracesOpen}
- TextComponent={(textComponentProps) => {
- let text;
- if (textComponentProps.data?.length > 1) {
- text = textComponentProps.data
- .map((stackTrace, index) => `StackTrace ${index + 1}:\n${stackTrace}`)
- .join('\n');
- } else {
- text = textComponentProps.data?.[0];
- }
- return (
-
- );
- }}
onToggle={() => stackTracesToggle(spanID)}
+ label={t('explore.span-detail.label-stack-trace', 'Stack trace')}
/>
) : null}
+
{references && references.length > 0 && (references.length > 1 || references[0].refType !== 'CHILD_OF') && (
({
'& .json-markup': {
lineHeight: '17px',
- fontSize: '13px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
},
diff --git a/public/app/features/explore/TraceView/components/common/CopyIcon.tsx b/public/app/features/explore/TraceView/components/common/CopyIcon.tsx
index 52b72fbbcbd..13614c7417b 100644
--- a/public/app/features/explore/TraceView/components/common/CopyIcon.tsx
+++ b/public/app/features/explore/TraceView/components/common/CopyIcon.tsx
@@ -24,7 +24,6 @@ const getStyles = () => ({
backgroundColor: 'transparent',
border: 'none',
color: 'inherit',
- height: '100%',
overflow: 'hidden',
'&:focus': {
backgroundColor: 'rgba(255, 255, 255, 0.25)',
diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts
index 3fd778a7659..bf2dc772cc1 100644
--- a/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts
+++ b/public/app/plugins/datasource/grafana-testdata-datasource/datasource.ts
@@ -290,6 +290,8 @@ export class TestDataDataSource extends DataSourceWithBackend
{ name: 'tags' },
{ name: 'kind' },
{ name: 'statusCode' },
+ { name: 'warnings' },
+ { name: 'stackTraces' },
],
});
const numberOfSpans = options.targets[0].spanCount || 10;
@@ -327,6 +329,52 @@ export class TestDataDataSource extends DataSourceWithBackend
: [],
kind: i === 0 ? 'client' : kinds[Math.floor(Math.random() * kinds.length)],
statusCode: statusCodes[Math.floor(Math.random() * statusCodes.length)],
+ references:
+ i === 0
+ ? []
+ : [
+ {
+ refType: 'EXTERNAL',
+ spanID: spanIdPrefix + 10001,
+ traceID: spanIdPrefix + '10000',
+ tags: [
+ { key: 'external.service', value: `Service${i}` },
+ { key: 'resource.pod', value: `Pod${i}` },
+ ],
+ },
+ ],
+ warnings:
+ i % 2 === 0
+ ? [
+ '[2025-07-30T14:12:09Z] [payment-service] Delayed response from external payment gateway (Stripe). Request ID: 8f3a7c2b-ff13-4e3c-b610-18a92c8b199d. Latency: 3421ms. Threshold: 2500ms.',
+ '[2025-07-30T14:14:42Z] [notification-service] Email delivery failed for user_id=93244 (Email: user@example.com). SMTP server responded with status 421: "Service not available, closing transmission channel".',
+ '[2025-07-30T14:18:55Z] [user-profile-service] Deprecated API version used in request: /v1/profile/update. Recommend migrating to /v2/profile/update. Client ID: svc-auth-34.',
+ ]
+ : [],
+ stackTraces:
+ i % 4 === 0
+ ? [
+ 'Traceback (most recent call last):\n' +
+ ' File "/app/services/payment.py", line 112, in process_transaction\n' +
+ ' response = gateway.charge(card_info, amount)\n' +
+ ' File "/app/lib/gateway/stripe_client.py", line 76, in charge\n' +
+ ' return self._send_request(payload)\n' +
+ ' File "/app/lib/gateway/stripe_client.py", line 45, in _send_request\n' +
+ ' raise GatewayTimeoutError("Stripe request timed out after 3000ms")\n' +
+ 'gateway.exceptions.GatewayTimeoutError: Stripe request timed out after 3000ms\n',
+
+ 'Traceback (most recent call last):\n' +
+ ' File "/usr/src/app/main.py", line 27, in \n' +
+ ' run_app()\n' +
+ ' File "/usr/src/app/core/server.py", line 88, in run_app\n' +
+ ' initialize_services()\n' +
+ ' File "/usr/src/app/core/init.py", line 52, in initialize_services\n' +
+ ' db.connect()\n' +
+ ' File "/usr/src/app/db/connection.py", line 31, in connect\n' +
+ ' raise DatabaseConnectionError("Failed to connect to database: timeout after 5s")\n' +
+ 'db.exceptions.DatabaseConnectionError: Failed to connect to database: timeout after 5s\n',
+ ]
+ : [],
});
}