2023-10-09 14:22:39 +01:00
import { groupBy , identity , pick , pickBy , startCase } from 'lodash' ;
2022-09-02 11:17:36 +01:00
import { EMPTY , from , lastValueFrom , merge , Observable , of , throwError } from 'rxjs' ;
2022-06-09 17:56:15 +01:00
import { catchError , concatMap , map , mergeMap , toArray } from 'rxjs/operators' ;
2023-09-06 09:40:37 +02:00
import semver from 'semver' ;
2022-04-22 14:33:13 +01:00
2020-10-13 19:12:49 +02:00
import {
2023-05-15 14:27:27 +01:00
CoreApp ,
DataFrame ,
2023-10-11 18:04:54 +02:00
DataFrameDTO ,
2020-10-13 19:12:49 +02:00
DataQueryRequest ,
DataQueryResponse ,
2022-06-09 17:56:15 +01:00
DataQueryResponseData ,
2021-05-10 17:12:19 +02:00
DataSourceApi ,
2021-03-05 14:28:17 +01:00
DataSourceInstanceSettings ,
2022-11-03 11:01:22 +00:00
dateTime ,
2022-06-09 17:56:15 +01:00
FieldType ,
2021-09-28 10:52:57 -06:00
isValidGoDuration ,
2021-08-05 15:13:44 +02:00
LoadingState ,
2022-10-21 20:08:10 +05:30
rangeUtil ,
2022-05-03 17:42:36 +01:00
ScopedVars ,
2023-11-06 11:28:44 +00:00
TestDataSourceResponse ,
2023-11-22 13:15:29 +01:00
urlUtil ,
2020-10-13 19:12:49 +02:00
} from '@grafana/data' ;
2024-01-23 08:35:34 +01:00
import { NodeGraphOptions , SpanBarOptions , TraceToLogsOptions } from '@grafana/o11y-ds-frontend' ;
2022-05-03 17:42:36 +01:00
import {
BackendSrvRequest ,
2023-10-09 14:22:39 +01:00
config ,
2022-05-03 17:42:36 +01:00
DataSourceWithBackend ,
getBackendSrv ,
2024-01-16 11:36:40 +01:00
getDataSourceSrv ,
2023-10-09 14:22:39 +01:00
getTemplateSrv ,
2022-06-01 10:28:45 +01:00
reportInteraction ,
2022-05-03 17:42:36 +01:00
TemplateSrv ,
} from '@grafana/runtime' ;
2023-06-19 13:28:56 +01:00
import { BarGaugeDisplayMode , TableCellDisplayMode , VariableFormatID } from '@grafana/schema' ;
2022-04-22 14:33:13 +01:00
2023-03-06 16:31:08 +00:00
import { generateQueryFromFilters } from './SearchTraceQLEditor/utils' ;
2023-08-30 13:45:39 +02:00
import { TempoVariableQuery , TempoVariableQueryType } from './VariableQueryEditor' ;
2024-01-16 11:36:40 +01:00
import { LokiOptions } from './_importedDependencies/datasources/loki/types' ;
import { PromQuery , PrometheusDatasource } from './_importedDependencies/datasources/prometheus/types' ;
2023-03-31 10:35:37 +01:00
import { TraceqlFilter , TraceqlSearchScope } from './dataquery.gen' ;
2022-02-01 10:41:14 -07:00
import {
2023-10-09 14:22:39 +01:00
defaultTableFilter ,
durationMetric ,
errorRateMetric ,
2022-02-01 10:41:14 -07:00
failedMetric ,
histogramMetric ,
mapPromMetricsToServiceMap ,
2023-10-09 14:22:39 +01:00
rateMetric ,
2022-02-01 10:41:14 -07:00
serviceMapMetrics ,
totalsMetric ,
} from './graphTransform' ;
2022-08-24 17:57:59 +01:00
import TempoLanguageProvider from './language_provider' ;
2023-08-28 15:02:12 +01:00
import { createTableFrameFromMetricsSummaryQuery , emptyResponse , MetricsSummary } from './metricsSummary' ;
2021-08-19 09:15:46 -06:00
import {
2023-10-09 14:22:39 +01:00
createTableFrameFromSearch ,
transformFromOTLP as transformFromOTEL ,
2021-08-19 09:15:46 -06:00
transformTrace ,
transformTraceList ,
2023-09-29 18:34:39 +01:00
formatTraceQLResponse ,
2021-08-19 09:15:46 -06:00
} from './resultTransformer' ;
2023-07-14 15:10:46 +01:00
import { doTempoChannelStream } from './streaming' ;
2023-10-09 14:22:39 +01:00
import { SearchQueryParams , TempoJsonData , TempoQuery } from './types' ;
2023-06-28 10:34:11 +01:00
import { getErrorMessage } from './utils' ;
2023-08-30 13:45:39 +02:00
import { TempoVariableSupport } from './variables' ;
2022-01-10 07:38:40 -07:00
2021-10-06 08:43:13 -06:00
export const DEFAULT_LIMIT = 20 ;
2023-09-14 10:49:18 +01:00
export const DEFAULT_SPSS = 3 ; // spans per span set
2021-10-06 08:43:13 -06:00
2023-09-06 09:40:37 +02:00
enum FeatureName {
streaming = 'streaming' ,
}
/ * M a p , f o r e a c h f e a t u r e ( e . g . , s t r e a m i n g ) , t h e m i n i m u m T e m p o v e r s i o n r e q u i r e d t o h a v e t h a t
* * feature available . If the running Tempo instance on the user ' s backend is older than the
* * target version , the feature is disabled in Grafana ( frontend ) .
* /
const featuresToTempoVersion = {
[ FeatureName . streaming ] : '2.2.0' ,
} ;
// The version that we use as default in case we cannot retrieve it from the backend.
// This is the last minor version of Tempo that does not expose the endpoint for build information.
const defaultTempoVersion = '2.1.0' ;
2023-10-11 18:04:54 +02:00
interface ServiceMapQueryResponse {
nodes : DataFrame ;
edges : DataFrame ;
}
interface ServiceMapQueryResponseWithRates {
rates : Array < DataFrame | DataFrameDTO > ;
nodes : DataFrame ;
edges : DataFrame ;
}
2021-08-17 15:48:29 +02:00
export class TempoDatasource extends DataSourceWithBackend < TempoQuery , TempoJsonData > {
2021-06-02 16:37:36 +02:00
tracesToLogs? : TraceToLogsOptions ;
2021-08-17 15:48:29 +02:00
serviceMap ? : {
datasourceUid? : string ;
} ;
2021-09-27 07:22:49 -06:00
search ? : {
hide? : boolean ;
2023-03-31 10:35:37 +01:00
filters? : TraceqlFilter [ ] ;
2021-09-27 07:22:49 -06:00
} ;
2021-10-06 13:39:14 -06:00
nodeGraph? : NodeGraphOptions ;
2022-03-17 11:23:15 -06:00
lokiSearch ? : {
datasourceUid? : string ;
} ;
2022-10-21 20:08:10 +05:30
traceQuery ? : {
2022-11-03 11:01:22 +00:00
timeShiftEnabled? : boolean ;
2022-10-21 20:08:10 +05:30
spanStartTimeShift? : string ;
spanEndTimeShift? : string ;
} ;
2021-08-05 15:13:44 +02:00
uploadedJson? : string | ArrayBuffer | null = null ;
2022-07-06 08:14:03 +01:00
spanBar? : SpanBarOptions ;
2022-08-24 17:57:59 +01:00
languageProvider : TempoLanguageProvider ;
2021-06-02 16:37:36 +02:00
2023-09-06 09:40:37 +02:00
// The version of Tempo running on the backend. `null` if we cannot retrieve it for whatever reason
tempoVersion? : string | null ;
2022-05-03 17:42:36 +01:00
constructor (
private instanceSettings : DataSourceInstanceSettings < TempoJsonData > ,
private readonly templateSrv : TemplateSrv = getTemplateSrv ( )
) {
2020-10-13 19:12:49 +02:00
super ( instanceSettings ) ;
2024-01-16 11:36:40 +01:00
2021-06-02 16:37:36 +02:00
this . tracesToLogs = instanceSettings . jsonData . tracesToLogs ;
2021-08-17 15:48:29 +02:00
this . serviceMap = instanceSettings . jsonData . serviceMap ;
2021-09-27 07:22:49 -06:00
this . search = instanceSettings . jsonData . search ;
2021-10-06 13:39:14 -06:00
this . nodeGraph = instanceSettings . jsonData . nodeGraph ;
2022-03-17 11:23:15 -06:00
this . lokiSearch = instanceSettings . jsonData . lokiSearch ;
2022-10-21 20:08:10 +05:30
this . traceQuery = instanceSettings . jsonData . traceQuery ;
2022-08-24 17:57:59 +01:00
this . languageProvider = new TempoLanguageProvider ( this ) ;
2023-07-14 15:10:46 +01:00
2023-03-31 10:35:37 +01:00
if ( ! this . search ? . filters ) {
this . search = {
. . . this . search ,
filters : [
{
id : 'service-name' ,
tag : 'service.name' ,
operator : '=' ,
scope : TraceqlSearchScope.Resource ,
} ,
{ id : 'span-name' , tag : 'name' , operator : '=' , scope : TraceqlSearchScope.Span } ,
] ,
} ;
}
2023-08-30 13:45:39 +02:00
this . variables = new TempoVariableSupport ( this ) ;
}
async executeVariableQuery ( query : TempoVariableQuery ) {
// Avoid failing if the user did not select the query type (label names, label values, etc.)
if ( query . type === undefined ) {
return new Promise < Array < { text : string } > > ( ( ) = > [ ] ) ;
}
switch ( query . type ) {
case TempoVariableQueryType . LabelNames : {
return await this . labelNamesQuery ( ) ;
}
case TempoVariableQueryType . LabelValues : {
return this . labelValuesQuery ( query . label ) ;
}
default : {
2024-01-16 11:36:40 +01:00
throw Error ( 'Invalid query type: ' + query . type ) ;
2023-08-30 13:45:39 +02:00
}
}
}
async labelNamesQuery ( ) : Promise < Array < { text : string } > > {
await this . languageProvider . fetchTags ( ) ;
const tags = this . languageProvider . getAutocompleteTags ( ) ;
2023-10-06 11:48:15 +01:00
return tags . filter ( ( tag ) = > tag !== undefined ) . map ( ( tag ) = > ( { text : tag } ) ) ;
2023-08-30 13:45:39 +02:00
}
async labelValuesQuery ( labelName? : string ) : Promise < Array < { text : string } > > {
if ( ! labelName ) {
return [ ] ;
}
let options ;
try {
// Retrieve the scope of the tag
// Example: given `http.status_code`, we want scope `span`
// Note that we ignore possible name clashes, e.g., `http.status_code` in both `span` and `resource`
const scope : string | undefined = ( this . languageProvider . tagsV2 || [ ] )
// flatten the Scope objects
. flatMap ( ( tagV2 ) = > tagV2 . tags . map ( ( tag ) = > ( { scope : tagV2.name , name : tag } ) ) )
// find associated scope
. find ( ( tag ) = > tag . name === labelName ) ? . scope ;
if ( ! scope ) {
throw Error ( ` Scope for tag ${ labelName } not found ` ) ;
}
// For V2, we need to send scope and tag name, e.g. `span.http.status_code`,
// unless the tag has intrinsic scope
const scopeAndTag = scope === 'intrinsic' ? labelName : ` ${ scope } . ${ labelName } ` ;
options = await this . languageProvider . getOptionsV2 ( scopeAndTag ) ;
} catch {
// For V1, the tag name (e.g. `http.status_code`) is enough
options = await this . languageProvider . getOptionsV1 ( labelName ) ;
}
return options . filter ( ( option ) = > option . value !== undefined ) . map ( ( option ) = > ( { text : option.value } ) ) as Array < {
text : string ;
} > ;
2020-10-13 19:12:49 +02:00
}
2023-09-06 09:40:37 +02:00
init = async ( ) = > {
const response = await lastValueFrom (
this . _request ( '/api/status/buildinfo' ) . pipe (
map ( ( response ) = > response ) ,
catchError ( ( error ) = > {
console . error ( 'Failure in retrieving build information' , error . data . message ) ;
return of ( { error , data : { version : null } } ) ; // unknown version
} )
)
) ;
this . tempoVersion = response . data . version ;
} ;
/ * *
* Check , for the given feature , whether it is available in Grafana .
*
* The check is done based on the version of the Tempo instance running on the backend and
* the minimum version required by the given feature to work .
*
* @param featureName - the name of the feature to consider
* @return true if the feature is available , false otherwise
* /
private isFeatureAvailable ( featureName : FeatureName ) {
// We know for old Tempo instances we don't know their version, so resort to default
const actualVersion = this . tempoVersion ? ? defaultTempoVersion ;
try {
return semver . gte ( actualVersion , featuresToTempoVersion [ featureName ] ) ;
} catch {
2023-09-11 14:34:05 +02:00
// We assume we are on a development and recent branch, thus we enable all features
return true ;
2023-09-06 09:40:37 +02:00
}
}
2020-10-13 19:12:49 +02:00
query ( options : DataQueryRequest < TempoQuery > ) : Observable < DataQueryResponse > {
2021-05-10 17:12:19 +02:00
const subQueries : Array < Observable < DataQueryResponse > > = [ ] ;
const filteredTargets = options . targets . filter ( ( target ) = > ! target . hide ) ;
2022-12-13 13:27:45 +00:00
const targets : { [ type : string ] : TempoQuery [ ] } = groupBy ( filteredTargets , ( t ) = > t . queryType || 'traceql' ) ;
2021-04-06 18:35:00 +02:00
2022-01-28 07:49:43 -07:00
if ( targets . clear ) {
return of ( { data : [ ] , state : LoadingState.Done } ) ;
}
2022-03-17 11:23:15 -06:00
const logsDatasourceUid = this . getLokiSearchDS ( ) ;
2021-05-10 17:12:19 +02:00
// Run search queries on linked datasource
2022-03-17 11:23:15 -06:00
if ( logsDatasourceUid && targets . search ? . length > 0 ) {
2022-07-11 10:21:24 +01:00
reportInteraction ( 'grafana_traces_loki_search_queried' , {
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
2023-01-27 13:33:27 +00:00
grafana_version : config.buildInfo.version ,
hasLinkedQueryExpr :
targets . search [ 0 ] . linkedQuery ? . expr && targets . search [ 0 ] . linkedQuery ? . expr !== '' ? true : false ,
2022-07-11 10:21:24 +01:00
} ) ;
2024-01-16 11:36:40 +01:00
const dsSrv = getDataSourceSrv ( ) ;
2021-06-02 16:37:36 +02:00
subQueries . push (
2022-03-17 11:23:15 -06:00
from ( dsSrv . get ( logsDatasourceUid ) ) . pipe (
2021-06-02 16:37:36 +02:00
mergeMap ( ( linkedDatasource : DataSourceApi ) = > {
// Wrap linked query into a data request based on original request
2021-08-17 15:48:29 +02:00
const linkedRequest : DataQueryRequest = { . . . options , targets : targets.search.map ( ( t ) = > t . linkedQuery ! ) } ;
2021-06-02 16:37:36 +02:00
// Find trace matchers in derived fields of the linked datasource that's identical to this datasource
const settings : DataSourceInstanceSettings < LokiOptions > = ( linkedDatasource as any ) . instanceSettings ;
const traceLinkMatcher : string [ ] =
settings . jsonData . derivedFields
? . filter ( ( field ) = > field . datasourceUid === this . uid && field . matcherRegex )
. map ( ( field ) = > field . matcherRegex ) || [ ] ;
2022-04-04 13:24:33 -07:00
2021-06-02 16:37:36 +02:00
if ( ! traceLinkMatcher || traceLinkMatcher . length === 0 ) {
return throwError (
2021-09-08 07:04:27 -06:00
( ) = >
new Error (
'No Loki datasource configured for search. Set up Derived Fields for traces in a Loki datasource settings and link it to this Tempo datasource.'
)
2021-06-02 16:37:36 +02:00
) ;
} else {
return ( linkedDatasource . query ( linkedRequest ) as Observable < DataQueryResponse > ) . pipe (
map ( ( response ) = >
response . error ? response : transformTraceList ( response , this . uid , this . name , traceLinkMatcher )
)
) ;
}
} )
)
) ;
2021-05-10 17:12:19 +02:00
}
2021-04-06 18:35:00 +02:00
2021-08-19 09:15:46 -06:00
if ( targets . nativeSearch ? . length ) {
2021-09-28 10:52:57 -06:00
try {
2022-06-01 10:28:45 +01:00
reportInteraction ( 'grafana_traces_search_queried' , {
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
2023-01-27 13:33:27 +00:00
grafana_version : config.buildInfo.version ,
hasServiceName : targets.nativeSearch [ 0 ] . serviceName ? true : false ,
hasSpanName : targets.nativeSearch [ 0 ] . spanName ? true : false ,
2022-07-11 10:21:24 +01:00
resultLimit : targets.nativeSearch [ 0 ] . limit ? ? '' ,
2023-01-27 13:33:27 +00:00
hasSearch : targets.nativeSearch [ 0 ] . search ? true : false ,
minDuration : targets.nativeSearch [ 0 ] . minDuration ? ? '' ,
maxDuration : targets.nativeSearch [ 0 ] . maxDuration ? ? '' ,
2022-06-01 10:28:45 +01:00
} ) ;
2022-06-01 11:32:10 -06:00
const timeRange = { startTime : options.range.from.unix ( ) , endTime : options.range.to.unix ( ) } ;
2022-05-03 17:42:36 +01:00
const query = this . applyVariables ( targets . nativeSearch [ 0 ] , options . scopedVars ) ;
const searchQuery = this . buildSearchQuery ( query , timeRange ) ;
2021-09-28 10:52:57 -06:00
subQueries . push (
this . _request ( '/api/search' , searchQuery ) . pipe (
map ( ( response ) = > {
return {
data : [ createTableFrameFromSearch ( response . data . traces , this . instanceSettings ) ] ,
} ;
} ) ,
2023-06-28 10:34:11 +01:00
catchError ( ( err ) = > {
return of ( { error : { message : getErrorMessage ( err . data . message ) } , data : [ ] } ) ;
2021-09-28 10:52:57 -06:00
} )
)
) ;
} catch ( error ) {
2022-06-15 08:59:29 +01:00
return of ( { error : { message : error instanceof Error ? error . message : 'Unknown error occurred' } , data : [ ] } ) ;
2022-09-09 19:00:35 +01:00
}
}
2023-08-28 15:02:12 +01:00
2022-09-09 19:00:35 +01:00
if ( targets . traceql ? . length ) {
try {
2023-01-02 14:23:50 +00:00
const appliedQuery = this . applyVariables ( targets . traceql [ 0 ] , options . scopedVars ) ;
const queryValue = appliedQuery ? . query || '' ;
2022-12-13 13:27:45 +00:00
const hexOnlyRegex = /^[0-9A-Fa-f]*$/ ;
// Check whether this is a trace ID or traceQL query by checking if it only contains hex characters
if ( queryValue . trim ( ) . match ( hexOnlyRegex ) ) {
// There's only hex characters so let's assume that this is a trace ID
2023-01-03 10:43:11 +00:00
reportInteraction ( 'grafana_traces_traceID_queried' , {
2022-12-13 13:27:45 +00:00
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
2023-01-27 13:33:27 +00:00
grafana_version : config.buildInfo.version ,
hasQuery : queryValue !== '' ? true : false ,
2022-12-13 13:27:45 +00:00
} ) ;
2022-09-09 19:00:35 +01:00
2022-12-13 13:27:45 +00:00
subQueries . push ( this . handleTraceIdQuery ( options , targets . traceql ) ) ;
} else {
reportInteraction ( 'grafana_traces_traceql_queried' , {
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
2023-01-27 13:33:27 +00:00
grafana_version : config.buildInfo.version ,
2022-12-13 13:27:45 +00:00
query : queryValue ? ? '' ,
2023-07-26 14:33:16 +01:00
streaming : config.featureToggles.traceQLStreaming ,
2022-12-13 13:27:45 +00:00
} ) ;
2023-07-14 15:10:46 +01:00
2023-09-06 09:40:37 +02:00
if ( config . featureToggles . traceQLStreaming && this . isFeatureAvailable ( FeatureName . streaming ) ) {
2023-08-24 14:10:14 +02:00
subQueries . push ( this . handleStreamingSearch ( options , targets . traceql , queryValue ) ) ;
2023-07-14 15:10:46 +01:00
} else {
subQueries . push (
this . _request ( '/api/search' , {
q : queryValue ,
limit : options.targets [ 0 ] . limit ? ? DEFAULT_LIMIT ,
2023-09-14 10:49:18 +01:00
spss : options.targets [ 0 ] . spss ? ? DEFAULT_SPSS ,
2023-07-14 15:10:46 +01:00
start : options.range.from.unix ( ) ,
end : options.range.to.unix ( ) ,
} ) . pipe (
map ( ( response ) = > {
return {
2023-09-29 18:34:39 +01:00
data : formatTraceQLResponse (
response . data . traces ,
this . instanceSettings ,
targets . traceql [ 0 ] . tableType
) ,
2023-07-14 15:10:46 +01:00
} ;
} ) ,
catchError ( ( err ) = > {
return of ( { error : { message : getErrorMessage ( err . data . message ) } , data : [ ] } ) ;
} )
)
) ;
}
}
} catch ( error ) {
return of ( { error : { message : error instanceof Error ? error . message : 'Unknown error occurred' } , data : [ ] } ) ;
}
}
2023-08-28 15:02:12 +01:00
2023-07-14 15:10:46 +01:00
if ( targets . traceqlSearch ? . length ) {
try {
2023-08-28 15:02:12 +01:00
if ( config . featureToggles . metricsSummary ) {
const groupBy = targets . traceqlSearch . find ( ( t ) = > this . hasGroupBy ( t ) ) ;
if ( groupBy ) {
subQueries . push ( this . handleMetricsSummary ( groupBy , generateQueryFromFilters ( groupBy . filters ) , options ) ) ;
}
}
2023-08-24 13:06:46 +02:00
2023-08-28 15:02:12 +01:00
const traceqlSearchTargets = config . featureToggles . metricsSummary
? targets . traceqlSearch . filter ( ( t ) = > ! this . hasGroupBy ( t ) )
: targets . traceqlSearch ;
if ( traceqlSearchTargets . length > 0 ) {
const queryValueFromFilters = generateQueryFromFilters ( traceqlSearchTargets [ 0 ] . filters ) ;
2023-08-24 13:06:46 +02:00
2023-08-28 15:02:12 +01:00
// We want to support template variables also in Search for consistency with other data sources
const queryValue = this . templateSrv . replace ( queryValueFromFilters , options . scopedVars ) ;
2023-07-14 15:10:46 +01:00
2023-08-28 15:02:12 +01:00
reportInteraction ( 'grafana_traces_traceql_search_queried' , {
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
grafana_version : config.buildInfo.version ,
query : queryValue ? ? '' ,
streaming : config.featureToggles.traceQLStreaming ,
} ) ;
2023-09-06 09:40:37 +02:00
if ( config . featureToggles . traceQLStreaming && this . isFeatureAvailable ( FeatureName . streaming ) ) {
2023-08-28 15:02:12 +01:00
subQueries . push ( this . handleStreamingSearch ( options , traceqlSearchTargets , queryValue ) ) ;
} else {
subQueries . push (
this . _request ( '/api/search' , {
q : queryValue ,
limit : options.targets [ 0 ] . limit ? ? DEFAULT_LIMIT ,
2023-09-14 10:49:18 +01:00
spss : options.targets [ 0 ] . spss ? ? DEFAULT_SPSS ,
2023-08-28 15:02:12 +01:00
start : options.range.from.unix ( ) ,
end : options.range.to.unix ( ) ,
} ) . pipe (
map ( ( response ) = > {
return {
2023-09-29 18:34:39 +01:00
data : formatTraceQLResponse (
response . data . traces ,
this . instanceSettings ,
targets . traceqlSearch [ 0 ] . tableType
) ,
2023-08-28 15:02:12 +01:00
} ;
} ) ,
catchError ( ( err ) = > {
return of ( { error : { message : getErrorMessage ( err . data . message ) } , data : [ ] } ) ;
} )
)
) ;
}
2022-12-13 13:27:45 +00:00
}
2022-09-09 19:00:35 +01:00
} catch ( error ) {
return of ( { error : { message : error instanceof Error ? error . message : 'Unknown error occurred' } , data : [ ] } ) ;
2021-09-28 10:52:57 -06:00
}
2021-08-19 09:15:46 -06:00
}
2021-08-17 15:48:29 +02:00
if ( targets . upload ? . length ) {
2021-08-05 15:13:44 +02:00
if ( this . uploadedJson ) {
2022-07-11 10:21:24 +01:00
reportInteraction ( 'grafana_traces_json_file_uploaded' , {
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
2023-01-27 13:33:27 +00:00
grafana_version : config.buildInfo.version ,
2022-07-11 10:21:24 +01:00
} ) ;
2022-06-07 06:37:19 -06:00
const jsonData = JSON . parse ( this . uploadedJson as string ) ;
const isTraceData = jsonData . batches ;
const isServiceGraphData =
Array . isArray ( jsonData ) && jsonData . some ( ( df ) = > df ? . meta ? . preferredVisualisationType === 'nodeGraph' ) ;
if ( isTraceData ) {
subQueries . push ( of ( transformFromOTEL ( jsonData . batches , this . nodeGraph ? . enabled ) ) ) ;
} else if ( isServiceGraphData ) {
subQueries . push ( of ( { data : jsonData , state : LoadingState.Done } ) ) ;
2021-08-05 15:13:44 +02:00
} else {
2022-06-07 06:37:19 -06:00
subQueries . push ( of ( { error : { message : 'Unable to parse uploaded data.' } , data : [ ] } ) ) ;
2021-08-05 15:13:44 +02:00
}
} else {
subQueries . push ( of ( { data : [ ] , state : LoadingState.Done } ) ) ;
}
}
2021-08-17 15:48:29 +02:00
if ( this . serviceMap ? . datasourceUid && targets . serviceMap ? . length > 0 ) {
2022-07-11 10:21:24 +01:00
reportInteraction ( 'grafana_traces_service_graph_queried' , {
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
2023-01-27 13:33:27 +00:00
grafana_version : config.buildInfo.version ,
hasServiceMapQuery : targets.serviceMap [ 0 ] . serviceMapQuery ? true : false ,
2022-07-11 10:21:24 +01:00
} ) ;
2022-06-09 17:56:15 +01:00
const dsId = this . serviceMap . datasourceUid ;
2022-07-18 15:36:16 +01:00
const tempoDsUid = this . uid ;
2023-02-01 15:56:34 +00:00
subQueries . push (
serviceMapQuery ( options , dsId , tempoDsUid ) . pipe (
concatMap ( ( result ) = >
rateQuery ( options , result , dsId ) . pipe (
concatMap ( ( result ) = > errorAndDurationQuery ( options , result , dsId , tempoDsUid ) )
2022-06-09 17:56:15 +01:00
)
)
2023-02-01 15:56:34 +00:00
)
) ;
2021-08-17 15:48:29 +02:00
}
2021-05-10 17:12:19 +02:00
return merge ( . . . subQueries ) ;
2020-10-13 19:12:49 +02:00
}
2023-11-06 11:28:44 +00:00
applyTemplateVariables ( query : TempoQuery , scopedVars : ScopedVars ) {
2022-05-03 17:42:36 +01:00
return this . applyVariables ( query , scopedVars ) ;
}
interpolateVariablesInQueries ( queries : TempoQuery [ ] , scopedVars : ScopedVars ) : TempoQuery [ ] {
if ( ! queries || queries . length === 0 ) {
return [ ] ;
}
return queries . map ( ( query ) = > {
return {
. . . query ,
datasource : this.getRef ( ) ,
. . . this . applyVariables ( query , scopedVars ) ,
} ;
} ) ;
}
applyVariables ( query : TempoQuery , scopedVars : ScopedVars ) {
const expandedQuery = { . . . query } ;
if ( query . linkedQuery ) {
expandedQuery . linkedQuery = {
. . . query . linkedQuery ,
expr : this.templateSrv.replace ( query . linkedQuery ? . expr ? ? '' , scopedVars ) ,
} ;
}
return {
. . . expandedQuery ,
2023-06-19 13:28:56 +01:00
query : this.templateSrv.replace ( query . query ? ? '' , scopedVars , VariableFormatID . Pipe ) ,
2022-07-18 08:08:35 +01:00
serviceName : this.templateSrv.replace ( query . serviceName ? ? '' , scopedVars ) ,
spanName : this.templateSrv.replace ( query . spanName ? ? '' , scopedVars ) ,
2022-05-03 17:42:36 +01:00
search : this.templateSrv.replace ( query . search ? ? '' , scopedVars ) ,
minDuration : this.templateSrv.replace ( query . minDuration ? ? '' , scopedVars ) ,
maxDuration : this.templateSrv.replace ( query . maxDuration ? ? '' , scopedVars ) ,
2024-01-26 16:37:49 +02:00
serviceMapQuery : Array.isArray ( query . serviceMapQuery )
? query . serviceMapQuery . map ( ( query ) = > this . templateSrv . replace ( query , scopedVars ) )
: this . templateSrv . replace ( query . serviceMapQuery ? ? '' , scopedVars ) ,
2022-05-03 17:42:36 +01:00
} ;
}
2023-08-28 15:02:12 +01:00
handleMetricsSummary = ( target : TempoQuery , query : string , options : DataQueryRequest < TempoQuery > ) = > {
reportInteraction ( 'grafana_traces_metrics_summary_queried' , {
datasourceType : 'tempo' ,
app : options.app ? ? '' ,
grafana_version : config.buildInfo.version ,
filterCount : target.groupBy?.length ? ? 0 ,
} ) ;
if ( query === '{}' ) {
return of ( {
error : {
message :
'Please ensure you do not have an empty query. This is so filters are applied and the metrics summary is not generated from all spans.' ,
} ,
data : emptyResponse ,
} ) ;
}
const groupBy = target . groupBy ? this . formatGroupBy ( target . groupBy ) : '' ;
return this . _request ( '/api/metrics/summary' , {
q : query ,
groupBy ,
start : options.range.from.unix ( ) ,
end : options.range.to.unix ( ) ,
} ) . pipe (
map ( ( response ) = > {
if ( ! response . data . summaries ) {
return {
error : {
message : getErrorMessage (
` No summary data for ' ${ groupBy } '. Note: the metrics summary API only considers spans of kind = server. You can check if the attributes exist by running a TraceQL query like { attr_key = attr_value && kind = server } `
) ,
} ,
data : emptyResponse ,
} ;
}
// Check if any of the results have series data as older versions of Tempo placed the series data in a different structure
const hasSeries = response . data . summaries . some ( ( summary : MetricsSummary ) = > summary . series . length > 0 ) ;
if ( ! hasSeries ) {
return {
error : {
message : getErrorMessage ( ` No series data. Ensure you are using an up to date version of Tempo ` ) ,
} ,
data : emptyResponse ,
} ;
}
return {
data : createTableFrameFromMetricsSummaryQuery ( response . data . summaries , query , this . instanceSettings ) ,
} ;
} ) ,
catchError ( ( error ) = > {
return of ( {
error : { message : getErrorMessage ( error . data . message ) } ,
data : emptyResponse ,
} ) ;
} )
) ;
} ;
formatGroupBy = ( groupBy : TraceqlFilter [ ] ) = > {
return groupBy
? . filter ( ( f ) = > f . tag )
. map ( ( f ) = > {
if ( f . scope === TraceqlSearchScope . Unscoped ) {
return ` . ${ f . tag } ` ;
}
return f . scope !== TraceqlSearchScope . Intrinsic ? ` ${ f . scope } . ${ f . tag } ` : f . tag ;
} )
. join ( ', ' ) ;
} ;
hasGroupBy = ( query : TempoQuery ) = > {
return query . groupBy ? . find ( ( gb ) = > gb . tag ) ;
} ;
2022-01-05 19:34:09 +01:00
/ * *
* Handles the simplest of the queries where we have just a trace id and return trace data for it .
* @param options
* @param targets
* @private
* /
2022-10-21 20:08:10 +05:30
handleTraceIdQuery ( options : DataQueryRequest < TempoQuery > , targets : TempoQuery [ ] ) : Observable < DataQueryResponse > {
2023-07-14 15:10:46 +01:00
const validTargets = targets
. filter ( ( t ) = > t . query )
2023-10-09 14:22:39 +01:00
. map ( ( t ) : TempoQuery = > ( { . . . t , query : t.query?.trim ( ) , queryType : 'traceId' } ) ) ;
2022-01-05 19:34:09 +01:00
if ( ! validTargets . length ) {
return EMPTY ;
}
2022-10-21 20:08:10 +05:30
const traceRequest = this . traceIdQueryRequest ( options , validTargets ) ;
2022-01-05 19:34:09 +01:00
return super . query ( traceRequest ) . pipe (
map ( ( response ) = > {
if ( response . error ) {
return response ;
}
2023-11-01 10:14:24 +00:00
return transformTrace ( response , this . instanceSettings , this . nodeGraph ? . enabled ) ;
2022-01-05 19:34:09 +01:00
} )
) ;
}
2022-10-21 20:08:10 +05:30
traceIdQueryRequest ( options : DataQueryRequest < TempoQuery > , targets : TempoQuery [ ] ) : DataQueryRequest < TempoQuery > {
2022-11-03 11:01:22 +00:00
const request = {
2022-10-21 20:08:10 +05:30
. . . options ,
2022-11-03 11:01:22 +00:00
targets ,
} ;
if ( this . traceQuery ? . timeShiftEnabled ) {
request . range = options . range && {
2022-10-21 20:08:10 +05:30
. . . options . range ,
from : options . range . from . subtract (
rangeUtil . intervalToMs ( this . traceQuery ? . spanStartTimeShift || '30m' ) ,
'milliseconds'
) ,
to : options.range.to.add ( rangeUtil . intervalToMs ( this . traceQuery ? . spanEndTimeShift || '30m' ) , 'milliseconds' ) ,
2022-11-03 11:01:22 +00:00
} ;
} else {
request . range = { from : dateTime ( 0 ) , to : dateTime ( 0 ) , raw : { from : dateTime ( 0 ) , to : dateTime ( 0 ) } } ;
}
return request ;
2022-10-21 20:08:10 +05:30
}
2023-08-24 14:10:14 +02:00
// This function can probably be simplified by avoiding passing both `targets` and `query`,
// since `query` is built from `targets`, if you look at how this function is currently called
2023-07-14 15:10:46 +01:00
handleStreamingSearch (
options : DataQueryRequest < TempoQuery > ,
targets : TempoQuery [ ] ,
2023-08-24 14:10:14 +02:00
query : string
2023-07-14 15:10:46 +01:00
) : Observable < DataQueryResponse > {
2023-08-24 14:10:14 +02:00
if ( query === '' ) {
2023-07-14 15:10:46 +01:00
return EMPTY ;
}
return merge (
2023-08-24 14:10:14 +02:00
. . . targets . map ( ( target ) = >
2023-07-14 15:10:46 +01:00
doTempoChannelStream (
2023-08-24 14:10:14 +02:00
{ . . . target , query } ,
2023-07-14 15:10:46 +01:00
this , // the datasource
options ,
this . instanceSettings
)
)
) ;
}
2021-08-19 09:15:46 -06:00
async metadataRequest ( url : string , params = { } ) {
2022-09-02 11:17:36 +01:00
return await lastValueFrom ( this . _request ( url , params , { method : 'GET' , hideFromInspector : true } ) ) ;
2021-08-19 09:15:46 -06:00
}
2023-11-06 11:28:44 +00:00
private _request (
apiUrl : string ,
data? : unknown ,
options? : Partial < BackendSrvRequest >
) : Observable < Record < string , any > > {
2023-11-22 13:15:29 +01:00
const params = data ? urlUtil . serializeParams ( data ) : '' ;
2021-08-19 09:15:46 -06:00
const url = ` ${ this . instanceSettings . url } ${ apiUrl } ${ params . length ? ` ? ${ params } ` : '' } ` ;
const req = { . . . options , url } ;
return getBackendSrv ( ) . fetch ( req ) ;
}
2023-11-06 11:28:44 +00:00
async testDatasource ( ) : Promise < TestDataSourceResponse > {
2021-08-19 09:56:03 -06:00
const options : BackendSrvRequest = {
headers : { } ,
method : 'GET' ,
url : ` ${ this . instanceSettings . url } /api/echo ` ,
} ;
2023-06-28 10:34:11 +01:00
return await lastValueFrom (
getBackendSrv ( )
. fetch ( options )
. pipe (
mergeMap ( ( ) = > {
return of ( { status : 'success' , message : 'Data source successfully connected.' } ) ;
} ) ,
catchError ( ( err ) = > {
return of ( { status : 'error' , message : getErrorMessage ( err . data . message , 'Unable to connect with Tempo' ) } ) ;
} )
)
) ;
2021-03-04 21:20:26 +01:00
}
2020-10-13 19:12:49 +02:00
getQueryDisplayText ( query : TempoQuery ) {
2021-09-16 08:04:15 -06:00
if ( query . queryType === 'nativeSearch' ) {
let result = [ ] ;
for ( const key of [ 'serviceName' , 'spanName' , 'search' , 'minDuration' , 'maxDuration' , 'limit' ] ) {
if ( query . hasOwnProperty ( key ) && query [ key as keyof TempoQuery ] ) {
result . push ( ` ${ startCase ( key ) } : ${ query [ key as keyof TempoQuery ] } ` ) ;
}
}
return result . join ( ', ' ) ;
}
2023-10-09 14:22:39 +01:00
return query . query ? ? '' ;
2020-10-13 19:12:49 +02:00
}
2021-08-19 09:15:46 -06:00
2022-01-10 07:38:40 -07:00
buildSearchQuery ( query : TempoQuery , timeRange ? : { startTime : number ; endTime? : number } ) : SearchQueryParams {
2021-12-14 07:41:46 -07:00
let tags = query . search ? ? '' ;
2021-08-19 09:15:46 -06:00
let tempoQuery = pick ( query , [ 'minDuration' , 'maxDuration' , 'limit' ] ) ;
// Remove empty properties
tempoQuery = pickBy ( tempoQuery , identity ) ;
2021-09-28 10:52:57 -06:00
2021-08-19 09:15:46 -06:00
if ( query . serviceName ) {
2021-12-14 07:41:46 -07:00
tags += ` service.name=" ${ query . serviceName } " ` ;
2021-08-19 09:15:46 -06:00
}
if ( query . spanName ) {
2021-12-14 07:41:46 -07:00
tags += ` name=" ${ query . spanName } " ` ;
2021-08-19 09:15:46 -06:00
}
2021-09-28 10:52:57 -06:00
// Set default limit
if ( ! tempoQuery . limit ) {
2021-10-06 08:43:13 -06:00
tempoQuery . limit = DEFAULT_LIMIT ;
2021-09-28 10:52:57 -06:00
}
// Validate query inputs and remove spaces if valid
if ( tempoQuery . minDuration ) {
2022-05-03 17:42:36 +01:00
tempoQuery . minDuration = this . templateSrv . replace ( tempoQuery . minDuration ? ? '' ) ;
2021-09-28 10:52:57 -06:00
if ( ! isValidGoDuration ( tempoQuery . minDuration ) ) {
throw new Error ( 'Please enter a valid min duration.' ) ;
}
tempoQuery . minDuration = tempoQuery . minDuration . replace ( /\s/g , '' ) ;
}
if ( tempoQuery . maxDuration ) {
2022-05-03 17:42:36 +01:00
tempoQuery . maxDuration = this . templateSrv . replace ( tempoQuery . maxDuration ? ? '' ) ;
2021-09-28 10:52:57 -06:00
if ( ! isValidGoDuration ( tempoQuery . maxDuration ) ) {
throw new Error ( 'Please enter a valid max duration.' ) ;
}
tempoQuery . maxDuration = tempoQuery . maxDuration . replace ( /\s/g , '' ) ;
}
if ( ! Number . isInteger ( tempoQuery . limit ) || tempoQuery . limit <= 0 ) {
throw new Error ( 'Please enter a valid limit.' ) ;
}
2022-01-10 07:38:40 -07:00
let searchQuery : SearchQueryParams = { tags , . . . tempoQuery } ;
if ( timeRange ) {
searchQuery . start = timeRange . startTime ;
searchQuery . end = timeRange . endTime ;
}
return searchQuery ;
2021-08-19 09:15:46 -06:00
}
2021-11-11 14:27:59 +01:00
2022-03-17 11:23:15 -06:00
// Get linked loki search datasource. Fall back to legacy loki search/trace to logs config
getLokiSearchDS = ( ) : string | undefined = > {
const legacyLogsDatasourceUid =
this . tracesToLogs ? . lokiSearch !== false && this . lokiSearch === undefined
? this . tracesToLogs ? . datasourceUid
: undefined ;
return this . lokiSearch ? . datasourceUid ? ? legacyLogsDatasourceUid ;
} ;
2020-10-13 19:12:49 +02:00
}
2021-08-17 15:48:29 +02:00
2022-06-09 17:56:15 +01:00
function queryPrometheus ( request : DataQueryRequest < PromQuery > , datasourceUid : string ) {
2024-01-16 11:36:40 +01:00
return from ( getDataSourceSrv ( ) . get ( datasourceUid ) ) . pipe (
2021-08-17 15:48:29 +02:00
mergeMap ( ( ds ) = > {
return ( ds as PrometheusDatasource ) . query ( request ) ;
} )
) ;
}
2023-10-11 18:04:54 +02:00
function serviceMapQuery (
request : DataQueryRequest < TempoQuery > ,
datasourceUid : string ,
tempoDatasourceUid : string
) : Observable < ServiceMapQueryResponse > {
2022-06-09 17:56:15 +01:00
const serviceMapRequest = makePromServiceMapRequest ( request ) ;
return queryPrometheus ( serviceMapRequest , datasourceUid ) . pipe (
2021-08-17 15:48:29 +02:00
// Just collect all the responses first before processing into node graph data
toArray ( ) ,
map ( ( responses : DataQueryResponse [ ] ) = > {
2021-10-13 14:41:42 -06:00
const errorRes = responses . find ( ( res ) = > ! ! res . error ) ;
if ( errorRes ) {
2023-06-28 10:34:11 +01:00
throw new Error ( getErrorMessage ( errorRes . error ? . message ) ) ;
2021-10-13 14:41:42 -06:00
}
2021-11-03 15:56:39 +01:00
const { nodes , edges } = mapPromMetricsToServiceMap ( responses , request . range ) ;
2023-01-17 14:43:28 +00:00
if ( nodes . fields . length > 0 && edges . fields . length > 0 ) {
const nodeLength = nodes . fields [ 0 ] . values . length ;
const edgeLength = edges . fields [ 0 ] . values . length ;
reportInteraction ( 'grafana_traces_service_graph_size' , {
datasourceType : 'tempo' ,
2023-01-27 13:33:27 +00:00
grafana_version : config.buildInfo.version ,
2023-01-17 14:43:28 +00:00
nodeLength ,
edgeLength ,
} ) ;
}
2022-10-19 13:11:33 +01:00
// No handling of multiple targets assume just one. NodeGraph does not support it anyway, but still should be
// fixed at some point.
2023-09-13 10:17:31 +02:00
const { serviceMapIncludeNamespace , refId } = request . targets [ 0 ] ;
nodes . refId = refId ;
edges . refId = refId ;
if ( serviceMapIncludeNamespace ) {
nodes . fields [ 0 ] . config = getFieldConfig (
datasourceUid , // datasourceUid
tempoDatasourceUid , // tempoDatasourceUid
'__data.fields.title' , // targetField
'__data.fields[0]' , // tempoField
undefined , // sourceField
{ targetNamespace : '__data.fields.subtitle' }
) ;
2022-10-19 13:11:33 +01:00
2023-09-13 10:17:31 +02:00
edges . fields [ 0 ] . config = getFieldConfig (
datasourceUid , // datasourceUid
tempoDatasourceUid , // tempoDatasourceUid
'__data.fields.targetName' , // targetField
'__data.fields.target' , // tempoField
'__data.fields.sourceName' , // sourceField
{ targetNamespace : '__data.fields.targetNamespace' , sourceNamespace : '__data.fields.sourceNamespace' }
) ;
} else {
nodes . fields [ 0 ] . config = getFieldConfig (
datasourceUid ,
tempoDatasourceUid ,
'__data.fields.id' ,
'__data.fields[0]'
) ;
edges . fields [ 0 ] . config = getFieldConfig (
datasourceUid ,
tempoDatasourceUid ,
'__data.fields.target' ,
'__data.fields.target' ,
'__data.fields.source'
) ;
}
2021-11-03 15:56:39 +01:00
2021-08-17 15:48:29 +02:00
return {
2023-10-11 18:04:54 +02:00
nodes ,
edges ,
2021-08-17 15:48:29 +02:00
state : LoadingState.Done ,
} ;
} )
) ;
}
2022-06-09 17:56:15 +01:00
function rateQuery (
request : DataQueryRequest < TempoQuery > ,
2023-10-11 18:04:54 +02:00
serviceMapResponse : ServiceMapQueryResponse ,
2022-06-09 17:56:15 +01:00
datasourceUid : string
2023-10-11 18:04:54 +02:00
) : Observable < ServiceMapQueryResponseWithRates > {
2022-06-09 17:56:15 +01:00
const serviceMapRequest = makePromServiceMapRequest ( request ) ;
2023-03-22 08:37:44 +00:00
serviceMapRequest . targets = makeServiceGraphViewRequest ( [ buildExpr ( rateMetric , defaultTableFilter , request ) ] ) ;
2022-06-09 17:56:15 +01:00
return queryPrometheus ( serviceMapRequest , datasourceUid ) . pipe (
toArray ( ) ,
map ( ( responses : DataQueryResponse [ ] ) = > {
const errorRes = responses . find ( ( res ) = > ! ! res . error ) ;
if ( errorRes ) {
2023-06-28 10:34:11 +01:00
throw new Error ( getErrorMessage ( errorRes . error ? . message ) ) ;
2022-06-09 17:56:15 +01:00
}
return {
2023-10-11 18:04:54 +02:00
rates : responses [ 0 ] ? . data ? ? [ ] ,
nodes : serviceMapResponse.nodes ,
edges : serviceMapResponse.edges ,
2022-06-09 17:56:15 +01:00
} ;
} )
) ;
}
// we need the response from the rate query to get the rate span_name(s),
// -> which determine the errorRate/duration span_name(s) we need to query
function errorAndDurationQuery (
request : DataQueryRequest < TempoQuery > ,
2023-10-11 18:04:54 +02:00
rateResponse : ServiceMapQueryResponseWithRates ,
2022-06-09 17:56:15 +01:00
datasourceUid : string ,
tempoDatasourceUid : string
) {
2023-03-22 08:37:44 +00:00
let serviceGraphViewMetrics = [ ] ;
2022-06-09 17:56:15 +01:00
let errorRateBySpanName = '' ;
let durationsBySpanName : string [ ] = [ ] ;
2023-05-15 14:27:27 +01:00
let labels = [ ] ;
2023-10-11 18:04:54 +02:00
if ( rateResponse . rates [ 0 ] && request . app === CoreApp . Explore ) {
const spanNameField = rateResponse . rates [ 0 ] . fields . find ( ( field ) = > field . name === 'span_name' ) ;
2023-08-03 07:55:32 +01:00
if ( spanNameField && spanNameField . values ) {
labels = spanNameField . values ;
2023-05-15 14:27:27 +01:00
}
2023-10-11 18:04:54 +02:00
} else if ( rateResponse . rates ) {
rateResponse . rates . map ( ( df : DataFrame | DataFrameDTO ) = > {
const spanNameLabels = df . fields . find ( ( field ) = > field . labels ? . [ 'span_name' ] ) ;
2023-08-03 07:55:32 +01:00
if ( spanNameLabels ) {
labels . push ( spanNameLabels . labels ? . [ 'span_name' ] ) ;
2023-05-15 14:27:27 +01:00
}
} ) ;
}
const spanNames = getEscapedSpanNames ( labels ) ;
2022-06-09 17:56:15 +01:00
if ( spanNames . length > 0 ) {
errorRateBySpanName = buildExpr ( errorRateMetric , 'span_name=~"' + spanNames . join ( '|' ) + '"' , request ) ;
2023-03-22 08:37:44 +00:00
serviceGraphViewMetrics . push ( errorRateBySpanName ) ;
2022-06-09 17:56:15 +01:00
spanNames . map ( ( name : string ) = > {
const metric = buildExpr ( durationMetric , 'span_name=~"' + name + '"' , request ) ;
durationsBySpanName . push ( metric ) ;
2023-03-22 08:37:44 +00:00
serviceGraphViewMetrics . push ( metric ) ;
2022-06-09 17:56:15 +01:00
} ) ;
}
const serviceMapRequest = makePromServiceMapRequest ( request ) ;
2023-03-22 08:37:44 +00:00
serviceMapRequest . targets = makeServiceGraphViewRequest ( serviceGraphViewMetrics ) ;
2022-06-09 17:56:15 +01:00
return queryPrometheus ( serviceMapRequest , datasourceUid ) . pipe (
// Just collect all the responses first before processing into node graph data
toArray ( ) ,
map ( ( errorAndDurationResponse : DataQueryResponse [ ] ) = > {
const errorRes = errorAndDurationResponse . find ( ( res ) = > ! ! res . error ) ;
if ( errorRes ) {
2023-06-28 10:34:11 +01:00
throw new Error ( getErrorMessage ( errorRes . error ? . message ) ) ;
2022-06-09 17:56:15 +01:00
}
2023-03-22 08:37:44 +00:00
const serviceGraphView = getServiceGraphView (
2022-06-09 17:56:15 +01:00
request ,
rateResponse ,
errorAndDurationResponse [ 0 ] ,
errorRateBySpanName ,
durationsBySpanName ,
datasourceUid ,
tempoDatasourceUid
) ;
2023-03-22 08:37:44 +00:00
if ( serviceGraphView . fields . length === 0 ) {
2022-06-09 17:56:15 +01:00
return {
2023-10-11 18:04:54 +02:00
data : [ rateResponse . nodes , rateResponse . edges ] ,
2022-06-09 17:56:15 +01:00
state : LoadingState.Done ,
} ;
}
return {
2023-10-11 18:04:54 +02:00
data : [ serviceGraphView , rateResponse . nodes , rateResponse . edges ] ,
2022-06-09 17:56:15 +01:00
state : LoadingState.Done ,
} ;
} )
) ;
}
function makePromLink ( title : string , expr : string , datasourceUid : string , instant : boolean ) {
2021-11-03 15:56:39 +01:00
return {
url : '' ,
title ,
internal : {
query : {
2022-06-09 17:56:15 +01:00
expr : expr ,
range : ! instant ,
exemplar : ! instant ,
instant : instant ,
2023-11-14 09:17:29 +00:00
} ,
2021-11-03 15:56:39 +01:00
datasourceUid ,
2024-01-16 11:36:40 +01:00
datasourceName : getDataSourceSrv ( ) . getInstanceSettings ( datasourceUid ) ? . name ? ? '' ,
2021-11-03 15:56:39 +01:00
} ,
} ;
}
2023-05-11 17:21:26 +03:00
export function getEscapedSpanNames ( values : string [ ] ) {
return values . map ( ( value : string ) = > value . replace ( /[.*+?^${}()|[\]\\]/g , '\\\\$&' ) ) ;
}
2022-07-25 10:03:57 +01:00
export function getFieldConfig (
datasourceUid : string ,
tempoDatasourceUid : string ,
targetField : string ,
tempoField : string ,
2023-09-13 10:17:31 +02:00
sourceField? : string ,
namespaceFields ? : { targetNamespace : string ; sourceNamespace? : string }
2022-07-25 10:03:57 +01:00
) {
2023-09-13 10:17:31 +02:00
let source = sourceField ? ` client=" \ ${ $ { sourceField } }", ` : '' ;
let target = ` server=" \ ${ $ { targetField } }" ` ;
let serverSumBy = 'server' ;
if ( namespaceFields !== undefined ) {
const { targetNamespace } = namespaceFields ;
target += ` ,server_service_namespace=" \ ${ $ { targetNamespace } }" ` ;
serverSumBy += ', server_service_namespace' ;
if ( source ) {
const { sourceNamespace } = namespaceFields ;
source += ` client_service_namespace=" \ ${ $ { sourceNamespace } }", ` ;
serverSumBy += ', client_service_namespace' ;
}
}
2022-07-25 10:03:57 +01:00
return {
links : [
makePromLink (
'Request rate' ,
2023-09-13 10:17:31 +02:00
` sum by (client, ${ serverSumBy } )(rate( ${ totalsMetric } { ${ source } ${ target } }[ $ __rate_interval])) ` ,
2022-07-25 10:03:57 +01:00
datasourceUid ,
false
) ,
makePromLink (
'Request histogram' ,
2023-09-13 10:17:31 +02:00
` histogram_quantile(0.9, sum(rate( ${ histogramMetric } { ${ source } ${ target } }[ $ __rate_interval])) by (le, client, ${ serverSumBy } )) ` ,
2022-07-25 10:03:57 +01:00
datasourceUid ,
false
) ,
makePromLink (
'Failed request rate' ,
2023-09-13 10:17:31 +02:00
` sum by (client, ${ serverSumBy } )(rate( ${ failedMetric } { ${ source } ${ target } }[ $ __rate_interval])) ` ,
2022-07-25 10:03:57 +01:00
datasourceUid ,
false
) ,
makeTempoLink ( 'View traces' , ` \ ${ $ { tempoField } } ` , '' , tempoDatasourceUid ) ,
] ,
} ;
}
2022-06-09 17:56:15 +01:00
export function makeTempoLink ( title : string , serviceName : string , spanName : string , datasourceUid : string ) {
2023-10-09 14:22:39 +01:00
let query : TempoQuery = { refId : 'A' , queryType : 'traceqlSearch' , filters : [ ] } ;
2022-06-09 17:56:15 +01:00
if ( serviceName !== '' ) {
2023-10-09 14:22:39 +01:00
query . filters . push ( {
id : 'service-name' ,
scope : TraceqlSearchScope.Resource ,
tag : 'service.name' ,
value : serviceName ,
operator : '=' ,
valueType : 'string' ,
} ) ;
2022-06-09 17:56:15 +01:00
}
if ( spanName !== '' ) {
2023-10-09 14:22:39 +01:00
query . filters . push ( {
id : 'span-name' ,
scope : TraceqlSearchScope.Span ,
tag : 'name' ,
value : spanName ,
operator : '=' ,
valueType : 'string' ,
} ) ;
2022-06-09 17:56:15 +01:00
}
2022-06-03 10:38:13 +01:00
return {
url : '' ,
title ,
internal : {
2022-06-09 17:56:15 +01:00
query ,
2022-07-18 15:36:16 +01:00
datasourceUid ,
2024-01-16 11:36:40 +01:00
datasourceName : getDataSourceSrv ( ) . getInstanceSettings ( datasourceUid ) ? . name ? ? '' ,
2022-06-03 10:38:13 +01:00
} ,
} ;
}
2021-08-17 15:48:29 +02:00
function makePromServiceMapRequest ( options : DataQueryRequest < TempoQuery > ) : DataQueryRequest < PromQuery > {
return {
. . . options ,
targets : serviceMapMetrics.map ( ( metric ) = > {
2023-07-18 13:11:12 +03:00
const { serviceMapQuery , serviceMapIncludeNamespace : serviceMapIncludeNamespace } = options . targets [ 0 ] ;
const extraSumByFields = serviceMapIncludeNamespace ? ', client_service_namespace, server_service_namespace' : '' ;
2024-01-26 16:37:49 +02:00
const queries = Array . isArray ( serviceMapQuery ) ? serviceMapQuery : [ serviceMapQuery ] ;
const subExprs = queries . map (
( query ) = > ` sum by (client, server ${ extraSumByFields } ) (rate( ${ metric } ${ query || '' } [ $ __range])) `
) ;
2021-08-17 15:48:29 +02:00
return {
2022-10-19 13:11:33 +01:00
format : 'table' ,
2021-08-17 15:48:29 +02:00
refId : metric ,
2021-11-11 14:27:59 +01:00
// options.targets[0] is not correct here, but not sure what should happen if you have multiple queries for
// service map at the same time anyway
2024-01-26 16:37:49 +02:00
expr : subExprs.join ( ' OR ' ) ,
2021-08-17 15:48:29 +02:00
instant : true ,
} ;
} ) ,
} ;
}
2022-06-09 17:56:15 +01:00
2023-03-22 08:37:44 +00:00
function getServiceGraphView (
2022-06-09 17:56:15 +01:00
request : DataQueryRequest < TempoQuery > ,
2023-10-11 18:04:54 +02:00
rateResponse : ServiceMapQueryResponseWithRates ,
2022-06-09 17:56:15 +01:00
secondResponse : DataQueryResponse ,
errorRateBySpanName : string ,
durationsBySpanName : string [ ] ,
datasourceUid : string ,
tempoDatasourceUid : string
) {
let df : any = { fields : [ ] } ;
2023-10-11 18:04:54 +02:00
const rate = rateResponse . rates . filter ( ( x ) = > {
2022-07-27 15:48:09 +01:00
return x . refId === buildExpr ( rateMetric , defaultTableFilter , request ) ;
2022-06-09 17:56:15 +01:00
} ) ;
const errorRate = secondResponse . data . filter ( ( x ) = > {
return x . refId === errorRateBySpanName ;
} ) ;
const duration = secondResponse . data . filter ( ( x ) = > {
2023-10-11 18:04:54 +02:00
return durationsBySpanName . includes ( x . refId ? ? '' ) ;
2022-06-09 17:56:15 +01:00
} ) ;
if ( rate . length > 0 && rate [ 0 ] . fields ? . length > 2 ) {
df . fields . push ( {
. . . rate [ 0 ] . fields [ 1 ] ,
name : 'Name' ,
config : {
filterable : false ,
} ,
} ) ;
df . fields . push ( {
. . . rate [ 0 ] . fields [ 2 ] ,
name : 'Rate' ,
config : {
links : [
makePromLink (
'Rate' ,
buildLinkExpr ( buildExpr ( rateMetric , 'span_name="${__data.fields[0]}"' , request ) ) ,
datasourceUid ,
false
) ,
] ,
decimals : 2 ,
} ,
} ) ;
df . fields . push ( {
. . . rate [ 0 ] . fields [ 2 ] ,
2023-03-22 08:37:44 +00:00
name : ' ' ,
2022-06-09 17:56:15 +01:00
labels : null ,
config : {
color : {
mode : 'continuous-BlPu' ,
} ,
custom : {
2023-03-22 08:37:44 +00:00
cellOptions : {
mode : BarGaugeDisplayMode.Lcd ,
type : TableCellDisplayMode . Gauge ,
} ,
2022-06-09 17:56:15 +01:00
} ,
decimals : 3 ,
} ,
} ) ;
}
if ( errorRate . length > 0 && errorRate [ 0 ] . fields ? . length > 2 ) {
2023-04-20 09:59:18 -05:00
const errorRateNames = errorRate [ 0 ] . fields [ 1 ] ? . values ? ? [ ] ;
const errorRateValues = errorRate [ 0 ] . fields [ 2 ] ? . values ? ? [ ] ;
2022-06-09 17:56:15 +01:00
let errorRateObj : any = { } ;
errorRateNames . map ( ( name : string , index : number ) = > {
2022-07-25 18:59:43 +01:00
errorRateObj [ name ] = { value : errorRateValues [ index ] } ;
2022-06-09 17:56:15 +01:00
} ) ;
2022-07-25 18:59:43 +01:00
const values = getRateAlignedValues ( { . . . rate } , errorRateObj ) ;
2022-06-09 17:56:15 +01:00
df . fields . push ( {
. . . errorRate [ 0 ] . fields [ 2 ] ,
name : 'Error Rate' ,
values : values ,
config : {
links : [
makePromLink (
'Error Rate' ,
buildLinkExpr ( buildExpr ( errorRateMetric , 'span_name="${__data.fields[0]}"' , request ) ) ,
datasourceUid ,
false
) ,
] ,
decimals : 2 ,
} ,
} ) ;
df . fields . push ( {
. . . errorRate [ 0 ] . fields [ 2 ] ,
2023-03-22 08:37:44 +00:00
name : ' ' ,
2022-06-09 17:56:15 +01:00
values : values ,
labels : null ,
config : {
color : {
mode : 'continuous-RdYlGr' ,
} ,
custom : {
2023-03-22 08:37:44 +00:00
cellOptions : {
mode : BarGaugeDisplayMode.Lcd ,
type : TableCellDisplayMode . Gauge ,
} ,
2022-06-09 17:56:15 +01:00
} ,
decimals : 3 ,
} ,
} ) ;
}
2023-10-10 17:57:05 +03:00
if ( duration . length > 0 ) {
2022-06-09 17:56:15 +01:00
let durationObj : any = { } ;
2023-10-10 17:57:05 +03:00
duration . forEach ( ( d ) = > {
if ( d . fields . length > 1 ) {
const delimiter = d . refId ? . includes ( 'span_name=~"' ) ? 'span_name=~"' : 'span_name="' ;
const name = d . refId ? . split ( delimiter ) [ 1 ] . split ( '"}' ) [ 0 ] ;
2023-10-11 18:04:54 +02:00
durationObj [ name ! ] = { value : d.fields [ 1 ] . values [ 0 ] } ;
2023-10-10 17:57:05 +03:00
}
2022-06-09 17:56:15 +01:00
} ) ;
2023-10-10 17:57:05 +03:00
if ( Object . keys ( durationObj ) . length > 0 ) {
df . fields . push ( {
. . . duration [ 0 ] . fields [ 1 ] ,
name : 'Duration (p90)' ,
values : getRateAlignedValues ( { . . . rate } , durationObj ) ,
config : {
links : [
makePromLink (
'Duration' ,
buildLinkExpr ( buildExpr ( durationMetric , 'span_name="${__data.fields[0]}"' , request ) ) ,
datasourceUid ,
false
) ,
] ,
unit : 's' ,
} ,
} ) ;
}
2022-06-09 17:56:15 +01:00
}
if ( df . fields . length > 0 && df . fields [ 0 ] . values ) {
df . fields . push ( {
name : 'Links' ,
type : FieldType . string ,
values : df.fields [ 0 ] . values . map ( ( ) = > {
return 'Tempo' ;
} ) ,
config : {
links : [ makeTempoLink ( 'Tempo' , '' , ` \ ${ __data . fields [ 0 ] } ` , tempoDatasourceUid ) ] ,
} ,
} ) ;
}
return df ;
}
export function buildExpr (
2024-01-26 16:37:49 +02:00
metric : { expr : string ; params : string [ ] ; topk? : number } ,
2022-06-09 17:56:15 +01:00
extraParams : string ,
request : DataQueryRequest < TempoQuery >
2024-01-26 16:37:49 +02:00
) : string {
2023-01-04 16:18:41 +00:00
let serviceMapQuery = request . targets [ 0 ] ? . serviceMapQuery ? ? '' ;
2024-01-26 16:37:49 +02:00
const serviceMapQueries = Array . isArray ( serviceMapQuery ) ? serviceMapQuery : [ serviceMapQuery ] ;
const metricParamsArray = serviceMapQueries . map ( ( query ) = > {
// remove surrounding curly braces from serviceMapQuery
const serviceMapQueryMatch = query . match ( /^{(.*)}$/ ) ;
if ( serviceMapQueryMatch ? . length ) {
query = serviceMapQueryMatch [ 1 ] ;
}
// map serviceGraph metric tags to serviceGraphView metric tags
query = query . replace ( 'client' , 'service' ) . replace ( 'server' , 'service' ) ;
return query . includes ( 'span_name' )
? metric . params . concat ( query )
: metric . params
. concat ( query )
. concat ( extraParams )
. filter ( ( item : string ) = > item ) ;
} ) ;
const exprs = metricParamsArray . map ( ( params ) = > metric . expr . replace ( '{}' , '{' + params . join ( ',' ) + '}' ) ) ;
const expr = exprs . join ( ' OR ' ) ;
if ( metric . topk ) {
return ` topk( ${ metric . topk } , ${ expr } ) ` ;
2023-01-04 16:18:41 +00:00
}
2024-01-26 16:37:49 +02:00
return expr ;
2022-06-09 17:56:15 +01:00
}
export function buildLinkExpr ( expr : string ) {
2022-06-20 09:56:45 +01:00
// don't want top 5 or by span name in links
expr = expr . replace ( 'topk(5, ' , '' ) . replace ( ' by (span_name))' , '' ) ;
2022-06-09 17:56:15 +01:00
return expr . replace ( '__range' , '__rate_interval' ) ;
}
// query result frames can come back in any order
// here we align the table col values to the same row name (rateName) across the table
export function getRateAlignedValues (
rateResp : DataQueryResponseData [ ] ,
objToAlign : { [ x : string ] : { value : string } }
) {
2023-04-20 09:59:18 -05:00
const rateNames = rateResp [ 0 ] ? . fields [ 1 ] ? . values ? ? [ ] ;
2022-06-09 17:56:15 +01:00
let values : string [ ] = [ ] ;
for ( let i = 0 ; i < rateNames . length ; i ++ ) {
2022-07-25 18:59:43 +01:00
if ( Object . keys ( objToAlign ) . includes ( rateNames [ i ] ) ) {
values . push ( objToAlign [ rateNames [ i ] ] . value ) ;
} else {
values . push ( '0' ) ;
2022-06-09 17:56:15 +01:00
}
}
return values ;
}
2023-11-06 11:28:44 +00:00
export function makeServiceGraphViewRequest ( metrics : string [ ] ) : PromQuery [ ] {
2022-06-09 17:56:15 +01:00
return metrics . map ( ( metric ) = > {
return {
refId : metric ,
expr : metric ,
instant : true ,
} ;
} ) ;
}