Trace View: Export trace button (#67368)

* Add export button to trace view

* Support zipkin, jaeger and otlp formats
This commit is contained in:
Andre Pereira 2023-04-28 15:34:14 +01:00 committed by GitHub
parent cd0fb21f29
commit ffd83a027d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 69 additions and 37 deletions

View File

@ -3,6 +3,7 @@ import React, { RefObject, useMemo, useState } from 'react';
import { useToggle } from 'react-use';
import {
CoreApp,
DataFrame,
DataLink,
DataSourceApi,
@ -76,7 +77,7 @@ type Props = {
};
export function TraceView(props: Props) {
const { spanFindMatches, traceProp, datasource, topOfViewRef, topOfViewRefType } = props;
const { spanFindMatches, traceProp, datasource, topOfViewRef, topOfViewRefType, exploreId } = props;
const {
detailStates,
@ -159,6 +160,7 @@ export function TraceView(props: Props) {
<>
<NewTracePageHeader
trace={traceProp}
data={props.dataFrames[0]}
timeZone={timeZone}
search={newTraceViewHeaderSearch}
setSearch={setNewTraceViewHeaderSearch}
@ -170,6 +172,7 @@ export function TraceView(props: Props) {
spanFilterMatches={spanFilterMatches}
datasourceType={datasourceType}
setHeaderHeight={setHeaderHeight}
app={exploreId ? CoreApp.Explore : CoreApp.Unknown}
/>
<SpanGraph
trace={traceProp}

View File

@ -1,8 +1,13 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { CoreApp, DataFrame } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { config } from '../../../../../../core/config';
import { downloadTraceAsJson } from '../../../../../inspector/utils/download';
import ActionButton from './ActionButton';
export const getStyles = () => {
@ -17,10 +22,12 @@ export const getStyles = () => {
export type TracePageActionsProps = {
traceId: string;
data: DataFrame;
app?: CoreApp;
};
export default function TracePageActions(props: TracePageActionsProps) {
const { traceId } = props;
const { traceId, data, app } = props;
const styles = useStyles2(getStyles);
const [copyTraceIdClicked, setCopyTraceIdClicked] = useState(false);
@ -32,6 +39,16 @@ export default function TracePageActions(props: TracePageActionsProps) {
}, 5000);
};
const exportTrace = () => {
const traceFormat = downloadTraceAsJson(data, 'Trace-' + traceId.substring(traceId.length - 6));
reportInteraction('grafana_traces_download_traces_clicked', {
app,
grafana_version: config.buildInfo.version,
trace_format: traceFormat,
location: 'trace-view',
});
};
return (
<div className={styles.TracePageActions}>
<ActionButton
@ -40,12 +57,7 @@ export default function TracePageActions(props: TracePageActionsProps) {
label={copyTraceIdClicked ? 'Copied!' : 'Trace ID'}
icon={'copy'}
/>
{/* <ActionButton
onClick={() => alert('not implemented')}
ariaLabel={'Export Trace'}
label={'Export'}
icon={'save'}
/> */}
<ActionButton onClick={exportTrace} ariaLabel={'Export Trace'} label={'Export'} icon={'save'} />
</div>
);
}

View File

@ -15,6 +15,7 @@
import { getByText, render } from '@testing-library/react';
import React from 'react';
import { MutableDataFrame } from '@grafana/data';
import config from 'app/core/config';
import { defaultFilters } from '../../useSearch';
@ -36,6 +37,7 @@ const setup = () => {
setFocusedSpanIdForSearch: jest.fn(),
datasourceType: 'tempo',
setHeaderHeight: jest.fn(),
data: new MutableDataFrame(),
};
return render(<NewTracePageHeader {...defaultProps} />);

View File

@ -16,7 +16,7 @@ import { css } from '@emotion/css';
import cx from 'classnames';
import React, { memo, useEffect, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { CoreApp, DataFrame, GrafanaTheme2 } from '@grafana/data';
import { TimeZone } from '@grafana/schema';
import { Badge, BadgeColor, Tooltip, useStyles2 } from '@grafana/ui';
@ -34,6 +34,8 @@ import { timestamp, getStyles } from './TracePageHeader';
export type TracePageHeaderProps = {
trace: Trace | null;
data: DataFrame;
app?: CoreApp;
timeZone: TimeZone;
search: SearchProps;
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
@ -50,6 +52,8 @@ export type TracePageHeaderProps = {
export const NewTracePageHeader = memo((props: TracePageHeaderProps) => {
const {
trace,
data,
app,
timeZone,
search,
setSearch,
@ -101,7 +105,7 @@ export const NewTracePageHeader = memo((props: TracePageHeaderProps) => {
<div className={styles.titleRow}>
{links && links.length > 0 && <ExternalLinks links={links} className={styles.TracePageHeaderBack} />}
{title}
<TracePageActions traceId={trace.traceID} />
<TracePageActions traceId={trace.traceID} data={data} app={app} />
</div>
<div className={styles.subtitle}>

View File

@ -9,7 +9,6 @@ import {
CSVConfig,
DataFrame,
DataTransformerID,
MutableDataFrame,
SelectableValue,
TimeZone,
transformDataFrame,
@ -22,13 +21,10 @@ import { t, Trans } from 'app/core/internationalization';
import { dataFrameToLogsModel } from 'app/core/logsModel';
import { PanelModel } from 'app/features/dashboard/state';
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
import { transformToJaeger } from 'app/plugins/datasource/jaeger/responseTransform';
import { transformToOTLP } from 'app/plugins/datasource/tempo/resultTransformer';
import { transformToZipkin } from 'app/plugins/datasource/zipkin/utils/transforms';
import { InspectDataOptions } from './InspectDataOptions';
import { getPanelInspectorStyles } from './styles';
import { downloadAsJson, downloadDataFrameAsCsv, downloadLogsModelAsTxt } from './utils/download';
import { downloadAsJson, downloadDataFrameAsCsv, downloadLogsModelAsTxt, downloadTraceAsJson } from './utils/download';
interface Props {
isLoading: boolean;
@ -124,28 +120,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
if (df.meta?.preferredVisualisationType !== 'trace') {
continue;
}
let traceFormat = 'otlp';
switch (df.meta?.custom?.traceFormat) {
case 'jaeger': {
let res = transformToJaeger(new MutableDataFrame(df));
downloadAsJson(res, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces');
traceFormat = 'jaeger';
break;
}
case 'zipkin': {
let res = transformToZipkin(new MutableDataFrame(df));
downloadAsJson(res, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces');
traceFormat = 'zipkin';
break;
}
case 'otlp':
default: {
let res = transformToOTLP(new MutableDataFrame(df));
downloadAsJson(res, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces');
break;
}
}
const traceFormat = downloadTraceAsJson(df, (panel ? panel.getDisplayTitle() : 'Explore') + '-traces');
reportInteraction('grafana_traces_download_traces_clicked', {
app,

View File

@ -7,9 +7,14 @@ import {
dateTimeFormat,
dateTimeFormatISO,
LogsModel,
MutableDataFrame,
toCSV,
} from '@grafana/data';
import { transformToJaeger } from '../../../plugins/datasource/jaeger/responseTransform';
import { transformToOTLP } from '../../../plugins/datasource/tempo/resultTransformer';
import { transformToZipkin } from '../../../plugins/datasource/zipkin/utils/transforms';
/**
* Downloads a DataFrame as a TXT file.
*
@ -77,3 +82,34 @@ export function downloadAsJson(json: unknown, title: string) {
const fileName = `${title}-${dateTimeFormat(new Date())}.json`;
saveAs(blob, fileName);
}
/**
* Downloads a trace as json, based on the DataFrame format or OTLP as a default
*
* @param {DataFrame} frame
* @param {string} title
*/
export function downloadTraceAsJson(frame: DataFrame, title: string): string {
let traceFormat = 'otlp';
switch (frame.meta?.custom?.traceFormat) {
case 'jaeger': {
let res = transformToJaeger(new MutableDataFrame(frame));
downloadAsJson(res, title);
traceFormat = 'jaeger';
break;
}
case 'zipkin': {
let res = transformToZipkin(new MutableDataFrame(frame));
downloadAsJson(res, title);
traceFormat = 'zipkin';
break;
}
case 'otlp':
default: {
let res = transformToOTLP(new MutableDataFrame(frame));
downloadAsJson(res, title);
break;
}
}
return traceFormat;
}