|
|
|
|
@ -2,6 +2,7 @@ |
|
|
|
|
import { cloneDeep, isEmpty, map as lodashMap } from 'lodash'; |
|
|
|
|
import { merge, Observable, of } from 'rxjs'; |
|
|
|
|
import { catchError, map, switchMap } from 'rxjs/operators'; |
|
|
|
|
import Prism from 'prismjs'; |
|
|
|
|
|
|
|
|
|
// Types
|
|
|
|
|
import { |
|
|
|
|
@ -44,6 +45,7 @@ import { LiveStreams, LokiLiveTarget } from './live_streams'; |
|
|
|
|
import LanguageProvider, { rangeToParams } from './language_provider'; |
|
|
|
|
import { serializeParams } from '../../../core/utils/fetch'; |
|
|
|
|
import { RowContextOptions } from '@grafana/ui/src/components/Logs/LogRowContextProvider'; |
|
|
|
|
import syntax from './syntax'; |
|
|
|
|
|
|
|
|
|
export type RangeQueryOptions = DataQueryRequest<LokiQuery> | AnnotationQueryRequest<LokiQuery>; |
|
|
|
|
export const DEFAULT_MAX_LINES = 1000; |
|
|
|
|
@ -148,7 +150,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
createRangeQuery(target: LokiQuery, options: RangeQueryOptions): LokiRangeQueryRequest { |
|
|
|
|
createRangeQuery(target: LokiQuery, options: RangeQueryOptions, limit: number): LokiRangeQueryRequest { |
|
|
|
|
const query = target.expr; |
|
|
|
|
let range: { start?: number; end?: number; step?: number } = {}; |
|
|
|
|
if (options.range) { |
|
|
|
|
@ -174,7 +176,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { |
|
|
|
|
...DEFAULT_QUERY_PARAMS, |
|
|
|
|
...range, |
|
|
|
|
query, |
|
|
|
|
limit: Math.min((options as DataQueryRequest<LokiQuery>).maxDataPoints || Infinity, this.maxLines), |
|
|
|
|
limit, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -186,31 +188,22 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { |
|
|
|
|
options: RangeQueryOptions, |
|
|
|
|
responseListLength = 1 |
|
|
|
|
): Observable<DataQueryResponse> => { |
|
|
|
|
// target.maxLines value already preprocessed
|
|
|
|
|
// available cases:
|
|
|
|
|
// 1) empty input -> mapped to NaN, falls back to dataSource.maxLines limit
|
|
|
|
|
// 2) input with at least 1 character and that is either incorrect (value in the input field is not a number) or negative
|
|
|
|
|
// - mapped to 0, falls back to the limit of 0 lines
|
|
|
|
|
// 3) default case - correct input, mapped to the value from the input field
|
|
|
|
|
|
|
|
|
|
let linesLimit = 0; |
|
|
|
|
if (target.maxLines === undefined) { |
|
|
|
|
// no target.maxLines, using options.maxDataPoints
|
|
|
|
|
linesLimit = Math.min((options as DataQueryRequest<LokiQuery>).maxDataPoints || Infinity, this.maxLines); |
|
|
|
|
} else { |
|
|
|
|
// using target.maxLines
|
|
|
|
|
if (isNaN(target.maxLines)) { |
|
|
|
|
linesLimit = this.maxLines; |
|
|
|
|
} else { |
|
|
|
|
linesLimit = target.maxLines; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// 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; |
|
|
|
|
|
|
|
|
|
const queryOptions = { ...options, maxDataPoints: linesLimit }; |
|
|
|
|
if ((options as DataQueryRequest<LokiQuery>).liveStreaming) { |
|
|
|
|
return this.runLiveQuery(target, queryOptions); |
|
|
|
|
return this.runLiveQuery(target, maxDataPoints); |
|
|
|
|
} |
|
|
|
|
const query = this.createRangeQuery(target, queryOptions); |
|
|
|
|
const query = this.createRangeQuery(target, options, maxDataPoints); |
|
|
|
|
return this._request(RANGE_QUERY_ENDPOINT, query).pipe( |
|
|
|
|
catchError((err: any) => this.throwUnless(err, err.status === 404, target)), |
|
|
|
|
switchMap((response: { data: LokiResponse; status: number }) => |
|
|
|
|
@ -219,7 +212,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { |
|
|
|
|
target, |
|
|
|
|
query, |
|
|
|
|
responseListLength, |
|
|
|
|
linesLimit, |
|
|
|
|
maxDataPoints, |
|
|
|
|
this.instanceSettings.jsonData, |
|
|
|
|
(options as DataQueryRequest<LokiQuery>).scopedVars, |
|
|
|
|
(options as DataQueryRequest<LokiQuery>).reverse |
|
|
|
|
@ -228,7 +221,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { |
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
createLiveTarget(target: LokiQuery, options: { maxDataPoints?: number }): LokiLiveTarget { |
|
|
|
|
createLiveTarget(target: LokiQuery, maxDataPoints: number): LokiLiveTarget { |
|
|
|
|
const query = target.expr; |
|
|
|
|
const baseUrl = this.instanceSettings.url; |
|
|
|
|
const params = serializeParams({ query }); |
|
|
|
|
@ -237,7 +230,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { |
|
|
|
|
query, |
|
|
|
|
url: convertToWebSocketUrl(`${baseUrl}/loki/api/v1/tail?${params}`), |
|
|
|
|
refId: target.refId, |
|
|
|
|
size: Math.min(options.maxDataPoints || Infinity, this.maxLines), |
|
|
|
|
size: maxDataPoints, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -247,8 +240,8 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> { |
|
|
|
|
* Loki streams, sets only common labels on dataframe.labels and has additional dataframe.fields.labels for unique |
|
|
|
|
* labels per row. |
|
|
|
|
*/ |
|
|
|
|
runLiveQuery = (target: LokiQuery, options: { maxDataPoints?: number }): Observable<DataQueryResponse> => { |
|
|
|
|
const liveTarget = this.createLiveTarget(target, options); |
|
|
|
|
runLiveQuery = (target: LokiQuery, maxDataPoints: number): Observable<DataQueryResponse> => { |
|
|
|
|
const liveTarget = this.createLiveTarget(target, maxDataPoints); |
|
|
|
|
|
|
|
|
|
return this.streams.getStream(liveTarget).pipe( |
|
|
|
|
map(data => ({ |
|
|
|
|
@ -554,4 +547,16 @@ export function lokiSpecialRegexEscape(value: any) { |
|
|
|
|
return value; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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. |
|
|
|
|
*/ |
|
|
|
|
function isMetricsQuery(query: string): boolean { |
|
|
|
|
const tokens = Prism.tokenize(query, syntax); |
|
|
|
|
return tokens.some(t => { |
|
|
|
|
// 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'; |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export default LokiDatasource; |
|
|
|
|
|