|
|
|
|
@ -1,10 +1,10 @@ |
|
|
|
|
import * as React from 'react'; |
|
|
|
|
|
|
|
|
|
import { CoreApp, IconName, PluginExtensionPoints, RawTimeRange, TimeRange } from '@grafana/data'; |
|
|
|
|
import { CoreApp, IconName, LinkModel, PluginExtensionPoints, RawTimeRange, TimeRange } from '@grafana/data'; |
|
|
|
|
import { TraceToProfilesOptions } from '@grafana/o11y-ds-frontend'; |
|
|
|
|
import { config, locationService, reportInteraction, usePluginLinks } from '@grafana/runtime'; |
|
|
|
|
import { DataSourceRef } from '@grafana/schema'; |
|
|
|
|
import { Button, DataLinkButton } from '@grafana/ui'; |
|
|
|
|
import { DataLinkButton, Dropdown, Menu, ToolbarButton } from '@grafana/ui'; |
|
|
|
|
import { RelatedProfilesTitle } from '@grafana-plugins/tempo/resultTransformer'; |
|
|
|
|
|
|
|
|
|
import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; |
|
|
|
|
@ -30,91 +30,122 @@ export type Props = { |
|
|
|
|
app: CoreApp; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Order in which known link types are shown in the span details |
|
|
|
|
* This was added in https://github.com/grafana/grafana/pull/101881 to preserve the order of links
|
|
|
|
|
* customers might have been used to. This will be revisted in https://github.com/grafana/grafana/issues/101925
|
|
|
|
|
*/ |
|
|
|
|
const LINKS_ORDER = [ |
|
|
|
|
SpanLinkType.Metrics, |
|
|
|
|
SpanLinkType.Logs, |
|
|
|
|
SpanLinkType.Profiles, |
|
|
|
|
SpanLinkType.ProfilesDrilldown, |
|
|
|
|
SpanLinkType.Session, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Maximum number of links to show before moving them to a dropdown |
|
|
|
|
*/ |
|
|
|
|
const MAX_LINKS = 3; |
|
|
|
|
|
|
|
|
|
export const getSpanDetailLinkButtons = (props: Props) => { |
|
|
|
|
const { span, createSpanLink, traceToProfilesOptions, timeRange, datasourceType, app } = props; |
|
|
|
|
|
|
|
|
|
let logLinkButton: JSX.Element | null = null; |
|
|
|
|
let profileLinkButton: JSX.Element | null = null; |
|
|
|
|
let sessionLinkButton: JSX.Element | null = null; |
|
|
|
|
if (createSpanLink) { |
|
|
|
|
const links = createSpanLink(span); |
|
|
|
|
const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs); |
|
|
|
|
if (links && logsLink && logsLink.length > 0) { |
|
|
|
|
logLinkButton = createLinkButton(logsLink[0], SpanLinkType.Logs, 'Logs for this span', 'gf-logs', datasourceType); |
|
|
|
|
} |
|
|
|
|
const profilesLink = links?.filter( |
|
|
|
|
(link) => link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle |
|
|
|
|
); |
|
|
|
|
if (links && profilesLink && profilesLink.length > 0) { |
|
|
|
|
profileLinkButton = createLinkButton( |
|
|
|
|
profilesLink[0], |
|
|
|
|
SpanLinkType.Profiles, |
|
|
|
|
'Profiles for this span', |
|
|
|
|
'link', |
|
|
|
|
datasourceType |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session); |
|
|
|
|
if (links && sessionLink && sessionLink.length > 0) { |
|
|
|
|
sessionLinkButton = createLinkButton( |
|
|
|
|
sessionLink[0], |
|
|
|
|
SpanLinkType.Session, |
|
|
|
|
'Session for this span', |
|
|
|
|
'frontend-observability', |
|
|
|
|
datasourceType |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
let linkToProfiles: SpanLinkDef | undefined; |
|
|
|
|
|
|
|
|
|
let profileLinkButtons = profileLinkButton; |
|
|
|
|
if (profileLinkButton) { |
|
|
|
|
// ensure we have a profile link
|
|
|
|
|
const profilesDrilldownPluginId = 'grafana-pyroscope-app'; |
|
|
|
|
const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange); |
|
|
|
|
if (createSpanLink) { |
|
|
|
|
const links = (createSpanLink(span) || []) |
|
|
|
|
// Linked spans are shown in a separate section
|
|
|
|
|
.filter((link) => link.type !== SpanLinkType.Traces) |
|
|
|
|
.map((link) => { |
|
|
|
|
if (link.type === SpanLinkType.Logs) { |
|
|
|
|
return createLinkModel(link, SpanLinkType.Logs, 'Logs for this span', 'gf-logs', datasourceType); |
|
|
|
|
} |
|
|
|
|
if (link.type === SpanLinkType.Profiles && link.title === RelatedProfilesTitle) { |
|
|
|
|
linkToProfiles = link; |
|
|
|
|
return createLinkModel(link, SpanLinkType.Profiles, 'Profiles for this span', 'link', datasourceType); |
|
|
|
|
} |
|
|
|
|
if (link.type === SpanLinkType.Session) { |
|
|
|
|
return createLinkModel( |
|
|
|
|
link, |
|
|
|
|
SpanLinkType.Session, |
|
|
|
|
'Session for this span', |
|
|
|
|
'frontend-observability', |
|
|
|
|
datasourceType |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
return createLinkModel(link, SpanLinkType.Unknown, link.title || '', 'link', datasourceType); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// if in explore, use the plugin extension point to get the link
|
|
|
|
|
// note: plugin extension point links are not currently supported in panel plugins
|
|
|
|
|
if (app === CoreApp.Explore) { |
|
|
|
|
// TODO: create SpanLinkDef in createSpanLink (https://github.com/grafana/grafana/issues/101925)
|
|
|
|
|
if (linkToProfiles && app === CoreApp.Explore) { |
|
|
|
|
// ensure we have a profile link
|
|
|
|
|
const profilesDrilldownPluginId = 'grafana-pyroscope-app'; |
|
|
|
|
const context = getProfileLinkButtonsContext(span, traceToProfilesOptions, timeRange); |
|
|
|
|
const extensionPointId = PluginExtensionPoints.TraceViewDetails; |
|
|
|
|
const { links } = usePluginLinks({ extensionPointId, context, limitPerPlugin: 1 }); |
|
|
|
|
const link = links && links.length > 0 ? links.find((link) => link.pluginId === profilesDrilldownPluginId) : null; |
|
|
|
|
const { links: pluginLinks } = usePluginLinks({ extensionPointId, context, limitPerPlugin: 1 }); |
|
|
|
|
const link = |
|
|
|
|
pluginLinks && pluginLinks.length > 0 |
|
|
|
|
? pluginLinks.find((link) => link.pluginId === profilesDrilldownPluginId) |
|
|
|
|
: null; |
|
|
|
|
const label = 'Open in Profiles Drilldown'; |
|
|
|
|
const appLink: SpanLinkDef = { |
|
|
|
|
...linkToProfiles, |
|
|
|
|
href: '', |
|
|
|
|
onClick: () => { |
|
|
|
|
link?.onClick?.(); |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
links.push(createLinkModel(appLink, SpanLinkType.ProfilesDrilldown, label, 'link', datasourceType)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// if we have a plugin link, add a button to open in Grafana Profiles Drilldown
|
|
|
|
|
if (link) { |
|
|
|
|
const profileDrilldownLinkButton = ( |
|
|
|
|
<Button |
|
|
|
|
icon="link" |
|
|
|
|
variant="primary" |
|
|
|
|
size="sm" |
|
|
|
|
onClick={() => { |
|
|
|
|
if (link && link.onClick) { |
|
|
|
|
reportInteraction('grafana_traces_trace_view_span_link_clicked', { |
|
|
|
|
datasourceType, |
|
|
|
|
grafana_version: config.buildInfo.version, |
|
|
|
|
type: SpanLinkType.ProfilesDrilldown, |
|
|
|
|
location: 'spanDetails', |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
link.onClick(); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
{label} |
|
|
|
|
</Button> |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
profileLinkButtons = ( |
|
|
|
|
<> |
|
|
|
|
{profileLinkButton} |
|
|
|
|
{profileDrilldownLinkButton} |
|
|
|
|
</> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
links.sort((a, b) => { |
|
|
|
|
const aIndex = LINKS_ORDER.indexOf(a.type); |
|
|
|
|
const bIndex = LINKS_ORDER.indexOf(b.type); |
|
|
|
|
const aValue = aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex; |
|
|
|
|
const bValue = bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex; |
|
|
|
|
return aValue - bValue; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (links.length > MAX_LINKS) { |
|
|
|
|
return <DropDownMenu links={links}></DropDownMenu>; |
|
|
|
|
} else { |
|
|
|
|
return ( |
|
|
|
|
<> |
|
|
|
|
{links.map(({ linkModel, icon, className }, index) => ( |
|
|
|
|
<DataLinkButton key={index} link={linkModel} buttonProps={{ icon, className }}></DataLinkButton> |
|
|
|
|
))} |
|
|
|
|
</> |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return { profileLinkButtons, logLinkButton, sessionLinkButton }; |
|
|
|
|
return <></>; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const DropDownMenu = ({ links }: { links: SpanLinkModel[] }) => { |
|
|
|
|
const [isOpen, setIsOpen] = React.useState(false); |
|
|
|
|
const menu = ( |
|
|
|
|
<Menu> |
|
|
|
|
{links.map(({ linkModel }, index) => ( |
|
|
|
|
<Menu.Item |
|
|
|
|
key={index} |
|
|
|
|
label={linkModel.title} |
|
|
|
|
onClick={(event: React.MouseEvent) => linkModel.onClick?.(event)} |
|
|
|
|
/> |
|
|
|
|
))} |
|
|
|
|
</Menu> |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<Dropdown overlay={menu} placement="bottom-start" onVisibleChange={setIsOpen}> |
|
|
|
|
<ToolbarButton variant="primary" icon="link" isOpen={isOpen} aria-label="Links"> |
|
|
|
|
Links |
|
|
|
|
</ToolbarButton> |
|
|
|
|
</Dropdown> |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export const getProfileLinkButtonsContext = ( |
|
|
|
|
@ -137,40 +168,47 @@ export const getProfileLinkButtonsContext = ( |
|
|
|
|
return context; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const createLinkButton = ( |
|
|
|
|
type SpanLinkModel = { |
|
|
|
|
linkModel: LinkModel; |
|
|
|
|
icon: IconName; |
|
|
|
|
className?: string; |
|
|
|
|
type: SpanLinkType; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const createLinkModel = ( |
|
|
|
|
link: SpanLinkDef, |
|
|
|
|
type: SpanLinkType, |
|
|
|
|
title: string, |
|
|
|
|
icon: IconName, |
|
|
|
|
datasourceType: string, |
|
|
|
|
className?: string |
|
|
|
|
) => { |
|
|
|
|
return ( |
|
|
|
|
<DataLinkButton |
|
|
|
|
link={{ |
|
|
|
|
...link, |
|
|
|
|
title: title, |
|
|
|
|
target: '_blank', |
|
|
|
|
origin: link.field, |
|
|
|
|
onClick: (event: React.MouseEvent) => { |
|
|
|
|
// DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation
|
|
|
|
|
// In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking
|
|
|
|
|
// this interaction will not be tracked with link right clicks
|
|
|
|
|
reportInteraction('grafana_traces_trace_view_span_link_clicked', { |
|
|
|
|
datasourceType, |
|
|
|
|
grafana_version: config.buildInfo.version, |
|
|
|
|
type, |
|
|
|
|
location: 'spanDetails', |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (link.onClick) { |
|
|
|
|
link.onClick?.(event); |
|
|
|
|
} else { |
|
|
|
|
locationService.push(link.href); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
}} |
|
|
|
|
buttonProps={{ icon, className }} |
|
|
|
|
/> |
|
|
|
|
); |
|
|
|
|
): SpanLinkModel => { |
|
|
|
|
return { |
|
|
|
|
icon, |
|
|
|
|
className, |
|
|
|
|
type, |
|
|
|
|
linkModel: { |
|
|
|
|
...link, |
|
|
|
|
title: title, |
|
|
|
|
target: '_blank', |
|
|
|
|
origin: link.field, |
|
|
|
|
onClick: (event: React.MouseEvent) => { |
|
|
|
|
// DataLinkButton assumes if you provide an onClick event you would want to prevent default behavior like navigation
|
|
|
|
|
// In this case, if an onClick is not defined, restore navigation to the provided href while keeping the tracking
|
|
|
|
|
// this interaction will not be tracked with link right clicks
|
|
|
|
|
reportInteraction('grafana_traces_trace_view_span_link_clicked', { |
|
|
|
|
datasourceType, |
|
|
|
|
grafana_version: config.buildInfo.version, |
|
|
|
|
type, |
|
|
|
|
location: 'spanDetails', |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (link.onClick) { |
|
|
|
|
link.onClick?.(event); |
|
|
|
|
} else { |
|
|
|
|
locationService.push(link.href); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
}; |
|
|
|
|
|