2019-01-18 10:43:58 -06:00
// Libraries
2020-08-31 23:21:21 -05:00
import { cloneDeep , isEmpty , map as lodashMap } from 'lodash' ;
2021-08-18 23:38:31 -05:00
import { lastValueFrom , merge , Observable , of , throwError } from 'rxjs' ;
2020-08-31 23:21:21 -05:00
import { catchError , map , switchMap } from 'rxjs/operators' ;
2020-10-16 06:30:02 -05:00
import Prism from 'prismjs' ;
2019-11-15 09:38:25 -06:00
// Types
2019-09-10 04:04:44 -05:00
import {
AnnotationEvent ,
2020-08-31 23:21:21 -05:00
AnnotationQueryRequest ,
2022-02-07 01:43:48 -06:00
CoreApp ,
2020-08-31 23:21:21 -05:00
DataFrame ,
2019-09-10 04:04:44 -05:00
DataFrameView ,
2019-05-10 07:00:39 -05:00
DataQueryError ,
2019-07-06 01:05:53 -05:00
DataQueryRequest ,
2019-06-18 04:01:12 -05:00
DataQueryResponse ,
2020-08-31 23:21:21 -05:00
DataSourceInstanceSettings ,
2021-10-01 06:38:16 -05:00
DataSourceWithLogsContextSupport ,
2021-09-30 08:46:11 -05:00
DataSourceWithLogsVolumeSupport ,
2021-12-14 07:36:47 -06:00
DataSourceWithQueryExportSupport ,
DataSourceWithQueryImportSupport ,
2020-08-31 23:21:21 -05:00
dateMath ,
DateTime ,
FieldCache ,
2021-12-14 07:36:47 -06:00
AbstractQuery ,
2021-11-02 05:53:47 -05:00
FieldType ,
getLogLevelFromKey ,
Labels ,
2020-08-31 23:21:21 -05:00
LoadingState ,
2021-11-02 05:53:47 -05:00
LogLevel ,
2020-08-31 23:21:21 -05:00
LogRowModel ,
QueryResultMeta ,
2020-01-24 02:50:09 -06:00
ScopedVars ,
2021-07-06 01:56:01 -05:00
TimeRange ,
2019-10-31 04:48:05 -05:00
} from '@grafana/data' ;
2022-02-07 01:43:48 -06:00
import { BackendSrvRequest , FetchError , getBackendSrv , DataSourceWithBackend } from '@grafana/runtime' ;
2021-07-06 03:30:27 -05:00
import { getTemplateSrv , TemplateSrv } from 'app/features/templating/template_srv' ;
2021-12-03 07:11:43 -06:00
import { addLabelToQuery } from './add_label_to_query' ;
2020-10-01 12:51:23 -05:00
import { getTimeSrv , TimeSrv } from 'app/features/dashboard/services/TimeSrv' ;
2020-08-31 23:21:21 -05:00
import { convertToWebSocketUrl } from 'app/core/utils/explore' ;
2020-11-20 04:12:34 -06:00
import {
lokiResultsToTableModel ,
lokiStreamResultToDataFrame ,
lokiStreamsToDataFrames ,
processRangeQueryResponse ,
} from './result_transformer' ;
2022-02-25 02:14:17 -06:00
import { transformBackendResult } from './backendResultTransformer' ;
2022-02-07 01:43:48 -06:00
import { addParsedLabelToQuery , getNormalizedLokiQuery , queryHasPipeParser } from './query_utils' ;
2018-07-20 10:07:17 -05:00
2019-11-15 09:38:25 -06:00
import {
LokiOptions ,
2020-08-31 23:21:21 -05:00
LokiQuery ,
2022-01-07 12:00:11 -06:00
LokiQueryType ,
2020-08-31 23:21:21 -05:00
LokiRangeQueryRequest ,
2019-11-15 09:38:25 -06:00
LokiResultType ,
LokiStreamResponse ,
2021-09-06 08:28:54 -05:00
LokiStreamResult ,
2019-11-15 09:38:25 -06:00
} from './types' ;
2020-04-22 06:59:06 -05:00
import { LiveStreams , LokiLiveTarget } from './live_streams' ;
2021-03-02 09:58:14 -06:00
import LanguageProvider from './language_provider' ;
2020-03-25 06:25:39 -05:00
import { serializeParams } from '../../../core/utils/fetch' ;
2020-04-25 15:48:20 -05:00
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider' ;
2020-10-16 06:30:02 -05:00
import syntax from './syntax' ;
2021-08-16 07:02:13 -05:00
import { DEFAULT_RESOLUTION } from './components/LokiOptionFields' ;
2021-11-02 05:53:47 -05:00
import { queryLogsVolume } from 'app/core/logs_model' ;
2021-11-10 04:20:30 -06:00
import config from 'app/core/config' ;
2021-12-14 21:19:16 -06:00
import { renderLegendFormat } from '../prometheus/legend' ;
2019-11-15 09:38:25 -06:00
2020-07-16 08:00:28 -05:00
export type RangeQueryOptions = DataQueryRequest < LokiQuery > | AnnotationQueryRequest < LokiQuery > ;
2018-12-31 08:10:45 -06:00
export const DEFAULT_MAX_LINES = 1000 ;
2019-12-03 02:40:22 -06:00
export const LOKI_ENDPOINT = '/loki/api/v1' ;
2021-03-02 09:58:14 -06:00
const NS_IN_MS = 1000000 ;
2019-12-03 02:40:22 -06:00
const RANGE_QUERY_ENDPOINT = ` ${ LOKI_ENDPOINT } /query_range ` ;
const INSTANT_QUERY_ENDPOINT = ` ${ LOKI_ENDPOINT } /query ` ;
2018-07-20 10:07:17 -05:00
2020-04-22 06:59:06 -05:00
const DEFAULT_QUERY_PARAMS : Partial < LokiRangeQueryRequest > = {
2018-07-20 10:07:17 -05:00
direction : 'BACKWARD' ,
2018-12-31 08:10:45 -06:00
limit : DEFAULT_MAX_LINES ,
2018-07-20 10:07:17 -05:00
query : '' ,
} ;
2021-09-30 08:46:11 -05:00
export class LokiDatasource
2022-02-07 01:43:48 -06:00
extends DataSourceWithBackend < LokiQuery , LokiOptions >
2021-12-14 07:36:47 -06:00
implements
DataSourceWithLogsContextSupport ,
DataSourceWithLogsVolumeSupport < LokiQuery > ,
DataSourceWithQueryImportSupport < LokiQuery > ,
2022-02-02 06:02:32 -06:00
DataSourceWithQueryExportSupport < LokiQuery >
{
2019-09-05 07:04:01 -05:00
private streams = new LiveStreams ( ) ;
2018-10-30 10:14:01 -05:00
languageProvider : LanguageProvider ;
2018-12-31 08:10:45 -06:00
maxLines : number ;
2018-10-30 10:14:01 -05:00
2020-10-01 12:51:23 -05:00
constructor (
private instanceSettings : DataSourceInstanceSettings < LokiOptions > ,
private readonly templateSrv : TemplateSrv = getTemplateSrv ( ) ,
private readonly timeSrv : TimeSrv = getTimeSrv ( )
) {
2019-05-10 04:37:43 -05:00
super ( instanceSettings ) ;
2019-11-15 09:38:25 -06:00
2018-10-30 10:14:01 -05:00
this . languageProvider = new LanguageProvider ( this ) ;
2018-12-31 05:25:28 -06:00
const settingsData = instanceSettings . jsonData || { } ;
2020-05-07 06:53:05 -05:00
this . maxLines = parseInt ( settingsData . maxLines ? ? '0' , 10 ) || DEFAULT_MAX_LINES ;
2018-10-30 10:14:01 -05:00
}
2018-07-20 10:07:17 -05:00
2020-07-07 14:22:47 -05:00
_request ( apiUrl : string , data? : any , options? : Partial < BackendSrvRequest > ) : Observable < Record < string , any > > {
2018-07-20 10:07:17 -05:00
const baseUrl = this . instanceSettings . url ;
const params = data ? serializeParams ( data ) : '' ;
2019-11-15 09:38:25 -06:00
const url = ` ${ baseUrl } ${ apiUrl } ${ params . length ? ` ? ${ params } ` : '' } ` ;
2021-07-20 10:05:12 -05:00
if ( this . instanceSettings . withCredentials || this . instanceSettings . basicAuth ) {
options = { . . . options , withCredentials : true } ;
if ( this . instanceSettings . basicAuth ) {
options . headers = { . . . options . headers , Authorization : this.instanceSettings.basicAuth } ;
}
}
2018-07-20 10:07:17 -05:00
const req = {
. . . options ,
url ,
} ;
2019-07-08 10:14:48 -05:00
2020-08-31 23:21:21 -05:00
return getBackendSrv ( ) . fetch < Record < string , any > > ( req ) ;
2018-07-20 10:07:17 -05:00
}
2021-09-30 08:46:11 -05:00
getLogsVolumeDataProvider ( request : DataQueryRequest < LokiQuery > ) : Observable < DataQueryResponse > | undefined {
const isLogsVolumeAvailable = request . targets . some ( ( target ) = > target . expr && ! isMetricsQuery ( target . expr ) ) ;
2021-11-02 05:53:47 -05:00
if ( ! isLogsVolumeAvailable ) {
return undefined ;
}
const logsVolumeRequest = cloneDeep ( request ) ;
logsVolumeRequest . targets = logsVolumeRequest . targets
. filter ( ( target ) = > target . expr && ! isMetricsQuery ( target . expr ) )
. map ( ( target ) = > {
return {
. . . target ,
instant : false ,
2021-11-10 04:20:30 -06:00
volumeQuery : true ,
2021-11-02 05:53:47 -05:00
expr : ` sum by (level) (count_over_time( ${ target . expr } [ $ __interval])) ` ,
} ;
} ) ;
return queryLogsVolume ( this , logsVolumeRequest , {
extractLevel ,
range : request.range ,
targets : request.targets ,
} ) ;
2021-09-30 08:46:11 -05:00
}
2022-02-07 01:43:48 -06:00
query ( request : DataQueryRequest < LokiQuery > ) : Observable < DataQueryResponse > {
2019-11-15 09:38:25 -06:00
const subQueries : Array < Observable < DataQueryResponse > > = [ ] ;
2021-07-06 01:56:01 -05:00
const scopedVars = {
2022-02-07 01:43:48 -06:00
. . . request . scopedVars ,
. . . this . getRangeScopedVars ( request . range ) ,
2021-07-06 01:56:01 -05:00
} ;
2022-02-07 01:43:48 -06:00
2022-02-25 02:14:17 -06:00
const shouldRunBackendQuery = config . featureToggles . lokiBackendMode && request . app === CoreApp . Explore ;
2022-02-07 01:43:48 -06:00
if ( shouldRunBackendQuery ) {
// we "fix" the loki queries to have `.queryType` and not have `.instant` and `.range`
const fixedRequest = {
. . . request ,
targets : request.targets.map ( getNormalizedLokiQuery ) ,
} ;
2022-02-25 02:14:17 -06:00
return super . query ( fixedRequest ) . pipe ( map ( ( response ) = > transformBackendResult ( response , fixedRequest ) ) ) ;
2022-02-07 01:43:48 -06:00
}
const filteredTargets = request . targets
2021-01-20 00:59:48 -06:00
. filter ( ( target ) = > target . expr && ! target . hide )
2021-07-06 03:30:27 -05:00
. map ( ( target ) = > {
const expr = this . addAdHocFilters ( target . expr ) ;
return {
. . . target ,
expr : this.templateSrv.replace ( expr , scopedVars , this . interpolateQueryExpr ) ,
} ;
} ) ;
2019-11-15 09:38:25 -06:00
2020-10-04 14:41:12 -05:00
for ( const target of filteredTargets ) {
2022-01-07 12:00:11 -06:00
if ( target . instant || target . queryType === LokiQueryType . Instant ) {
2022-02-07 01:43:48 -06:00
subQueries . push ( this . runInstantQuery ( target , request , filteredTargets . length ) ) ;
2020-11-20 04:12:34 -06:00
} else {
2022-02-07 01:43:48 -06:00
subQueries . push ( this . runRangeQuery ( target , request , filteredTargets . length ) ) ;
2020-10-04 14:41:12 -05:00
}
}
2019-09-05 07:04:01 -05:00
2019-11-15 09:38:25 -06:00
// No valid targets, return the empty result to save a round trip.
if ( isEmpty ( subQueries ) ) {
return of ( {
data : [ ] ,
state : LoadingState.Done ,
} ) ;
}
return merge ( . . . subQueries ) ;
2019-06-03 07:54:32 -05:00
}
2019-05-20 06:28:23 -05:00
2019-11-15 09:38:25 -06:00
runInstantQuery = (
target : LokiQuery ,
options : DataQueryRequest < LokiQuery > ,
2021-02-16 06:27:17 -06:00
responseListLength = 1
2019-11-15 09:38:25 -06:00
) : Observable < DataQueryResponse > = > {
const timeNs = this . getTime ( options . range . to , true ) ;
2020-11-20 04:12:34 -06:00
const queryLimit = isMetricsQuery ( target . expr ) ? options.maxDataPoints : target.maxLines ;
2019-11-15 09:38:25 -06:00
const query = {
2020-05-06 04:21:25 -05:00
query : target.expr ,
2019-11-15 09:38:25 -06:00
time : ` ${ timeNs + ( 1 e9 - ( timeNs % 1 e9 ) ) } ` ,
2020-11-20 04:12:34 -06:00
limit : Math.min ( queryLimit || Infinity , this . maxLines ) ,
2019-11-15 09:38:25 -06:00
} ;
2020-11-20 04:12:34 -06:00
/** Used only for results of metrics instant queries */
2020-06-30 02:25:05 -05:00
const meta : QueryResultMeta = {
preferredVisualisationType : 'table' ,
} ;
2019-11-15 09:38:25 -06:00
return this . _request ( INSTANT_QUERY_ENDPOINT , query ) . pipe (
2021-09-06 08:28:54 -05:00
map ( ( response ) = > {
2019-11-15 09:38:25 -06:00
if ( response . data . data . resultType === LokiResultType . Stream ) {
2020-07-09 09:14:55 -05:00
return {
2020-11-20 04:12:34 -06:00
data : response.data
? lokiStreamsToDataFrames (
response . data as LokiStreamResponse ,
target ,
query . limit ,
this . instanceSettings . jsonData
)
: [ ] ,
2020-07-09 09:14:55 -05:00
key : ` ${ target . refId } _instant ` ,
} ;
2019-11-15 09:38:25 -06:00
}
2019-09-12 10:28:46 -05:00
return {
2020-10-01 07:39:32 -05:00
data : [ lokiResultsToTableModel ( response . data . data . result , responseListLength , target . refId , meta , true ) ] ,
2019-11-15 09:38:25 -06:00
key : ` ${ target . refId } _instant ` ,
2019-09-12 10:28:46 -05:00
} ;
2020-08-27 03:32:36 -05:00
} ) ,
2021-09-06 08:28:54 -05:00
catchError ( ( err ) = > throwError ( ( ) = > this . processError ( err , target ) ) )
2019-09-12 10:28:46 -05:00
) ;
2019-06-03 07:54:32 -05:00
} ;
2020-10-16 06:30:02 -05:00
createRangeQuery ( target : LokiQuery , options : RangeQueryOptions , limit : number ) : LokiRangeQueryRequest {
2020-05-06 04:21:25 -05:00
const query = target . expr ;
2019-11-15 09:38:25 -06:00
let range : { start? : number ; end? : number ; step? : number } = { } ;
2019-12-04 06:43:22 -06:00
if ( options . range ) {
2019-11-15 09:38:25 -06:00
const startNs = this . getTime ( options . range . from , false ) ;
const endNs = this . getTime ( options . range . to , true ) ;
const rangeMs = Math . ceil ( ( endNs - startNs ) / 1 e6 ) ;
2021-08-16 07:02:13 -05:00
const resolution = target . resolution || ( DEFAULT_RESOLUTION . value as number ) ;
2021-01-14 08:46:51 -06:00
const adjustedInterval =
2021-08-16 07:02:13 -05:00
this . adjustInterval ( ( options as DataQueryRequest < LokiQuery > ) . intervalMs || 1000 , resolution , rangeMs ) / 1000 ;
2021-01-14 08:46:51 -06:00
// We want to ceil to 3 decimal places
const step = Math . ceil ( adjustedInterval * 1000 ) / 1000 ;
2019-05-10 07:00:39 -05:00
2019-11-15 09:38:25 -06:00
range = {
2021-07-12 04:25:41 -05:00
start : startNs ,
end : endNs ,
2019-11-15 09:38:25 -06:00
step ,
} ;
}
return {
. . . DEFAULT_QUERY_PARAMS ,
. . . range ,
query ,
2020-10-16 06:30:02 -05:00
limit ,
2019-11-15 09:38:25 -06:00
} ;
}
/ * *
2020-04-22 06:59:06 -05:00
* Attempts to send a query to / loki / api / v1 / query_range
2019-11-15 09:38:25 -06:00
* /
2020-04-22 06:59:06 -05:00
runRangeQuery = (
2019-11-15 09:38:25 -06:00
target : LokiQuery ,
options : RangeQueryOptions ,
responseListLength = 1
) : Observable < DataQueryResponse > = > {
2020-10-16 06:30:02 -05:00
// For metric query we use maxDataPoints from the request options which should be something like width of the
// visualisation in pixels. In case of logs request we either use lines limit defined in the query target or
// global limit defined for the data source which ever is lower.
let maxDataPoints = isMetricsQuery ( target . expr )
? // We fallback to maxLines here because maxDataPoints is defined as possibly undefined. Not sure that can
// actually happen both Dashboards and Explore should send some value here. If not maxLines does not make that
// much sense but nor any other arbitrary value.
( options as DataQueryRequest < LokiQuery > ) . maxDataPoints || this . maxLines
: // If user wants maxLines 0 we still fallback to data source limit. I think that makes sense as why would anyone
// want to do a query and not see any results?
target . maxLines || this . maxLines ;
2019-11-15 09:38:25 -06:00
2020-07-16 08:00:28 -05:00
if ( ( options as DataQueryRequest < LokiQuery > ) . liveStreaming ) {
2020-10-16 06:30:02 -05:00
return this . runLiveQuery ( target , maxDataPoints ) ;
2020-02-06 06:34:52 -06:00
}
2020-10-16 06:30:02 -05:00
const query = this . createRangeQuery ( target , options , maxDataPoints ) ;
2021-07-06 01:56:01 -05:00
2021-11-24 05:41:52 -06:00
const headers = target . volumeQuery ? { 'X-Query-Tags' : 'Source=logvolhist' } : undefined ;
2021-11-23 04:08:37 -06:00
return this . _request ( RANGE_QUERY_ENDPOINT , query , { headers } ) . pipe (
2021-09-06 08:28:54 -05:00
catchError ( ( err ) = > throwError ( ( ) = > this . processError ( err , target ) ) ) ,
switchMap ( ( response ) = >
2020-04-22 06:59:06 -05:00
processRangeQueryResponse (
response . data ,
target ,
query ,
responseListLength ,
2020-10-16 06:30:02 -05:00
maxDataPoints ,
2020-04-22 06:59:06 -05:00
this . instanceSettings . jsonData ,
2020-08-19 01:28:50 -05:00
( options as DataQueryRequest < LokiQuery > ) . scopedVars ,
2020-07-16 08:00:28 -05:00
( options as DataQueryRequest < LokiQuery > ) . reverse
2019-11-15 09:38:25 -06:00
)
)
2019-05-10 07:00:39 -05:00
) ;
2019-06-03 07:54:32 -05:00
} ;
2020-10-16 06:30:02 -05:00
createLiveTarget ( target : LokiQuery , maxDataPoints : number ) : LokiLiveTarget {
2020-05-06 04:21:25 -05:00
const query = target . expr ;
2019-11-15 09:38:25 -06:00
const baseUrl = this . instanceSettings . url ;
const params = serializeParams ( { query } ) ;
return {
query ,
url : convertToWebSocketUrl ( ` ${ baseUrl } /loki/api/v1/tail? ${ params } ` ) ,
refId : target.refId ,
2020-10-16 06:30:02 -05:00
size : maxDataPoints ,
2019-11-15 09:38:25 -06:00
} ;
2018-07-20 10:07:17 -05:00
}
2019-11-15 09:38:25 -06:00
/ * *
* Runs live queries which in this case means creating a websocket and listening on it for new logs .
* This returns a bit different dataFrame than runQueries as it returns single dataframe even if there are multiple
* Loki streams , sets only common labels on dataframe . labels and has additional dataframe . fields . labels for unique
* labels per row .
* /
2020-10-16 06:30:02 -05:00
runLiveQuery = ( target : LokiQuery , maxDataPoints : number ) : Observable < DataQueryResponse > = > {
const liveTarget = this . createLiveTarget ( target , maxDataPoints ) ;
2019-11-15 09:38:25 -06:00
2020-04-22 06:59:06 -05:00
return this . streams . getStream ( liveTarget ) . pipe (
2021-01-20 00:59:48 -06:00
map ( ( data ) = > ( {
2020-12-07 03:13:57 -06:00
data : data || [ ] ,
2019-11-15 09:38:25 -06:00
key : ` loki- ${ liveTarget . refId } ` ,
state : LoadingState.Streaming ,
2021-01-25 10:05:27 -06:00
} ) ) ,
catchError ( ( err : any ) = > {
2021-09-06 08:28:54 -05:00
return throwError ( ( ) = > ` Live tailing was stopped due to following error: ${ err . reason } ` ) ;
2021-01-25 10:05:27 -06:00
} )
2019-11-15 09:38:25 -06:00
) ;
} ;
2021-07-06 01:56:01 -05:00
getRangeScopedVars ( range : TimeRange = this . timeSrv . timeRange ( ) ) {
const msRange = range . to . diff ( range . from ) ;
const sRange = Math . round ( msRange / 1000 ) ;
return {
__range_ms : { text : msRange , value : msRange } ,
__range_s : { text : sRange , value : sRange } ,
__range : { text : sRange + 's' , value : sRange + 's' } ,
} ;
}
2020-01-24 02:50:09 -06:00
interpolateVariablesInQueries ( queries : LokiQuery [ ] , scopedVars : ScopedVars ) : LokiQuery [ ] {
2019-10-08 10:01:20 -05:00
let expandedQueries = queries ;
2019-11-15 09:38:25 -06:00
if ( queries && queries . length ) {
2021-01-20 00:59:48 -06:00
expandedQueries = queries . map ( ( query ) = > ( {
2019-11-15 09:38:25 -06:00
. . . query ,
2021-10-29 12:57:24 -05:00
datasource : this.getRef ( ) ,
2020-01-24 02:50:09 -06:00
expr : this.templateSrv.replace ( query . expr , scopedVars , this . interpolateQueryExpr ) ,
2019-11-15 09:38:25 -06:00
} ) ) ;
2019-10-08 10:01:20 -05:00
}
2019-11-15 09:38:25 -06:00
2019-10-08 10:01:20 -05:00
return expandedQueries ;
}
Explore: Rich History (#22570)
* Explore: Refactor active buttons css
* Explore: Add query history button
* WIP: Creating drawer
* WIP: Create custom drawer (for now)
* Revert changes to Drawer.tsx
* WIP: Layout finished
* Rich History: Set up boilerplate for Settings
* WIP: Query history cards
* Refactor, split components
* Add resizability, interactivity
* Save history to local storage
* Visualise queries from queryhistory local storage
* Set up query history settings
* Refactor
* Create link, un-refactored verison
* Copyable url
* WIP: Add slider
* Commenting feature
* Slider filtration
* Add headings
* Hide Rich history behind feature toggle
* Cleaning up, refactors
* Update tests
* Implement getQueryDisplayText
* Update lockfile for new dependencies
* Update heading based on sorting
* Fix typescript strinctNullCheck errors
* Fix Forms, new forms
* Fixes based on provided feedback
* Fixes, splitting component into two
* Add tooltips, add delete history button
* Clicking on card adds queries to query rows
* Delete history, width of drawers
* UI/UX changes, updates
* Add number of queries to headings, box shadows
* Fix slider, remove feature toggle
* Fix typo in the beta announcement
* Fix how the rich history state is rendered when initialization
* Move updateFilters when activeDatasourceFilter onlyto RichHistory, remove duplicated code
* Fix typescript strictnull errors, not used variables errors
2020-03-10 09:08:15 -05:00
getQueryDisplayText ( query : LokiQuery ) {
return query . expr ;
}
2021-03-02 09:58:14 -06:00
getTimeRangeParams() {
const timeRange = this . timeSrv . timeRange ( ) ;
2021-06-29 11:06:42 -05:00
return { start : timeRange.from.valueOf ( ) * NS_IN_MS , end : timeRange.to.valueOf ( ) * NS_IN_MS } ;
2021-03-02 09:58:14 -06:00
}
2021-12-14 07:36:47 -06:00
async importFromAbstractQueries ( abstractQueries : AbstractQuery [ ] ) : Promise < LokiQuery [ ] > {
await this . languageProvider . start ( ) ;
const existingKeys = this . languageProvider . labelKeys ;
if ( existingKeys && existingKeys . length ) {
abstractQueries = abstractQueries . map ( ( abstractQuery ) = > {
abstractQuery . labelMatchers = abstractQuery . labelMatchers . filter ( ( labelMatcher ) = > {
return existingKeys . includes ( labelMatcher . name ) ;
} ) ;
return abstractQuery ;
} ) ;
}
return abstractQueries . map ( ( abstractQuery ) = > this . languageProvider . importFromAbstractQuery ( abstractQuery ) ) ;
}
async exportToAbstractQueries ( queries : LokiQuery [ ] ) : Promise < AbstractQuery [ ] > {
return queries . map ( ( query ) = > this . languageProvider . exportToAbstractQuery ( query ) ) ;
2018-11-13 09:35:20 -06:00
}
2020-08-14 03:33:37 -05:00
async metadataRequest ( url : string , params? : Record < string , string | number > ) {
2021-08-18 23:38:31 -05:00
const res = await lastValueFrom ( this . _request ( url , params , { hideFromInspector : true } ) ) ;
2020-03-04 10:17:02 -06:00
return res . data . data || res . data . values || [ ] ;
2018-07-20 10:07:17 -05:00
}
2021-03-02 09:58:14 -06:00
async metricFindQuery ( query : string ) {
2019-12-03 02:40:22 -06:00
if ( ! query ) {
return Promise . resolve ( [ ] ) ;
}
2021-07-06 01:56:01 -05:00
2019-12-03 02:40:22 -06:00
const interpolated = this . templateSrv . replace ( query , { } , this . interpolateQueryExpr ) ;
2021-03-02 09:58:14 -06:00
return await this . processMetricFindQuery ( interpolated ) ;
2019-12-03 02:40:22 -06:00
}
2021-03-02 09:58:14 -06:00
async processMetricFindQuery ( query : string ) {
2019-12-03 02:40:22 -06:00
const labelNamesRegex = /^label_names\(\)\s*$/ ;
const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/ ;
const labelNames = query . match ( labelNamesRegex ) ;
if ( labelNames ) {
2021-06-11 03:57:40 -05:00
return await this . labelNamesQuery ( ) ;
2019-12-03 02:40:22 -06:00
}
const labelValues = query . match ( labelValuesRegex ) ;
if ( labelValues ) {
2021-06-11 03:57:40 -05:00
// If we have query expr, use /series endpoint
if ( labelValues [ 1 ] ) {
return await this . labelValuesSeriesQuery ( labelValues [ 1 ] , labelValues [ 2 ] ) ;
}
return await this . labelValuesQuery ( labelValues [ 2 ] ) ;
2019-12-03 02:40:22 -06:00
}
return Promise . resolve ( [ ] ) ;
}
2021-06-11 03:57:40 -05:00
async labelNamesQuery() {
2020-04-22 06:59:06 -05:00
const url = ` ${ LOKI_ENDPOINT } /label ` ;
2021-06-11 03:57:40 -05:00
const params = this . getTimeRangeParams ( ) ;
2020-08-14 03:33:37 -05:00
const result = await this . metadataRequest ( url , params ) ;
2020-03-04 10:17:02 -06:00
return result . map ( ( value : string ) = > ( { text : value } ) ) ;
2019-12-03 02:40:22 -06:00
}
2021-06-11 03:57:40 -05:00
async labelValuesQuery ( label : string ) {
const params = this . getTimeRangeParams ( ) ;
2020-04-22 06:59:06 -05:00
const url = ` ${ LOKI_ENDPOINT } /label/ ${ label } /values ` ;
2020-08-14 03:33:37 -05:00
const result = await this . metadataRequest ( url , params ) ;
2020-03-04 10:17:02 -06:00
return result . map ( ( value : string ) = > ( { text : value } ) ) ;
2019-12-03 02:40:22 -06:00
}
2021-06-11 03:57:40 -05:00
async labelValuesSeriesQuery ( expr : string , label : string ) {
const timeParams = this . getTimeRangeParams ( ) ;
const params = {
. . . timeParams ,
2021-09-07 08:44:45 -05:00
'match[]' : expr ,
2021-06-11 03:57:40 -05:00
} ;
const url = ` ${ LOKI_ENDPOINT } /series ` ;
const streams = new Set ( ) ;
const result = await this . metadataRequest ( url , params ) ;
result . forEach ( ( stream : { [ key : string ] : string } ) = > {
if ( stream [ label ] ) {
streams . add ( { text : stream [ label ] } ) ;
}
} ) ;
return Array . from ( streams ) ;
}
2021-07-06 03:30:27 -05:00
// By implementing getTagKeys and getTagValues we add ad-hoc filtters functionality
async getTagKeys() {
return await this . labelNamesQuery ( ) ;
}
async getTagValues ( options : any = { } ) {
return await this . labelValuesQuery ( options . key ) ;
}
2019-11-06 10:29:44 -06:00
interpolateQueryExpr ( value : any , variable : any ) {
// if no multi or include all do not regexEscape
if ( ! variable . multi && ! variable . includeAll ) {
return lokiRegularEscape ( value ) ;
}
if ( typeof value === 'string' ) {
return lokiSpecialRegexEscape ( value ) ;
}
const escapedValues = lodashMap ( value , lokiSpecialRegexEscape ) ;
return escapedValues . join ( '|' ) ;
}
2019-01-18 11:14:27 -06:00
modifyQuery ( query : LokiQuery , action : any ) : LokiQuery {
2020-05-04 11:16:54 -05:00
let expression = query . expr ? ? '' ;
2018-11-28 03:46:35 -06:00
switch ( action . type ) {
case 'ADD_FILTER' : {
2021-08-25 09:08:41 -05:00
expression = this . addLabelToQuery ( expression , action . key , action . value , '=' ) ;
2018-11-28 03:46:35 -06:00
break ;
}
2019-11-01 04:01:00 -05:00
case 'ADD_FILTER_OUT' : {
2021-08-25 09:08:41 -05:00
expression = this . addLabelToQuery ( expression , action . key , action . value , '!=' ) ;
2019-11-01 04:01:00 -05:00
break ;
}
2018-11-28 03:46:35 -06:00
default :
break ;
}
return { . . . query , expr : expression } ;
}
2019-06-18 04:01:12 -05:00
getTime ( date : string | DateTime , roundUp : boolean ) {
2019-11-15 09:38:25 -06:00
if ( typeof date === 'string' ) {
2020-07-06 14:16:27 -05:00
date = dateMath . parse ( date , roundUp ) ! ;
2018-07-20 10:07:17 -05:00
}
2019-11-15 09:38:25 -06:00
2018-07-20 10:07:17 -05:00
return Math . ceil ( date . valueOf ( ) * 1 e6 ) ;
}
2020-04-25 15:48:20 -05:00
getLogRowContext = ( row : LogRowModel , options? : RowContextOptions ) : Promise < { data : DataFrame [ ] } > = > {
2019-11-15 09:38:25 -06:00
const target = this . prepareLogRowContextQueryTarget (
row ,
( options && options . limit ) || 10 ,
( options && options . direction ) || 'BACKWARD'
) ;
const reverse = options && options . direction === 'FORWARD' ;
2021-08-18 23:38:31 -05:00
return lastValueFrom (
this . _request ( RANGE_QUERY_ENDPOINT , target ) . pipe (
2021-09-06 08:28:54 -05:00
catchError ( ( err ) = > {
2019-11-15 09:38:25 -06:00
const error : DataQueryError = {
message : 'Error during context query. Please check JS console logs.' ,
status : err.status ,
statusText : err.statusText ,
} ;
throw error ;
} ) ,
2021-09-06 08:28:54 -05:00
switchMap ( ( res ) = >
2020-04-22 06:59:06 -05:00
of ( {
2021-09-06 08:28:54 -05:00
data : res.data
? res . data . data . result . map ( ( stream : LokiStreamResult ) = > lokiStreamResultToDataFrame ( stream , reverse ) )
: [ ] ,
2020-04-22 06:59:06 -05:00
} )
2019-11-15 09:38:25 -06:00
)
)
2021-08-18 23:38:31 -05:00
) ;
2019-11-15 09:38:25 -06:00
} ;
2019-05-22 16:10:05 -05:00
prepareLogRowContextQueryTarget = ( row : LogRowModel , limit : number , direction : 'BACKWARD' | 'FORWARD' ) = > {
2021-09-28 03:42:38 -05:00
const labels = this . languageProvider . getLabelKeys ( ) ;
2019-05-20 01:44:37 -05:00
const query = Object . keys ( row . labels )
2021-09-28 03:42:38 -05:00
. map ( ( label : string ) = > {
if ( labels . includes ( label ) ) {
// escape backslashes in label as users can't escape them by themselves
return ` ${ label } =" ${ row . labels [ label ] . replace ( /\\/g , '\\\\' ) } " ` ;
}
return '' ;
} )
// Filter empty strings
. filter ( ( label ) = > ! ! label )
2019-05-20 01:44:37 -05:00
. join ( ',' ) ;
2019-11-15 09:38:25 -06:00
2020-01-26 16:13:56 -06:00
const contextTimeBuffer = 2 * 60 * 60 * 1000 ; // 2h buffer
2019-11-15 09:38:25 -06:00
const commonTargetOptions = {
2019-05-20 01:44:37 -05:00
limit ,
query : ` { ${ query } } ` ,
2019-11-15 09:38:25 -06:00
expr : ` { ${ query } } ` ,
2019-05-22 16:10:05 -05:00
direction ,
2019-05-20 01:44:37 -05:00
} ;
2019-05-22 16:10:05 -05:00
2020-01-26 16:13:56 -06:00
const fieldCache = new FieldCache ( row . dataFrame ) ;
const nsField = fieldCache . getFieldByName ( 'tsNs' ) ! ;
const nsTimestamp = nsField . values . get ( row . rowIndex ) ;
2019-05-22 16:10:05 -05:00
if ( direction === 'BACKWARD' ) {
return {
2019-11-15 09:38:25 -06:00
. . . commonTargetOptions ,
2020-01-26 16:13:56 -06:00
// convert to ns, we loose some precision here but it is not that important at the far points of the context
start : row.timeEpochMs - contextTimeBuffer + '000000' ,
end : nsTimestamp ,
2019-05-22 16:10:05 -05:00
direction ,
} ;
} else {
return {
2019-11-15 09:38:25 -06:00
. . . commonTargetOptions ,
2020-01-26 16:13:56 -06:00
// start param in Loki API is inclusive so we'll have to filter out the row that this request is based from
// and any other that were logged in the same ns but before the row. Right now these rows will be lost
// because the are before but came it he response that should return only rows after.
start : nsTimestamp ,
// convert to ns, we loose some precision here but it is not that important at the far points of the context
end : row.timeEpochMs + contextTimeBuffer + '000000' ,
2019-05-22 16:10:05 -05:00
} ;
}
2019-05-20 01:44:37 -05:00
} ;
2018-07-20 10:07:17 -05:00
testDatasource() {
2019-08-03 09:58:08 -05:00
// Consider only last 10 minutes otherwise request takes too long
const startMs = Date . now ( ) - 10 * 60 * 1000 ;
const start = ` ${ startMs } 000000 ` ; // API expects nanoseconds
2021-08-18 23:38:31 -05:00
return lastValueFrom (
this . _request ( ` ${ LOKI_ENDPOINT } /label ` , { start } ) . pipe (
2021-01-20 00:59:48 -06:00
map ( ( res ) = > {
2019-12-09 09:45:56 -06:00
const values : any [ ] = res ? . data ? . data || res ? . data ? . values || [ ] ;
const testResult =
values . length > 0
? { status : 'success' , message : 'Data source connected and labels found.' }
: {
status : 'error' ,
message :
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.' ,
} ;
return testResult ;
} ) ,
2019-11-15 09:38:25 -06:00
catchError ( ( err : any ) = > {
let message = 'Loki: ' ;
if ( err . statusText ) {
message += err . statusText ;
} else {
message += 'Cannot connect to Loki' ;
}
2018-12-05 17:19:55 -06:00
2019-11-15 09:38:25 -06:00
if ( err . status ) {
message += ` . ${ err . status } ` ;
}
if ( err . data && err . data . message ) {
message += ` . ${ err . data . message } ` ;
} else if ( err . data ) {
message += ` . ${ err . data } ` ;
}
return of ( { status : 'error' , message : message } ) ;
} )
)
2021-08-18 23:38:31 -05:00
) ;
2018-07-20 10:07:17 -05:00
}
2019-09-10 04:04:44 -05:00
2021-05-28 03:12:03 -05:00
async annotationQuery ( options : any ) : Promise < AnnotationEvent [ ] > {
2021-08-16 07:02:13 -05:00
const {
expr ,
maxLines ,
instant ,
stepInterval ,
tagKeys = '' ,
titleFormat = '' ,
textFormat = '' ,
} = options . annotation ;
2021-02-16 06:27:17 -06:00
if ( ! expr ) {
2019-09-10 04:04:44 -05:00
return [ ] ;
}
2021-02-16 06:27:17 -06:00
const interpolatedExpr = this . templateSrv . replace ( expr , { } , this . interpolateQueryExpr ) ;
2021-08-16 07:02:13 -05:00
const query = {
refId : ` annotation- ${ options . annotation . name } ` ,
expr : interpolatedExpr ,
maxLines ,
instant ,
stepInterval ,
2022-01-07 12:00:11 -06:00
queryType : instant ? LokiQueryType.Instant : LokiQueryType.Range ,
2021-08-16 07:02:13 -05:00
} ;
2021-02-16 06:27:17 -06:00
const { data } = instant
2021-08-18 23:38:31 -05:00
? await lastValueFrom ( this . runInstantQuery ( query , options as any ) )
: await lastValueFrom ( this . runRangeQuery ( query , options as any ) ) ;
2021-02-16 06:27:17 -06:00
2019-09-10 04:04:44 -05:00
const annotations : AnnotationEvent [ ] = [ ] ;
2021-05-28 03:12:03 -05:00
const splitKeys : string [ ] = tagKeys . split ( ',' ) . filter ( ( v : string ) = > v !== '' ) ;
2019-09-12 10:28:46 -05:00
2019-09-10 04:04:44 -05:00
for ( const frame of data ) {
2021-05-28 03:12:03 -05:00
const labels : { [ key : string ] : string } = { } ;
2019-11-07 09:50:45 -06:00
for ( const field of frame . fields ) {
if ( field . labels ) {
2021-05-28 03:12:03 -05:00
for ( const [ key , value ] of Object . entries ( field . labels ) ) {
labels [ key ] = String ( value ) . trim ( ) ;
}
2019-11-07 09:50:45 -06:00
}
}
2021-05-28 03:12:03 -05:00
const tags : string [ ] = [
. . . new Set (
Object . entries ( labels ) . reduce ( ( acc : string [ ] , [ key , val ] ) = > {
if ( val === '' ) {
return acc ;
}
if ( splitKeys . length && ! splitKeys . includes ( key ) ) {
return acc ;
}
acc . push . apply ( acc , [ val ] ) ;
return acc ;
} , [ ] )
) ,
] ;
2019-09-10 04:04:44 -05:00
const view = new DataFrameView < { ts : string ; line : string } > ( frame ) ;
2019-09-12 10:28:46 -05:00
2021-01-20 00:59:48 -06:00
view . forEach ( ( row ) = > {
2019-09-10 04:04:44 -05:00
annotations . push ( {
time : new Date ( row . ts ) . valueOf ( ) ,
2021-12-14 21:19:16 -06:00
title : renderLegendFormat ( titleFormat , labels ) ,
text : renderLegendFormat ( textFormat , labels ) || row . line ,
2019-09-10 04:04:44 -05:00
tags ,
} ) ;
} ) ;
}
return annotations ;
}
2019-11-06 09:15:08 -06:00
2020-07-06 14:16:27 -05:00
showContextToggle ( row? : LogRowModel ) : boolean {
return ( row && row . searchWords && row . searchWords . length > 0 ) === true ;
}
2020-04-25 15:48:20 -05:00
2020-07-16 00:45:26 -05:00
processError ( err : FetchError , target : LokiQuery ) {
let error = cloneDeep ( err ) ;
if ( err . data . message . includes ( 'escape' ) && target . expr . includes ( '\\' ) ) {
2020-11-11 13:00:47 -06:00
error . data . message = ` Error: ${ err . data . message } . Make sure that all special characters are escaped with \\ . For more information on escaping of special characters visit LogQL documentation at https://grafana.com/docs/loki/latest/logql/. ` ;
2019-11-15 09:38:25 -06:00
}
return error ;
2020-07-16 00:45:26 -05:00
}
2019-11-15 09:38:25 -06:00
2021-08-16 07:02:13 -05:00
adjustInterval ( dynamicInterval : number , resolution : number , range : number ) {
2019-11-15 09:38:25 -06:00
// Loki will drop queries that might return more than 11000 data points.
// Calibrate interval if it is too small.
2021-08-16 07:02:13 -05:00
let safeInterval = range / 11000 ;
if ( safeInterval > 1 ) {
safeInterval = Math . ceil ( safeInterval ) ;
2019-11-15 09:38:25 -06:00
}
2021-08-16 07:02:13 -05:00
let adjustedInterval = Math . max ( resolution * dynamicInterval , safeInterval ) ;
return adjustedInterval ;
2019-11-15 09:38:25 -06:00
}
2021-07-06 03:30:27 -05:00
addAdHocFilters ( queryExpr : string ) {
const adhocFilters = this . templateSrv . getAdhocFilters ( this . name ) ;
let expr = queryExpr ;
expr = adhocFilters . reduce ( ( acc : string , filter : { key? : any ; operator? : any ; value? : any } ) = > {
const { key , operator } = filter ;
let { value } = filter ;
if ( operator === '=~' || operator === '!~' ) {
value = lokiRegularEscape ( value ) ;
}
2021-08-25 09:08:41 -05:00
2021-12-03 07:11:43 -06:00
return this . addLabelToQuery ( acc , key , value , operator , true ) ;
2021-07-06 03:30:27 -05:00
} , expr ) ;
return expr ;
}
2021-08-25 09:08:41 -05:00
2021-12-03 07:11:43 -06:00
addLabelToQuery (
queryExpr : string ,
key : string ,
value : string | number ,
operator : string ,
// Override to make sure that we use label as actual label and not parsed label
notParsedLabelOverride? : boolean
) {
if ( queryHasPipeParser ( queryExpr ) && ! isMetricsQuery ( queryExpr ) && ! notParsedLabelOverride ) {
2021-08-25 09:08:41 -05:00
// If query has parser, we treat all labels as parsed and use | key="value" syntax
return addParsedLabelToQuery ( queryExpr , key , value , operator ) ;
} else {
return addLabelToQuery ( queryExpr , key , value , operator , true ) ;
}
}
2022-02-03 04:40:19 -06:00
2022-02-07 01:43:48 -06:00
// Used when running queries through backend
filterQuery ( query : LokiQuery ) : boolean {
if ( query . hide || query . expr === '' ) {
return false ;
}
return true ;
}
// Used when running queries through backend
applyTemplateVariables ( target : LokiQuery , scopedVars : ScopedVars ) : Record < string , any > {
// We want to interpolate these variables on backend
const { __interval , __interval_ms , . . . rest } = scopedVars ;
return {
. . . target ,
legendFormat : this.templateSrv.replace ( target . legendFormat , rest ) ,
expr : this.templateSrv.replace ( target . expr , rest , this . interpolateQueryExpr ) ,
} ;
}
2022-02-03 04:40:19 -06:00
interpolateString ( string : string ) {
return this . templateSrv . replace ( string , undefined , this . interpolateQueryExpr ) ;
}
getVariables ( ) : string [ ] {
return this . templateSrv . getVariables ( ) . map ( ( v ) = > ` $ ${ v . name } ` ) ;
}
2018-07-20 10:07:17 -05:00
}
2019-01-23 10:44:22 -06:00
2019-11-06 10:29:44 -06:00
export function lokiRegularEscape ( value : any ) {
if ( typeof value === 'string' ) {
return value . replace ( /'/g , "\\\\'" ) ;
}
return value ;
}
export function lokiSpecialRegexEscape ( value : any ) {
if ( typeof value === 'string' ) {
return lokiRegularEscape ( value . replace ( /\\/g , '\\\\\\\\' ) . replace ( /[$^*{}\[\]+?.()|]/g , '\\\\$&' ) ) ;
}
return value ;
}
2020-10-16 06:30:02 -05:00
/ * *
* Checks if the query expression uses function and so should return a time series instead of logs .
* Sometimes important to know that before we actually do the query .
* /
2021-09-30 08:46:11 -05:00
export function isMetricsQuery ( query : string ) : boolean {
2020-10-16 06:30:02 -05:00
const tokens = Prism . tokenize ( query , syntax ) ;
2021-01-20 00:59:48 -06:00
return tokens . some ( ( t ) = > {
2020-10-16 06:30:02 -05:00
// Not sure in which cases it can be string maybe if nothing matched which means it should not be a function
return typeof t !== 'string' && t . type === 'function' ;
} ) ;
}
2021-11-02 05:53:47 -05:00
function extractLevel ( dataFrame : DataFrame ) : LogLevel {
let valueField ;
try {
valueField = new FieldCache ( dataFrame ) . getFirstFieldOfType ( FieldType . number ) ;
} catch { }
return valueField ? . labels ? getLogLevelFromLabels ( valueField . labels ) : LogLevel . unknown ;
}
function getLogLevelFromLabels ( labels : Labels ) : LogLevel {
const labelNames = [ 'level' , 'lvl' , 'loglevel' ] ;
let levelLabel ;
for ( let labelName of labelNames ) {
if ( labelName in labels ) {
levelLabel = labelName ;
break ;
}
}
return levelLabel ? getLogLevelFromKey ( labels [ levelLabel ] ) : LogLevel . unknown ;
}
2019-01-23 10:44:22 -06:00
export default LokiDatasource ;