mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Grafana/Loki: Adds support for new Loki endpoints and metrics (#20158)
* Grafana/Loki: Adds support for new Loki endpoints and metrics * Adds `/loki/` prefix to new loki endpoints and updates response interfaces * Improved legacy support * Removed changes related to plugin.json and added Loki-specific hacks * Fixes live streaming for legacy loki datasources
This commit is contained in:
parent
1248457fee
commit
e0a2d4beac
@ -26,22 +26,24 @@ export class DataFrameView<T = any> implements Vector<T> {
|
|||||||
|
|
||||||
constructor(private data: DataFrame) {
|
constructor(private data: DataFrame) {
|
||||||
const obj = ({} as unknown) as T;
|
const obj = ({} as unknown) as T;
|
||||||
|
|
||||||
for (let i = 0; i < data.fields.length; i++) {
|
for (let i = 0; i < data.fields.length; i++) {
|
||||||
const field = data.fields[i];
|
const field = data.fields[i];
|
||||||
const getter = () => {
|
const getter = () => field.values.get(this.index);
|
||||||
return field.values.get(this.index);
|
|
||||||
};
|
|
||||||
if (!(obj as any).hasOwnProperty(field.name)) {
|
if (!(obj as any).hasOwnProperty(field.name)) {
|
||||||
Object.defineProperty(obj, field.name, {
|
Object.defineProperty(obj, field.name, {
|
||||||
enumerable: true, // Shows up as enumerable property
|
enumerable: true, // Shows up as enumerable property
|
||||||
get: getter,
|
get: getter,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(obj, i, {
|
Object.defineProperty(obj, i, {
|
||||||
enumerable: false, // Don't enumerate array index
|
enumerable: false, // Don't enumerate array index
|
||||||
get: getter,
|
get: getter,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.obj = obj;
|
this.obj = obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,11 +61,7 @@ export class DataFrameView<T = any> implements Vector<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toArray(): T[] {
|
toArray(): T[] {
|
||||||
const arr: T[] = [];
|
return new Array(this.data.length).fill(0).map((_, i) => ({ ...this.get(i) }));
|
||||||
for (let i = 0; i < this.data.length; i++) {
|
|
||||||
arr.push({ ...this.get(i) });
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): T[] {
|
toJSON(): T[] {
|
||||||
|
@ -261,6 +261,8 @@ export abstract class DataSourceApi<
|
|||||||
*/
|
*/
|
||||||
languageProvider?: any;
|
languageProvider?: any;
|
||||||
|
|
||||||
|
getVersion?(): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible
|
* Can be optionally implemented to allow datasource to be a source of annotations for dashboard. To be visible
|
||||||
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
|
* in the annotation editor `annotations` capability also needs to be enabled in plugin.json.
|
||||||
@ -302,6 +304,7 @@ export interface ExploreQueryFieldProps<
|
|||||||
|
|
||||||
export interface ExploreStartPageProps {
|
export interface ExploreStartPageProps {
|
||||||
datasource?: DataSourceApi;
|
datasource?: DataSourceApi;
|
||||||
|
exploreMode: 'Logs' | 'Metrics';
|
||||||
onClickExample: (query: DataQuery) => void;
|
onClickExample: (query: DataQuery) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,18 +446,22 @@ export interface DataQueryError {
|
|||||||
|
|
||||||
export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
|
export interface DataQueryRequest<TQuery extends DataQuery = DataQuery> {
|
||||||
requestId: string; // Used to identify results and optionally cancel the request in backendSrv
|
requestId: string; // Used to identify results and optionally cancel the request in backendSrv
|
||||||
|
|
||||||
|
dashboardId: number;
|
||||||
|
interval: string;
|
||||||
|
intervalMs?: number;
|
||||||
|
maxDataPoints?: number;
|
||||||
|
panelId: number;
|
||||||
|
range?: TimeRange;
|
||||||
|
reverse?: boolean;
|
||||||
|
scopedVars: ScopedVars;
|
||||||
|
targets: TQuery[];
|
||||||
timezone: string;
|
timezone: string;
|
||||||
range: TimeRange;
|
|
||||||
|
cacheTimeout?: string;
|
||||||
|
exploreMode?: 'Logs' | 'Metrics';
|
||||||
rangeRaw?: RawTimeRange;
|
rangeRaw?: RawTimeRange;
|
||||||
timeInfo?: string; // The query time description (blue text in the upper right)
|
timeInfo?: string; // The query time description (blue text in the upper right)
|
||||||
targets: TQuery[];
|
|
||||||
panelId: number;
|
|
||||||
dashboardId: number;
|
|
||||||
cacheTimeout?: string;
|
|
||||||
interval: string;
|
|
||||||
intervalMs: number;
|
|
||||||
maxDataPoints: number;
|
|
||||||
scopedVars: ScopedVars;
|
|
||||||
|
|
||||||
// Request Timing
|
// Request Timing
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
@ -10,11 +10,10 @@ export type GraphSeriesValue = number | null;
|
|||||||
|
|
||||||
/** View model projection of a series */
|
/** View model projection of a series */
|
||||||
export interface GraphSeriesXY {
|
export interface GraphSeriesXY {
|
||||||
label: string;
|
|
||||||
color: string;
|
color: string;
|
||||||
data: GraphSeriesValue[][]; // [x,y][]
|
data: GraphSeriesValue[][]; // [x,y][]
|
||||||
info?: DisplayValue[]; // Legend info
|
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
label: string;
|
||||||
yAxis: YAxis;
|
yAxis: YAxis;
|
||||||
// Field with series' time values
|
// Field with series' time values
|
||||||
timeField: Field;
|
timeField: Field;
|
||||||
@ -22,6 +21,8 @@ export interface GraphSeriesXY {
|
|||||||
valueField: Field;
|
valueField: Field;
|
||||||
seriesIndex: number;
|
seriesIndex: number;
|
||||||
timeStep: number;
|
timeStep: number;
|
||||||
|
|
||||||
|
info?: DisplayValue[]; // Legend info
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreatePlotOverlay {
|
export interface CreatePlotOverlay {
|
||||||
|
@ -10,6 +10,17 @@ import { FolderInfo, DashboardDTO, CoreEvents } from 'app/types';
|
|||||||
import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime';
|
import { BackendSrv as BackendService, getBackendSrv as getBackendService, BackendSrvRequest } from '@grafana/runtime';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface DatasourceRequestOptions {
|
||||||
|
retry?: number;
|
||||||
|
method?: string;
|
||||||
|
requestId?: string;
|
||||||
|
timeout?: angular.IPromise<any>;
|
||||||
|
url?: string;
|
||||||
|
headers?: { [key: string]: any };
|
||||||
|
silent?: boolean;
|
||||||
|
data?: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
export class BackendSrv implements BackendService {
|
export class BackendSrv implements BackendService {
|
||||||
private inFlightRequests: { [key: string]: Array<angular.IDeferred<any>> } = {};
|
private inFlightRequests: { [key: string]: Array<angular.IDeferred<any>> } = {};
|
||||||
private HTTP_REQUEST_CANCELED = -1;
|
private HTTP_REQUEST_CANCELED = -1;
|
||||||
|
@ -148,6 +148,7 @@ export function buildQueryTransaction(
|
|||||||
__interval_ms: { text: intervalMs, value: intervalMs },
|
__interval_ms: { text: intervalMs, value: intervalMs },
|
||||||
},
|
},
|
||||||
maxDataPoints: queryOptions.maxDataPoints,
|
maxDataPoints: queryOptions.maxDataPoints,
|
||||||
|
exploreMode: queryOptions.mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -517,7 +518,7 @@ export const convertToWebSocketUrl = (url: string) => {
|
|||||||
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
let backend = `${protocol}${window.location.host}${config.appSubUrl}`;
|
let backend = `${protocol}${window.location.host}${config.appSubUrl}`;
|
||||||
if (backend.endsWith('/')) {
|
if (backend.endsWith('/')) {
|
||||||
backend = backend.slice(0, backend.length - 1);
|
backend = backend.slice(0, -1);
|
||||||
}
|
}
|
||||||
return `${backend}${url}`;
|
return `${backend}${url}`;
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ import { DashboardModel } from '../dashboard/state/DashboardModel';
|
|||||||
import DatasourceSrv from '../plugins/datasource_srv';
|
import DatasourceSrv from '../plugins/datasource_srv';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { TimeSrv } from '../dashboard/services/TimeSrv';
|
import { TimeSrv } from '../dashboard/services/TimeSrv';
|
||||||
import { DataSourceApi, PanelEvents, AnnotationEvent, AppEvents } from '@grafana/data';
|
import { DataSourceApi, PanelEvents, AnnotationEvent, AppEvents, PanelModel, TimeRange } from '@grafana/data';
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
|
|
||||||
export class AnnotationsSrv {
|
export class AnnotationsSrv {
|
||||||
@ -44,7 +44,7 @@ export class AnnotationsSrv {
|
|||||||
this.datasourcePromises = null;
|
this.datasourcePromises = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAnnotations(options: any) {
|
getAnnotations(options: { dashboard: DashboardModel; panel: PanelModel; range: TimeRange }) {
|
||||||
return this.$q
|
return this.$q
|
||||||
.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
|
.all([this.getGlobalAnnotations(options), this.getAlertStates(options)])
|
||||||
.then(results => {
|
.then(results => {
|
||||||
@ -104,7 +104,7 @@ export class AnnotationsSrv {
|
|||||||
return this.alertStatesPromise;
|
return this.alertStatesPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getGlobalAnnotations(options: any) {
|
getGlobalAnnotations(options: { dashboard: DashboardModel; panel: PanelModel; range: TimeRange }) {
|
||||||
const dashboard = options.dashboard;
|
const dashboard = options.dashboard;
|
||||||
|
|
||||||
if (this.globalAnnotationsPromise) {
|
if (this.globalAnnotationsPromise) {
|
||||||
@ -130,7 +130,7 @@ export class AnnotationsSrv {
|
|||||||
.then((datasource: DataSourceApi) => {
|
.then((datasource: DataSourceApi) => {
|
||||||
// issue query against data source
|
// issue query against data source
|
||||||
return datasource.annotationQuery({
|
return datasource.annotationQuery({
|
||||||
range: range,
|
range,
|
||||||
rangeRaw: range.raw,
|
rangeRaw: range.raw,
|
||||||
annotation: annotation,
|
annotation: annotation,
|
||||||
dashboard: dashboard,
|
dashboard: dashboard,
|
||||||
|
@ -78,7 +78,7 @@ export function processResponsePacket(packet: DataQueryResponse, state: RunningQ
|
|||||||
* It will
|
* It will
|
||||||
* * Merge multiple responses into a single DataFrame array based on the packet key
|
* * Merge multiple responses into a single DataFrame array based on the packet key
|
||||||
* * Will emit a loading state if no response after 50ms
|
* * Will emit a loading state if no response after 50ms
|
||||||
* * Cancel any still runnning network requests on unsubscribe (using request.requestId)
|
* * Cancel any still running network requests on unsubscribe (using request.requestId)
|
||||||
*/
|
*/
|
||||||
export function runRequest(datasource: DataSourceApi, request: DataQueryRequest): Observable<PanelData> {
|
export function runRequest(datasource: DataSourceApi, request: DataQueryRequest): Observable<PanelData> {
|
||||||
let state: RunningQueryState = {
|
let state: RunningQueryState = {
|
||||||
|
@ -83,7 +83,7 @@ export const getPanelMenu = (dashboard: DashboardModel, panel: PanelModel) => {
|
|||||||
const onNavigateToExplore = (event: React.MouseEvent<any>) => {
|
const onNavigateToExplore = (event: React.MouseEvent<any>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const openInNewWindow = event.ctrlKey || event.metaKey ? (url: string) => window.open(url) : undefined;
|
const openInNewWindow = event.ctrlKey || event.metaKey ? (url: string) => window.open(url) : undefined;
|
||||||
store.dispatch(navigateToExplore(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow }));
|
store.dispatch(navigateToExplore(panel, { getDataSourceSrv, getTimeSrv, getExploreUrl, openInNewWindow }) as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu: PanelMenuItem[] = [];
|
const menu: PanelMenuItem[] = [];
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
TimeZone,
|
TimeZone,
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExploreItemState,
|
ExploreItemState,
|
||||||
ExploreUrlState,
|
ExploreUrlState,
|
||||||
@ -288,7 +289,11 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
<ErrorBoundaryAlert>
|
<ErrorBoundaryAlert>
|
||||||
{showingStartPage && (
|
{showingStartPage && (
|
||||||
<div className="grafana-info-box grafana-info-box--max-lg">
|
<div className="grafana-info-box grafana-info-box--max-lg">
|
||||||
<StartPage onClickExample={this.onClickExample} datasource={datasourceInstance} />
|
<StartPage
|
||||||
|
onClickExample={this.onClickExample}
|
||||||
|
datasource={datasourceInstance}
|
||||||
|
exploreMode={mode}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!showingStartPage && (
|
{!showingStartPage && (
|
||||||
@ -373,6 +378,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
|||||||
const initialRange = urlRange ? getTimeRangeFromUrlMemoized(urlRange, timeZone).raw : DEFAULT_RANGE;
|
const initialRange = urlRange ? getTimeRangeFromUrlMemoized(urlRange, timeZone).raw : DEFAULT_RANGE;
|
||||||
|
|
||||||
let newMode: ExploreMode;
|
let newMode: ExploreMode;
|
||||||
|
|
||||||
if (supportedModes.length) {
|
if (supportedModes.length) {
|
||||||
const urlModeIsValid = supportedModes.includes(urlMode);
|
const urlModeIsValid = supportedModes.includes(urlMode);
|
||||||
const modeStateIsValid = supportedModes.includes(mode);
|
const modeStateIsValid = supportedModes.includes(mode);
|
||||||
@ -385,7 +391,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
|||||||
newMode = supportedModes[0];
|
newMode = supportedModes[0];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(mode) ? mode : ExploreMode.Metrics;
|
newMode = [ExploreMode.Metrics, ExploreMode.Logs].includes(urlMode) ? urlMode : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialUI = ui || DEFAULT_UI_STATE;
|
const initialUI = ui || DEFAULT_UI_STATE;
|
||||||
|
@ -347,7 +347,9 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
||||||
: undefined;
|
: undefined;
|
||||||
const hasLiveOption =
|
const hasLiveOption =
|
||||||
datasourceInstance && datasourceInstance.meta && datasourceInstance.meta.streaming ? true : false;
|
datasourceInstance && datasourceInstance.meta && datasourceInstance.meta.streaming && mode === ExploreMode.Logs
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
|
@ -186,6 +186,7 @@ export interface UpdateUIStatePayload extends Partial<ExploreUIState> {
|
|||||||
export interface UpdateDatasourceInstancePayload {
|
export interface UpdateDatasourceInstancePayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
datasourceInstance: DataSourceApi;
|
datasourceInstance: DataSourceApi;
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToggleLogLevelPayload {
|
export interface ToggleLogLevelPayload {
|
||||||
|
@ -123,8 +123,15 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun
|
|||||||
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
|
||||||
const queries = getState().explore[exploreId].queries;
|
const queries = getState().explore[exploreId].queries;
|
||||||
const orgId = getState().user.orgId;
|
const orgId = getState().user.orgId;
|
||||||
|
const datasourceVersion = newDataSourceInstance.getVersion && (await newDataSourceInstance.getVersion());
|
||||||
|
|
||||||
dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance }));
|
dispatch(
|
||||||
|
updateDatasourceInstanceAction({
|
||||||
|
exploreId,
|
||||||
|
datasourceInstance: newDataSourceInstance,
|
||||||
|
version: datasourceVersion,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
|
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance));
|
||||||
|
|
||||||
@ -436,6 +443,7 @@ export function runQueries(exploreId: ExploreId): ThunkResult<void> {
|
|||||||
liveStreaming: live,
|
liveStreaming: live,
|
||||||
showingGraph,
|
showingGraph,
|
||||||
showingTable,
|
showingTable,
|
||||||
|
mode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const datasourceId = datasourceInstance.meta.id;
|
const datasourceId = datasourceInstance.meta.id;
|
||||||
|
@ -275,30 +275,46 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
.addMapper({
|
.addMapper({
|
||||||
filter: updateDatasourceInstanceAction,
|
filter: updateDatasourceInstanceAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
const { datasourceInstance } = action.payload;
|
const { datasourceInstance, version } = action.payload;
|
||||||
const [supportedModes, mode] = getModesForDatasource(datasourceInstance, state.mode);
|
|
||||||
|
|
||||||
const originPanelId = state.urlState && state.urlState.originPanelId;
|
|
||||||
|
|
||||||
// Custom components
|
// Custom components
|
||||||
const StartPage = datasourceInstance.components.ExploreStartPage;
|
const StartPage = datasourceInstance.components.ExploreStartPage;
|
||||||
stopQueryState(state.querySubscription);
|
stopQueryState(state.querySubscription);
|
||||||
|
|
||||||
|
let newMetadata = datasourceInstance.meta;
|
||||||
|
|
||||||
|
// HACK: Temporary hack for Loki datasource. Can remove when plugin.json structure is changed.
|
||||||
|
if (version && version.length && datasourceInstance.meta.name === 'Loki') {
|
||||||
|
const lokiVersionMetadata: Record<string, { metrics: boolean }> = {
|
||||||
|
v0: {
|
||||||
|
metrics: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
v1: {
|
||||||
|
metrics: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
newMetadata = { ...newMetadata, ...lokiVersionMetadata[version] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedDatasourceInstance = Object.assign(datasourceInstance, { meta: newMetadata });
|
||||||
|
const [supportedModes, mode] = getModesForDatasource(updatedDatasourceInstance, state.mode);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
datasourceInstance,
|
datasourceInstance: updatedDatasourceInstance,
|
||||||
graphResult: null,
|
graphResult: null,
|
||||||
tableResult: null,
|
tableResult: null,
|
||||||
logsResult: null,
|
logsResult: null,
|
||||||
latency: 0,
|
latency: 0,
|
||||||
queryResponse: createEmptyQueryResponse(),
|
queryResponse: createEmptyQueryResponse(),
|
||||||
loading: false,
|
loading: false,
|
||||||
StartPage,
|
StartPage: datasourceInstance.components.ExploreStartPage,
|
||||||
showingStartPage: Boolean(StartPage),
|
showingStartPage: Boolean(StartPage),
|
||||||
queryKeys: [],
|
queryKeys: [],
|
||||||
supportedModes,
|
supportedModes,
|
||||||
mode,
|
mode,
|
||||||
originPanelId,
|
originPanelId: state.urlState && state.urlState.originPanelId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -657,10 +673,7 @@ export const updateChildRefreshState = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMode): [ExploreMode[], ExploreMode] => {
|
const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMode): [ExploreMode[], ExploreMode] => {
|
||||||
// Temporary hack here. We want Loki to work in dashboards for which it needs to have metrics = true which is weird
|
const supportsGraph = dataSource.meta.metrics;
|
||||||
// for Explore.
|
|
||||||
// TODO: need to figure out a better way to handle this situation
|
|
||||||
const supportsGraph = dataSource.meta.name === 'Loki' ? false : dataSource.meta.metrics;
|
|
||||||
const supportsLogs = dataSource.meta.logs;
|
const supportsLogs = dataSource.meta.logs;
|
||||||
|
|
||||||
let mode = currentMode || ExploreMode.Metrics;
|
let mode = currentMode || ExploreMode.Metrics;
|
||||||
@ -678,6 +691,12 @@ const getModesForDatasource = (dataSource: DataSourceApi, currentMode: ExploreMo
|
|||||||
mode = supportedModes[0];
|
mode = supportedModes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: Used to set Loki's default explore mode to Logs mode.
|
||||||
|
// A better solution would be to introduce a "default" or "preferred" mode to the datasource config
|
||||||
|
if (dataSource.meta.name === 'Loki' && !currentMode) {
|
||||||
|
mode = ExploreMode.Logs;
|
||||||
|
}
|
||||||
|
|
||||||
return [supportedModes, mode];
|
return [supportedModes, mode];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,13 +3,11 @@ import configureMockStore from 'redux-mock-store';
|
|||||||
import { PlaylistSrv } from '../playlist_srv';
|
import { PlaylistSrv } from '../playlist_srv';
|
||||||
import { setStore } from 'app/store/store';
|
import { setStore } from 'app/store/store';
|
||||||
|
|
||||||
const mockStore = configureMockStore();
|
const mockStore = configureMockStore<any, any>();
|
||||||
|
|
||||||
setStore(
|
setStore(mockStore({
|
||||||
mockStore({
|
|
||||||
location: {},
|
location: {},
|
||||||
})
|
}) as any);
|
||||||
);
|
|
||||||
|
|
||||||
const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
|
const dashboards = [{ url: 'dash1' }, { url: 'dash2' }];
|
||||||
|
|
||||||
@ -122,13 +120,11 @@ describe('PlaylistSrv', () => {
|
|||||||
|
|
||||||
srv.next();
|
srv.next();
|
||||||
|
|
||||||
setStore(
|
setStore(mockStore({
|
||||||
mockStore({
|
|
||||||
location: {
|
location: {
|
||||||
path: 'dash2',
|
path: 'dash2',
|
||||||
},
|
},
|
||||||
})
|
}) as any);
|
||||||
);
|
|
||||||
|
|
||||||
expect((srv as any).validPlaylistUrl).toBe('dash2');
|
expect((srv as any).validPlaylistUrl).toBe('dash2');
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
|
|
||||||
const dsConfig = config.datasources[name];
|
const dsConfig = config.datasources[name];
|
||||||
if (!dsConfig) {
|
if (!dsConfig) {
|
||||||
return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
|
return this.$q.reject({ message: `Datasource named ${name} was not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const deferred = this.$q.defer();
|
const deferred = this.$q.defer();
|
||||||
|
@ -285,7 +285,7 @@ describe('CloudWatchDatasource', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
redux.setStore({
|
redux.setStore({
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
});
|
} as any);
|
||||||
|
|
||||||
ctx.backendSrv.datasourceRequest = jest.fn(() => {
|
ctx.backendSrv.datasourceRequest = jest.fn(() => {
|
||||||
return Promise.reject(backendErrorResponse);
|
return Promise.reject(backendErrorResponse);
|
||||||
|
@ -2,11 +2,31 @@ import React, { PureComponent } from 'react';
|
|||||||
import { shuffle } from 'lodash';
|
import { shuffle } from 'lodash';
|
||||||
import { ExploreStartPageProps, DataQuery } from '@grafana/data';
|
import { ExploreStartPageProps, DataQuery } from '@grafana/data';
|
||||||
import LokiLanguageProvider from '../language_provider';
|
import LokiLanguageProvider from '../language_provider';
|
||||||
|
import { ExploreMode } from 'app/types';
|
||||||
|
|
||||||
const DEFAULT_EXAMPLES = ['{job="default/prometheus"}'];
|
const DEFAULT_EXAMPLES = ['{job="default/prometheus"}'];
|
||||||
const PREFERRED_LABELS = ['job', 'app', 'k8s_app'];
|
const PREFERRED_LABELS = ['job', 'app', 'k8s_app'];
|
||||||
const EXAMPLES_LIMIT = 5;
|
const EXAMPLES_LIMIT = 5;
|
||||||
|
|
||||||
|
const LOGQL_EXAMPLES = [
|
||||||
|
{
|
||||||
|
title: 'Count over time',
|
||||||
|
expression: 'count_over_time({job="mysql"}[5m])',
|
||||||
|
label: 'This query counts all the log lines within the last five minutes for the MySQL job.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Rate',
|
||||||
|
expression: 'rate(({job="mysql"} |= "error" != "timeout")[10s])',
|
||||||
|
label:
|
||||||
|
'This query gets the per-second rate of all non-timeout errors within the last ten seconds for the MySQL job.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Aggregate, count, and group',
|
||||||
|
expression: 'sum(count_over_time({job="mysql"}[5m])) by (level)',
|
||||||
|
label: 'Get the count of logs during the last five minutes, grouping by level.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps, { userExamples: string[] }> {
|
export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps, { userExamples: string[] }> {
|
||||||
userLabelTimer: NodeJS.Timeout;
|
userLabelTimer: NodeJS.Timeout;
|
||||||
state = {
|
state = {
|
||||||
@ -57,7 +77,7 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderLogsCheatSheet() {
|
||||||
const { userExamples } = this.state;
|
const { userExamples } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -98,4 +118,25 @@ export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps,
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderMetricsCheatSheet() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>LogQL Cheat Sheet</h2>
|
||||||
|
{LOGQL_EXAMPLES.map(item => (
|
||||||
|
<div className="cheat-sheet-item" key={item.expression}>
|
||||||
|
<div className="cheat-sheet-item__title">{item.title}</div>
|
||||||
|
{this.renderExpression(item.expression)}
|
||||||
|
<div className="cheat-sheet-item__label">{item.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { exploreMode } = this.props;
|
||||||
|
|
||||||
|
return exploreMode === ExploreMode.Logs ? this.renderLogsCheatSheet() : this.renderMetricsCheatSheet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@ import {
|
|||||||
import { Plugin, Node } from 'slate';
|
import { Plugin, Node } from 'slate';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { LokiQuery } from '../types';
|
|
||||||
import { DOMUtil } from '@grafana/ui';
|
import { DOMUtil } from '@grafana/ui';
|
||||||
import { ExploreQueryFieldProps, AbsoluteTimeRange } from '@grafana/data';
|
import { ExploreQueryFieldProps, AbsoluteTimeRange } from '@grafana/data';
|
||||||
|
import { LokiQuery, LokiOptions } from '../types';
|
||||||
import { Grammar } from 'prismjs';
|
import { Grammar } from 'prismjs';
|
||||||
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
||||||
import LokiDatasource from '../datasource';
|
import LokiDatasource from '../datasource';
|
||||||
@ -61,7 +61,7 @@ function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadTe
|
|||||||
return suggestion;
|
return suggestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<LokiDatasource, LokiQuery> {
|
export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<LokiDatasource, LokiQuery, LokiOptions> {
|
||||||
history: LokiHistoryItem[];
|
history: LokiHistoryItem[];
|
||||||
syntax: Grammar;
|
syntax: Grammar;
|
||||||
logLabelOptions: CascaderOption[];
|
logLabelOptions: CascaderOption[];
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import LokiDatasource from './datasource';
|
import LokiDatasource from './datasource';
|
||||||
import { LokiQuery } from './types';
|
import { LokiQuery, LokiResultType, LokiResponse, LokiLegacyStreamResponse } from './types';
|
||||||
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
import { getQueryOptions } from 'test/helpers/getQueryOptions';
|
||||||
import { AnnotationQueryRequest, DataSourceApi, DataFrame, dateTime } from '@grafana/data';
|
import { AnnotationQueryRequest, DataSourceApi, DataFrame, dateTime } from '@grafana/data';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { CustomVariable } from 'app/features/templating/custom_variable';
|
import { CustomVariable } from 'app/features/templating/custom_variable';
|
||||||
|
import { ExploreMode } from 'app/types';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
describe('LokiDatasource', () => {
|
describe('LokiDatasource', () => {
|
||||||
const instanceSettings: any = {
|
const instanceSettings: any = {
|
||||||
url: 'myloggingurl',
|
url: 'myloggingurl',
|
||||||
};
|
};
|
||||||
|
|
||||||
const testResp = {
|
const legacyTestResp: { data: LokiLegacyStreamResponse; status: number } = {
|
||||||
data: {
|
data: {
|
||||||
streams: [
|
streams: [
|
||||||
{
|
{
|
||||||
@ -20,6 +23,22 @@ describe('LokiDatasource', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
status: 404, // for simulating legacy endpoint
|
||||||
|
};
|
||||||
|
|
||||||
|
const testResp: { data: LokiResponse } = {
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
resultType: LokiResultType.Stream,
|
||||||
|
result: [
|
||||||
|
{
|
||||||
|
stream: {},
|
||||||
|
values: [['1573646419522934000', 'hello']],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const backendSrvMock = { datasourceRequest: jest.fn() };
|
const backendSrvMock = { datasourceRequest: jest.fn() };
|
||||||
@ -30,8 +49,67 @@ describe('LokiDatasource', () => {
|
|||||||
replace: (a: string) => a,
|
replace: (a: string) => a,
|
||||||
} as unknown) as TemplateSrv;
|
} as unknown) as TemplateSrv;
|
||||||
|
|
||||||
|
describe('when running range query with fallback', () => {
|
||||||
|
let ds: LokiDatasource;
|
||||||
|
beforeEach(() => {
|
||||||
|
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||||
|
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||||
|
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
|
||||||
|
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(legacyTestResp));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should try latest endpoint but fall back to legacy endpoint if it cannot be reached', async () => {
|
||||||
|
const options = getQueryOptions<LokiQuery>({
|
||||||
|
targets: [{ expr: '{job="grafana"}', refId: 'B' }],
|
||||||
|
exploreMode: ExploreMode.Logs,
|
||||||
|
});
|
||||||
|
|
||||||
|
ds.runLegacyQuery = jest.fn();
|
||||||
|
await ds.runRangeQueryWithFallback(options.targets[0], options).toPromise();
|
||||||
|
expect(ds.runLegacyQuery).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('when querying', () => {
|
describe('when querying', () => {
|
||||||
const testLimit = makeLimitTest(instanceSettings, backendSrvMock, backendSrv, templateSrvMock, testResp);
|
const testLimit = makeLimitTest(instanceSettings, backendSrvMock, backendSrv, templateSrvMock, legacyTestResp);
|
||||||
|
let ds: LokiDatasource;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||||
|
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||||
|
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
|
||||||
|
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run instant query and range query when in metrics mode', async () => {
|
||||||
|
const options = getQueryOptions<LokiQuery>({
|
||||||
|
targets: [{ expr: 'rate({job="grafana"}[5m])', refId: 'A' }],
|
||||||
|
exploreMode: ExploreMode.Metrics,
|
||||||
|
});
|
||||||
|
|
||||||
|
ds.runInstantQuery = jest.fn(() => of({ data: [] }));
|
||||||
|
ds.runLegacyQuery = jest.fn();
|
||||||
|
ds.runRangeQueryWithFallback = jest.fn(() => of({ data: [] }));
|
||||||
|
await ds.query(options).toPromise();
|
||||||
|
|
||||||
|
expect(ds.runInstantQuery).toBeCalled();
|
||||||
|
expect(ds.runLegacyQuery).not.toBeCalled();
|
||||||
|
expect(ds.runRangeQueryWithFallback).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should just run range query when in logs mode', async () => {
|
||||||
|
const options = getQueryOptions<LokiQuery>({
|
||||||
|
targets: [{ expr: '{job="grafana"}', refId: 'B' }],
|
||||||
|
exploreMode: ExploreMode.Logs,
|
||||||
|
});
|
||||||
|
|
||||||
|
ds.runInstantQuery = jest.fn(() => of({ data: [] }));
|
||||||
|
ds.runRangeQueryWithFallback = jest.fn(() => of({ data: [] }));
|
||||||
|
await ds.query(options).toPromise();
|
||||||
|
|
||||||
|
expect(ds.runInstantQuery).not.toBeCalled();
|
||||||
|
expect(ds.runRangeQueryWithFallback).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
test('should use default max lines when no limit given', () => {
|
test('should use default max lines when no limit given', () => {
|
||||||
testLimit({
|
testLimit({
|
||||||
@ -61,14 +139,17 @@ describe('LokiDatasource', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return series data', async done => {
|
test('should return series data', async () => {
|
||||||
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||||
const customSettings = { ...instanceSettings, jsonData: customData };
|
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||||
const ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
|
const ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
|
||||||
backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp));
|
backendSrvMock.datasourceRequest = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(Promise.resolve(legacyTestResp))
|
||||||
|
.mockReturnValueOnce(Promise.resolve(omit(legacyTestResp, 'status')));
|
||||||
|
|
||||||
const options = getQueryOptions<LokiQuery>({
|
const options = getQueryOptions<LokiQuery>({
|
||||||
targets: [{ expr: '{} foo', refId: 'B' }],
|
targets: [{ expr: '{job="grafana"} |= "foo"', refId: 'B' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await ds.query(options).toPromise();
|
const res = await ds.query(options).toPromise();
|
||||||
@ -76,14 +157,13 @@ describe('LokiDatasource', () => {
|
|||||||
const dataFrame = res.data[0] as DataFrame;
|
const dataFrame = res.data[0] as DataFrame;
|
||||||
expect(dataFrame.fields[1].values.get(0)).toBe('hello');
|
expect(dataFrame.fields[1].values.get(0)).toBe('hello');
|
||||||
expect(dataFrame.meta.limit).toBe(20);
|
expect(dataFrame.meta.limit).toBe(20);
|
||||||
expect(dataFrame.meta.searchWords).toEqual(['(?i)foo']);
|
expect(dataFrame.meta.searchWords).toEqual(['foo']);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When interpolating variables', () => {
|
describe('When interpolating variables', () => {
|
||||||
let ds: any = {};
|
let ds: LokiDatasource;
|
||||||
let variable: any = {};
|
let variable: CustomVariable;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||||
@ -155,23 +235,25 @@ describe('LokiDatasource', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('and call fails with 401 error', () => {
|
describe('and call fails with 401 error', () => {
|
||||||
beforeEach(async () => {
|
let ds: LokiDatasource;
|
||||||
const backendSrv = ({
|
beforeEach(() => {
|
||||||
async datasourceRequest() {
|
backendSrvMock.datasourceRequest = jest.fn(() =>
|
||||||
return Promise.reject({
|
Promise.reject({
|
||||||
statusText: 'Unauthorized',
|
statusText: 'Unauthorized',
|
||||||
status: 401,
|
status: 401,
|
||||||
data: {
|
data: {
|
||||||
message: 'Unauthorized',
|
message: 'Unauthorized',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
},
|
);
|
||||||
} as unknown) as BackendSrv;
|
|
||||||
ds = new LokiDatasource(instanceSettings, backendSrv, {} as TemplateSrv);
|
const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 };
|
||||||
result = await ds.testDatasource();
|
const customSettings = { ...instanceSettings, jsonData: customData };
|
||||||
|
ds = new LokiDatasource(customSettings, backendSrv, templateSrvMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error status and a detailed error message', () => {
|
it('should return error status and a detailed error message', async () => {
|
||||||
|
const result = await ds.testDatasource();
|
||||||
expect(result.status).toEqual('error');
|
expect(result.status).toEqual('error');
|
||||||
expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized');
|
expect(result.message).toBe('Loki: Unauthorized. 401. Unauthorized');
|
||||||
});
|
});
|
||||||
@ -221,9 +303,17 @@ describe('LokiDatasource', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('annotationQuery', () => {
|
describe('annotationQuery', () => {
|
||||||
it('should transform the loki data to annototion response', async () => {
|
it('should transform the loki data to annotation response', async () => {
|
||||||
const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
|
const ds = new LokiDatasource(instanceSettings, backendSrv, templateSrvMock);
|
||||||
backendSrvMock.datasourceRequest = jest.fn(() =>
|
backendSrvMock.datasourceRequest = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(
|
||||||
|
Promise.resolve({
|
||||||
|
data: [],
|
||||||
|
status: 404,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mockReturnValueOnce(
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
data: {
|
data: {
|
||||||
streams: [
|
streams: [
|
||||||
|
@ -1,24 +1,36 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import { isEmpty, isString, fromPairs, map as lodashMap } from 'lodash';
|
import { isEmpty, map as lodashMap, fromPairs } from 'lodash';
|
||||||
|
import { Observable, from, merge, of, iif, defer } from 'rxjs';
|
||||||
|
import { map, filter, catchError, switchMap, mergeMap } from 'rxjs/operators';
|
||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import {
|
import { dateMath } from '@grafana/data';
|
||||||
dateMath,
|
|
||||||
DataFrame,
|
|
||||||
LogRowModel,
|
|
||||||
DateTime,
|
|
||||||
AnnotationEvent,
|
|
||||||
DataFrameView,
|
|
||||||
LoadingState,
|
|
||||||
ArrayVector,
|
|
||||||
FieldType,
|
|
||||||
FieldConfig,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||||
import LanguageProvider from './language_provider';
|
import { BackendSrv, DatasourceRequestOptions } from 'app/core/services/backend_srv';
|
||||||
import { logStreamToDataFrame } from './result_transformer';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore';
|
||||||
|
import {
|
||||||
|
lokiResultsToTableModel,
|
||||||
|
processRangeQueryResponse,
|
||||||
|
legacyLogStreamToDataFrame,
|
||||||
|
lokiStreamResultToDataFrame,
|
||||||
|
isLokiLogsStream,
|
||||||
|
} from './result_transformer';
|
||||||
import { formatQuery, parseQuery, getHighlighterExpressionsFromQuery } from './query_utils';
|
import { formatQuery, parseQuery, getHighlighterExpressionsFromQuery } from './query_utils';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
|
LogRowModel,
|
||||||
|
DateTime,
|
||||||
|
LoadingState,
|
||||||
|
AnnotationEvent,
|
||||||
|
DataFrameView,
|
||||||
|
TimeRange,
|
||||||
|
FieldConfig,
|
||||||
|
ArrayVector,
|
||||||
|
FieldType,
|
||||||
|
DataFrame,
|
||||||
|
TimeSeries,
|
||||||
PluginMeta,
|
PluginMeta,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
@ -27,29 +39,38 @@ import {
|
|||||||
DataQueryResponse,
|
DataQueryResponse,
|
||||||
AnnotationQueryRequest,
|
AnnotationQueryRequest,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { LokiQuery, LokiOptions, LokiLogsStream, LokiResponse } from './types';
|
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
|
||||||
import { safeStringifyValue, convertToWebSocketUrl } from 'app/core/utils/explore';
|
|
||||||
import { LiveTarget, LiveStreams } from './live_streams';
|
|
||||||
import { Observable, from, merge, of } from 'rxjs';
|
|
||||||
import { map, filter } from 'rxjs/operators';
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
LokiQuery,
|
||||||
|
LokiOptions,
|
||||||
|
LokiLegacyQueryRequest,
|
||||||
|
LokiLegacyStreamResponse,
|
||||||
|
LokiResponse,
|
||||||
|
LokiResultType,
|
||||||
|
LokiRangeQueryRequest,
|
||||||
|
LokiStreamResponse,
|
||||||
|
LokiLegacyStreamResult,
|
||||||
|
} from './types';
|
||||||
|
import { ExploreMode } from 'app/types';
|
||||||
|
import { LegacyTarget, LiveStreams } from './live_streams';
|
||||||
|
import LanguageProvider from './language_provider';
|
||||||
|
|
||||||
|
type RangeQueryOptions = Pick<DataQueryRequest<LokiQuery>, 'range' | 'intervalMs' | 'maxDataPoints' | 'reverse'>;
|
||||||
export const DEFAULT_MAX_LINES = 1000;
|
export const DEFAULT_MAX_LINES = 1000;
|
||||||
|
const LEGACY_QUERY_ENDPOINT = '/api/prom/query';
|
||||||
|
const RANGE_QUERY_ENDPOINT = '/loki/api/v1/query_range';
|
||||||
|
const INSTANT_QUERY_ENDPOINT = '/loki/api/v1/query';
|
||||||
|
|
||||||
const DEFAULT_QUERY_PARAMS = {
|
const DEFAULT_QUERY_PARAMS: Partial<LokiLegacyQueryRequest> = {
|
||||||
direction: 'BACKWARD',
|
direction: 'BACKWARD',
|
||||||
limit: DEFAULT_MAX_LINES,
|
limit: DEFAULT_MAX_LINES,
|
||||||
regexp: '',
|
regexp: '',
|
||||||
query: '',
|
query: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
function serializeParams(data: any) {
|
function serializeParams(data: Record<string, any>) {
|
||||||
return Object.keys(data)
|
return Object.keys(data)
|
||||||
.map(k => {
|
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(data[k])}`)
|
||||||
const v = data[k];
|
|
||||||
return encodeURIComponent(k) + '=' + encodeURIComponent(v);
|
|
||||||
})
|
|
||||||
.join('&');
|
.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +83,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
private streams = new LiveStreams();
|
private streams = new LiveStreams();
|
||||||
languageProvider: LanguageProvider;
|
languageProvider: LanguageProvider;
|
||||||
maxLines: number;
|
maxLines: number;
|
||||||
|
version: string;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
@ -70,155 +92,74 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
private templateSrv: TemplateSrv
|
private templateSrv: TemplateSrv
|
||||||
) {
|
) {
|
||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
|
|
||||||
this.languageProvider = new LanguageProvider(this);
|
this.languageProvider = new LanguageProvider(this);
|
||||||
const settingsData = instanceSettings.jsonData || {};
|
const settingsData = instanceSettings.jsonData || {};
|
||||||
this.maxLines = parseInt(settingsData.maxLines, 10) || DEFAULT_MAX_LINES;
|
this.maxLines = parseInt(settingsData.maxLines, 10) || DEFAULT_MAX_LINES;
|
||||||
}
|
}
|
||||||
|
|
||||||
_request(apiUrl: string, data?: any, options?: any) {
|
getVersion() {
|
||||||
|
if (this.version) {
|
||||||
|
return Promise.resolve(this.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._request(RANGE_QUERY_ENDPOINT)
|
||||||
|
.toPromise()
|
||||||
|
.then(() => {
|
||||||
|
this.version = 'v1';
|
||||||
|
return this.version;
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
this.version = err.status !== 404 ? 'v1' : 'v0';
|
||||||
|
return this.version;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_request(apiUrl: string, data?: any, options?: DatasourceRequestOptions): Observable<Record<string, any>> {
|
||||||
const baseUrl = this.instanceSettings.url;
|
const baseUrl = this.instanceSettings.url;
|
||||||
const params = data ? serializeParams(data) : '';
|
const params = data ? serializeParams(data) : '';
|
||||||
const url = `${baseUrl}${apiUrl}?${params}`;
|
const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`;
|
||||||
const req = {
|
const req = {
|
||||||
...options,
|
...options,
|
||||||
url,
|
url,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.backendSrv.datasourceRequest(req);
|
return from(this.backendSrv.datasourceRequest(req));
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareLiveTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>): LiveTarget {
|
|
||||||
const interpolated = this.templateSrv.replace(target.expr, {}, this.interpolateQueryExpr);
|
|
||||||
const { query, regexp } = parseQuery(interpolated);
|
|
||||||
const refId = target.refId;
|
|
||||||
const baseUrl = this.instanceSettings.url;
|
|
||||||
const params = serializeParams({ query, regexp });
|
|
||||||
const url = convertToWebSocketUrl(`${baseUrl}/api/prom/tail?${params}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
query,
|
|
||||||
regexp,
|
|
||||||
url,
|
|
||||||
refId,
|
|
||||||
size: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareQueryTarget(target: LokiQuery, options: DataQueryRequest<LokiQuery>) {
|
|
||||||
const interpolated = this.templateSrv.replace(target.expr, {}, this.interpolateQueryExpr);
|
|
||||||
const { query, regexp } = parseQuery(interpolated);
|
|
||||||
const start = this.getTime(options.range.from, false);
|
|
||||||
const end = this.getTime(options.range.to, true);
|
|
||||||
const refId = target.refId;
|
|
||||||
return {
|
|
||||||
...DEFAULT_QUERY_PARAMS,
|
|
||||||
query,
|
|
||||||
regexp,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
limit: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
|
||||||
refId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
processError = (err: any, target: any): DataQueryError => {
|
|
||||||
const error: DataQueryError = {
|
|
||||||
message: (err && err.statusText) || 'Unknown error during query transaction. Please check JS console logs.',
|
|
||||||
refId: target.refId,
|
|
||||||
};
|
|
||||||
if (err.data) {
|
|
||||||
if (typeof err.data === 'string') {
|
|
||||||
error.message = err.data;
|
|
||||||
} else if (err.data.error) {
|
|
||||||
error.message = safeStringifyValue(err.data.error);
|
|
||||||
}
|
|
||||||
} else if (err.message) {
|
|
||||||
error.message = err.message;
|
|
||||||
} else if (typeof err === 'string') {
|
|
||||||
error.message = err;
|
|
||||||
}
|
|
||||||
|
|
||||||
error.status = err.status;
|
|
||||||
error.statusText = err.statusText;
|
|
||||||
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
|
|
||||||
processResult = (data: LokiLogsStream | LokiResponse, target: any): DataFrame[] => {
|
|
||||||
const series: DataFrame[] = [];
|
|
||||||
|
|
||||||
if (Object.keys(data).length === 0) {
|
|
||||||
return series;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(data as any).streams) {
|
|
||||||
return [logStreamToDataFrame(data as LokiLogsStream, false, target.refId)];
|
|
||||||
}
|
|
||||||
|
|
||||||
data = data as LokiResponse;
|
|
||||||
for (const stream of data.streams || []) {
|
|
||||||
const dataFrame = logStreamToDataFrame(stream);
|
|
||||||
this.enhanceDataFrame(dataFrame);
|
|
||||||
dataFrame.refId = target.refId;
|
|
||||||
dataFrame.meta = {
|
|
||||||
searchWords: getHighlighterExpressionsFromQuery(formatQuery(target.query, target.regexp)),
|
|
||||||
limit: this.maxLines,
|
|
||||||
};
|
|
||||||
series.push(dataFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
return series;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
runLiveQuery = (options: DataQueryRequest<LokiQuery>, target: LokiQuery): Observable<DataQueryResponse> => {
|
|
||||||
const liveTarget = this.prepareLiveTarget(target, options);
|
|
||||||
const stream = this.streams.getStream(liveTarget);
|
|
||||||
return stream.pipe(
|
|
||||||
map(data => {
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
key: `loki-${liveTarget.refId}`,
|
|
||||||
state: LoadingState.Streaming,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
runQuery = (options: DataQueryRequest<LokiQuery>, target: LokiQuery): Observable<DataQueryResponse> => {
|
|
||||||
const query = this.prepareQueryTarget(target, options);
|
|
||||||
return from(
|
|
||||||
this._request('/api/prom/query', query).catch((err: any) => {
|
|
||||||
if (err.cancelled) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error: DataQueryError = this.processError(err, query);
|
|
||||||
throw error;
|
|
||||||
})
|
|
||||||
).pipe(
|
|
||||||
filter((response: any) => (response.cancelled ? false : true)),
|
|
||||||
map((response: any) => {
|
|
||||||
const data = this.processResult(response.data, query);
|
|
||||||
return { data, key: query.refId };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
query(options: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
|
query(options: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
|
||||||
const subQueries = options.targets
|
const subQueries: Array<Observable<DataQueryResponse>> = [];
|
||||||
|
const filteredTargets = options.targets
|
||||||
.filter(target => target.expr && !target.hide)
|
.filter(target => target.expr && !target.hide)
|
||||||
.map(target => {
|
.map(target => ({
|
||||||
if (target.liveStreaming) {
|
...target,
|
||||||
return this.runLiveQuery(options, target);
|
expr: this.templateSrv.replace(target.expr, {}, this.interpolateQueryExpr),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (options.exploreMode === ExploreMode.Metrics) {
|
||||||
|
filteredTargets.forEach(target =>
|
||||||
|
subQueries.push(
|
||||||
|
this.runInstantQuery(target, options, filteredTargets.length),
|
||||||
|
this.runRangeQueryWithFallback(target, options, filteredTargets.length)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
filteredTargets.forEach(target =>
|
||||||
|
subQueries.push(
|
||||||
|
this.runRangeQueryWithFallback(target, options, filteredTargets.length).pipe(
|
||||||
|
map(dataQueryResponse => {
|
||||||
|
if (options.exploreMode === ExploreMode.Logs && dataQueryResponse.data.find(d => isTimeSeries(d))) {
|
||||||
|
throw new Error(
|
||||||
|
'Logs mode does not support queries that return time series data. Please perform a logs query or switch to Metrics mode.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return dataQueryResponse;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return this.runQuery(options, target);
|
|
||||||
});
|
|
||||||
|
|
||||||
// No valid targets, return the empty result to save a round trip.
|
// No valid targets, return the empty result to save a round trip.
|
||||||
if (isEmpty(subQueries)) {
|
if (isEmpty(subQueries)) {
|
||||||
@ -231,18 +172,216 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
return merge(...subQueries);
|
return merge(...subQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runLegacyQuery = (
|
||||||
|
target: LokiQuery,
|
||||||
|
options: { range?: TimeRange; maxDataPoints?: number; reverse?: boolean }
|
||||||
|
): Observable<DataQueryResponse> => {
|
||||||
|
if (target.liveStreaming) {
|
||||||
|
return this.runLiveQuery(target, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = options.range
|
||||||
|
? { start: this.getTime(options.range.from, false), end: this.getTime(options.range.to, true) }
|
||||||
|
: {};
|
||||||
|
const query: LokiLegacyQueryRequest = {
|
||||||
|
...DEFAULT_QUERY_PARAMS,
|
||||||
|
...parseQuery(target.expr),
|
||||||
|
...range,
|
||||||
|
limit: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
||||||
|
refId: target.refId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._request(LEGACY_QUERY_ENDPOINT, query).pipe(
|
||||||
|
catchError((err: any) => this.throwUnless(err, err.cancelled, target)),
|
||||||
|
filter((response: any) => !response.cancelled),
|
||||||
|
map((response: { data: LokiLegacyStreamResponse }) => ({
|
||||||
|
data: this.lokiLegacyStreamsToDataframes(response.data, query, this.maxLines, options.reverse),
|
||||||
|
key: `${target.refId}_log`,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
lokiLegacyStreamsToDataframes = (
|
||||||
|
data: LokiLegacyStreamResult | LokiLegacyStreamResponse,
|
||||||
|
target: { refId: string; query?: string; regexp?: string },
|
||||||
|
limit: number,
|
||||||
|
reverse = false
|
||||||
|
): DataFrame[] => {
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLokiLogsStream(data)) {
|
||||||
|
return [legacyLogStreamToDataFrame(data, false, target.refId)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const series: DataFrame[] = data.streams.map(stream => {
|
||||||
|
const dataFrame = legacyLogStreamToDataFrame(stream, reverse);
|
||||||
|
this.enhanceDataFrame(dataFrame);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...dataFrame,
|
||||||
|
refId: target.refId,
|
||||||
|
meta: {
|
||||||
|
searchWords: getHighlighterExpressionsFromQuery(formatQuery(target.query, target.regexp)),
|
||||||
|
limit: this.maxLines,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return series;
|
||||||
|
};
|
||||||
|
|
||||||
|
runInstantQuery = (
|
||||||
|
target: LokiQuery,
|
||||||
|
options: DataQueryRequest<LokiQuery>,
|
||||||
|
responseListLength: number
|
||||||
|
): Observable<DataQueryResponse> => {
|
||||||
|
const timeNs = this.getTime(options.range.to, true);
|
||||||
|
const query = {
|
||||||
|
query: parseQuery(target.expr).query,
|
||||||
|
time: `${timeNs + (1e9 - (timeNs % 1e9))}`,
|
||||||
|
limit: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this._request(INSTANT_QUERY_ENDPOINT, query).pipe(
|
||||||
|
catchError((err: any) => this.throwUnless(err, err.cancelled, target)),
|
||||||
|
filter((response: any) => (response.cancelled ? false : true)),
|
||||||
|
map((response: { data: LokiResponse }) => {
|
||||||
|
if (response.data.data.resultType === LokiResultType.Stream) {
|
||||||
|
throw new Error('Metrics mode does not support logs. Use an aggregation or switch to Logs mode.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: [lokiResultsToTableModel(response.data.data.result, responseListLength, target.refId, true)],
|
||||||
|
key: `${target.refId}_instant`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
createRangeQuery(target: LokiQuery, options: RangeQueryOptions): LokiRangeQueryRequest {
|
||||||
|
const { query } = parseQuery(target.expr);
|
||||||
|
let range: { start?: number; end?: number; step?: number } = {};
|
||||||
|
if (options.range && options.intervalMs) {
|
||||||
|
const startNs = this.getTime(options.range.from, false);
|
||||||
|
const endNs = this.getTime(options.range.to, true);
|
||||||
|
const rangeMs = Math.ceil((endNs - startNs) / 1e6);
|
||||||
|
const step = this.adjustInterval(options.intervalMs, rangeMs) / 1000;
|
||||||
|
const alignedTimes = {
|
||||||
|
start: startNs - (startNs % 1e9),
|
||||||
|
end: endNs + (1e9 - (endNs % 1e9)),
|
||||||
|
};
|
||||||
|
|
||||||
|
range = {
|
||||||
|
start: alignedTimes.start,
|
||||||
|
end: alignedTimes.end,
|
||||||
|
step,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULT_QUERY_PARAMS,
|
||||||
|
...range,
|
||||||
|
query,
|
||||||
|
limit: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to send a query to /loki/api/v1/query_range but falls back to the legacy endpoint if necessary.
|
||||||
|
*/
|
||||||
|
runRangeQueryWithFallback = (
|
||||||
|
target: LokiQuery,
|
||||||
|
options: RangeQueryOptions,
|
||||||
|
responseListLength = 1
|
||||||
|
): Observable<DataQueryResponse> => {
|
||||||
|
if (target.liveStreaming) {
|
||||||
|
return this.runLiveQuery(target, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.createRangeQuery(target, options);
|
||||||
|
return this._request(RANGE_QUERY_ENDPOINT, query).pipe(
|
||||||
|
catchError((err: any) => this.throwUnless(err, err.cancelled || err.status === 404, target)),
|
||||||
|
filter((response: any) => (response.cancelled ? false : true)),
|
||||||
|
switchMap((response: { data: LokiResponse; status: number }) =>
|
||||||
|
iif<DataQueryResponse, DataQueryResponse>(
|
||||||
|
() => response.status === 404,
|
||||||
|
defer(() => this.runLegacyQuery(target, options)),
|
||||||
|
defer(() =>
|
||||||
|
processRangeQueryResponse(response.data, target, query, responseListLength, this.maxLines, options.reverse)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
createLegacyLiveTarget(target: LokiQuery, options: { maxDataPoints?: number }): LegacyTarget {
|
||||||
|
const { query, regexp } = parseQuery(target.expr);
|
||||||
|
const baseUrl = this.instanceSettings.url;
|
||||||
|
const params = serializeParams({ query });
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
regexp,
|
||||||
|
url: convertToWebSocketUrl(`${baseUrl}/api/prom/tail?${params}`),
|
||||||
|
refId: target.refId,
|
||||||
|
size: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createLiveTarget(target: LokiQuery, options: { maxDataPoints?: number }): LegacyTarget {
|
||||||
|
const { query, regexp } = parseQuery(target.expr);
|
||||||
|
const baseUrl = this.instanceSettings.url;
|
||||||
|
const params = serializeParams({ query });
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
regexp,
|
||||||
|
url: convertToWebSocketUrl(`${baseUrl}/loki/api/v1/tail?${params}`),
|
||||||
|
refId: target.refId,
|
||||||
|
size: Math.min(options.maxDataPoints || Infinity, this.maxLines),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
runLiveQuery = (target: LokiQuery, options: { maxDataPoints?: number }): Observable<DataQueryResponse> => {
|
||||||
|
const liveTarget = this.createLiveTarget(target, options);
|
||||||
|
|
||||||
|
return from(this.getVersion()).pipe(
|
||||||
|
mergeMap(version =>
|
||||||
|
iif(
|
||||||
|
() => version === 'v1',
|
||||||
|
defer(() => this.streams.getStream(liveTarget)),
|
||||||
|
defer(() => {
|
||||||
|
const legacyTarget = this.createLegacyLiveTarget(target, options);
|
||||||
|
return this.streams.getLegacyStream(legacyTarget);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
map(data => ({
|
||||||
|
data,
|
||||||
|
key: `loki-${liveTarget.refId}`,
|
||||||
|
state: LoadingState.Streaming,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interpolateVariablesInQueries(queries: LokiQuery[]): LokiQuery[] {
|
interpolateVariablesInQueries(queries: LokiQuery[]): LokiQuery[] {
|
||||||
let expandedQueries = queries;
|
let expandedQueries = queries;
|
||||||
if (queries && queries.length > 0) {
|
if (queries && queries.length) {
|
||||||
expandedQueries = queries.map(query => {
|
expandedQueries = queries.map(query => ({
|
||||||
const expandedQuery = {
|
|
||||||
...query,
|
...query,
|
||||||
datasource: this.name,
|
datasource: this.name,
|
||||||
expr: this.templateSrv.replace(query.expr, {}, this.interpolateQueryExpr),
|
expr: this.templateSrv.replace(query.expr, {}, this.interpolateQueryExpr),
|
||||||
};
|
}));
|
||||||
return expandedQuery;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return expandedQueries;
|
return expandedQueries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,13 +389,11 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
return this.languageProvider.importQueries(queries, originMeta.id);
|
return this.languageProvider.importQueries(queries, originMeta.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataRequest(url: string, params?: any) {
|
async metadataRequest(url: string, params?: Record<string, string>) {
|
||||||
// HACK to get label values for {job=|}, will be replaced when implementing LokiQueryField
|
const res = await this._request(url, params, { silent: true }).toPromise();
|
||||||
const apiUrl = url.replace('v1', 'prom');
|
return {
|
||||||
return this._request(apiUrl, params, { silent: true }).then((res: DataQueryResponse) => {
|
data: { data: res.data.values || [] },
|
||||||
const data: any = { data: { data: res.data.values || [] } };
|
};
|
||||||
return data;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolateQueryExpr(value: any, variable: any) {
|
interpolateQueryExpr(value: any, variable: any) {
|
||||||
@ -288,6 +425,7 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expression = formatQuery(selector, parsed.regexp);
|
const expression = formatQuery(selector, parsed.regexp);
|
||||||
return { ...query, expr: expression };
|
return { ...query, expr: expression };
|
||||||
}
|
}
|
||||||
@ -297,70 +435,87 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTime(date: string | DateTime, roundUp: boolean) {
|
getTime(date: string | DateTime, roundUp: boolean) {
|
||||||
if (isString(date)) {
|
if (typeof date === 'string') {
|
||||||
date = dateMath.parse(date, roundUp);
|
date = dateMath.parse(date, roundUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.ceil(date.valueOf() * 1e6);
|
return Math.ceil(date.valueOf() * 1e6);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareLogRowContextQueryTarget = (row: LogRowModel, limit: number, direction: 'BACKWARD' | 'FORWARD') => {
|
getLogRowContext = (row: LogRowModel, options?: LokiContextQueryOptions) => {
|
||||||
const query = Object.keys(row.labels)
|
|
||||||
.map(label => {
|
|
||||||
return `${label}="${row.labels[label]}"`;
|
|
||||||
})
|
|
||||||
.join(',');
|
|
||||||
const contextTimeBuffer = 2 * 60 * 60 * 1000 * 1e6; // 2h buffer
|
|
||||||
const timeEpochNs = row.timeEpochMs * 1e6;
|
|
||||||
|
|
||||||
const commontTargetOptons = {
|
|
||||||
limit,
|
|
||||||
query: `{${query}}`,
|
|
||||||
direction,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (direction === 'BACKWARD') {
|
|
||||||
return {
|
|
||||||
...commontTargetOptons,
|
|
||||||
start: timeEpochNs - contextTimeBuffer,
|
|
||||||
end: row.timestamp, // using RFC3339Nano format to avoid precision loss
|
|
||||||
direction,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...commontTargetOptons,
|
|
||||||
start: row.timestamp, // start param in Loki API is inclusive so we'll have to filter out the row that this request is based from
|
|
||||||
end: timeEpochNs + contextTimeBuffer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getLogRowContext = async (row: LogRowModel, options?: LokiContextQueryOptions) => {
|
|
||||||
const target = this.prepareLogRowContextQueryTarget(
|
const target = this.prepareLogRowContextQueryTarget(
|
||||||
row,
|
row,
|
||||||
(options && options.limit) || 10,
|
(options && options.limit) || 10,
|
||||||
(options && options.direction) || 'BACKWARD'
|
(options && options.direction) || 'BACKWARD'
|
||||||
);
|
);
|
||||||
const series: DataFrame[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const reverse = options && options.direction === 'FORWARD';
|
const reverse = options && options.direction === 'FORWARD';
|
||||||
const result = await this._request('/api/prom/query', target);
|
return this._request(RANGE_QUERY_ENDPOINT, target)
|
||||||
if (result.data) {
|
.pipe(
|
||||||
for (const stream of result.data.streams || []) {
|
catchError((err: any) => {
|
||||||
series.push(logStreamToDataFrame(stream, reverse));
|
if (err.status === 404) {
|
||||||
}
|
return of(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
data: series,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
const error: DataQueryError = {
|
const error: DataQueryError = {
|
||||||
message: 'Error during context query. Please check JS console logs.',
|
message: 'Error during context query. Please check JS console logs.',
|
||||||
status: e.status,
|
status: err.status,
|
||||||
statusText: e.statusText,
|
statusText: err.statusText,
|
||||||
};
|
};
|
||||||
throw error;
|
throw error;
|
||||||
|
}),
|
||||||
|
switchMap((res: { data: LokiStreamResponse; status: number }) =>
|
||||||
|
iif(
|
||||||
|
() => res.status === 404,
|
||||||
|
this._request(LEGACY_QUERY_ENDPOINT, target).pipe(
|
||||||
|
catchError((err: any) => {
|
||||||
|
const error: DataQueryError = {
|
||||||
|
message: 'Error during context query. Please check JS console logs.',
|
||||||
|
status: err.status,
|
||||||
|
statusText: err.statusText,
|
||||||
|
};
|
||||||
|
throw error;
|
||||||
|
}),
|
||||||
|
map((res: { data: LokiLegacyStreamResponse }) => ({
|
||||||
|
data: res.data ? res.data.streams.map(stream => legacyLogStreamToDataFrame(stream, reverse)) : [],
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
of({
|
||||||
|
data: res.data ? res.data.data.result.map(stream => lokiStreamResultToDataFrame(stream, reverse)) : [],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.toPromise();
|
||||||
|
};
|
||||||
|
|
||||||
|
prepareLogRowContextQueryTarget = (row: LogRowModel, limit: number, direction: 'BACKWARD' | 'FORWARD') => {
|
||||||
|
const query = Object.keys(row.labels)
|
||||||
|
.map(label => `${label}="${row.labels[label]}"`)
|
||||||
|
.join(',');
|
||||||
|
|
||||||
|
const contextTimeBuffer = 2 * 60 * 60 * 1000 * 1e6; // 2h buffer
|
||||||
|
const timeEpochNs = row.timeEpochMs * 1e6;
|
||||||
|
const commonTargetOptions = {
|
||||||
|
limit,
|
||||||
|
query: `{${query}}`,
|
||||||
|
expr: `{${query}}`,
|
||||||
|
direction,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (direction === 'BACKWARD') {
|
||||||
|
return {
|
||||||
|
...commonTargetOptions,
|
||||||
|
start: timeEpochNs - contextTimeBuffer,
|
||||||
|
end: timeEpochNs, // using RFC3339Nano format to avoid precision loss
|
||||||
|
direction,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...commonTargetOptions,
|
||||||
|
start: timeEpochNs, // start param in Loki API is inclusive so we'll have to filter out the row that this request is based from
|
||||||
|
end: timeEpochNs + contextTimeBuffer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -368,18 +523,32 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
// Consider only last 10 minutes otherwise request takes too long
|
// Consider only last 10 minutes otherwise request takes too long
|
||||||
const startMs = Date.now() - 10 * 60 * 1000;
|
const startMs = Date.now() - 10 * 60 * 1000;
|
||||||
const start = `${startMs}000000`; // API expects nanoseconds
|
const start = `${startMs}000000`; // API expects nanoseconds
|
||||||
return this._request('/api/prom/label', { start })
|
return this._request('/loki/api/v1/label', { start })
|
||||||
.then((res: DataQueryResponse) => {
|
.pipe(
|
||||||
if (res && res.data && res.data.values && res.data.values.length > 0) {
|
catchError((err: any) => {
|
||||||
return { status: 'success', message: 'Data source connected and labels found.' };
|
if (err.status === 404) {
|
||||||
|
return of(err);
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
|
throw err;
|
||||||
|
}),
|
||||||
|
switchMap((response: { data: { values: string[] }; status: number }) =>
|
||||||
|
iif<DataQueryResponse, DataQueryResponse>(
|
||||||
|
() => response.status === 404,
|
||||||
|
defer(() => this._request('/api/prom/label', { start })),
|
||||||
|
defer(() => of(response))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
map(res =>
|
||||||
|
res && res.data && res.data.values && res.data.values.length
|
||||||
|
? { status: 'success', message: 'Data source connected and labels found.' }
|
||||||
|
: {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message:
|
message:
|
||||||
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
|
'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.',
|
||||||
};
|
}
|
||||||
})
|
),
|
||||||
.catch((err: any) => {
|
catchError((err: any) => {
|
||||||
let message = 'Loki: ';
|
let message = 'Loki: ';
|
||||||
if (err.statusText) {
|
if (err.statusText) {
|
||||||
message += err.statusText;
|
message += err.statusText;
|
||||||
@ -396,8 +565,10 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
} else if (err.data) {
|
} else if (err.data) {
|
||||||
message += `. ${err.data}`;
|
message += `. ${err.data}`;
|
||||||
}
|
}
|
||||||
return { status: 'error', message: message };
|
return of({ status: 'error', message: message });
|
||||||
});
|
})
|
||||||
|
)
|
||||||
|
.toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
async annotationQuery(options: AnnotationQueryRequest<LokiQuery>): Promise<AnnotationEvent[]> {
|
async annotationQuery(options: AnnotationQueryRequest<LokiQuery>): Promise<AnnotationEvent[]> {
|
||||||
@ -405,8 +576,8 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = queryRequestFromAnnotationOptions(options);
|
const query = { refId: `annotation-${options.annotation.name}`, expr: options.annotation.expr };
|
||||||
const { data } = await this.runQuery(request, request.targets[0]).toPromise();
|
const { data } = await this.runRangeQueryWithFallback(query, options).toPromise();
|
||||||
const annotations: AnnotationEvent[] = [];
|
const annotations: AnnotationEvent[] = [];
|
||||||
|
|
||||||
for (const frame of data) {
|
for (const frame of data) {
|
||||||
@ -474,29 +645,48 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
|
|||||||
dataFrame.fields = [...dataFrame.fields, ...Object.values(fields)];
|
dataFrame.fields = [...dataFrame.fields, ...Object.values(fields)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throwUnless = (err: any, condition: boolean, target: LokiQuery) => {
|
||||||
|
if (condition) {
|
||||||
|
return of(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryRequestFromAnnotationOptions(options: AnnotationQueryRequest<LokiQuery>): DataQueryRequest<LokiQuery> {
|
const error: DataQueryError = this.processError(err, target);
|
||||||
const refId = `annotation-${options.annotation.name}`;
|
throw error;
|
||||||
const target: LokiQuery = { refId, expr: options.annotation.expr };
|
|
||||||
|
|
||||||
return {
|
|
||||||
requestId: refId,
|
|
||||||
range: options.range,
|
|
||||||
targets: [target],
|
|
||||||
dashboardId: options.dashboard.id,
|
|
||||||
scopedVars: null,
|
|
||||||
startTime: Date.now(),
|
|
||||||
|
|
||||||
// This should mean the default defined on datasource is used.
|
|
||||||
maxDataPoints: 0,
|
|
||||||
|
|
||||||
// Dummy values, are required in type but not used here.
|
|
||||||
timezone: 'utc',
|
|
||||||
panelId: 0,
|
|
||||||
interval: '',
|
|
||||||
intervalMs: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
processError = (err: any, target: LokiQuery): DataQueryError => {
|
||||||
|
const error: DataQueryError = {
|
||||||
|
message: (err && err.statusText) || 'Unknown error during query transaction. Please check JS console logs.',
|
||||||
|
refId: target.refId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (err.data) {
|
||||||
|
if (typeof err.data === 'string') {
|
||||||
|
error.message = err.data;
|
||||||
|
} else if (err.data.error) {
|
||||||
|
error.message = safeStringifyValue(err.data.error);
|
||||||
|
}
|
||||||
|
} else if (err.message) {
|
||||||
|
error.message = err.message;
|
||||||
|
} else if (typeof err === 'string') {
|
||||||
|
error.message = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.status = err.status;
|
||||||
|
error.statusText = err.statusText;
|
||||||
|
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
adjustInterval(interval: number, range: number) {
|
||||||
|
// Loki will drop queries that might return more than 11000 data points.
|
||||||
|
// Calibrate interval if it is too small.
|
||||||
|
if (interval !== 0 && range / interval > 11000) {
|
||||||
|
interval = Math.ceil(range / 11000);
|
||||||
|
}
|
||||||
|
return Math.max(interval, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lokiRegularEscape(value: any) {
|
export function lokiRegularEscape(value: any) {
|
||||||
@ -514,3 +704,7 @@ export function lokiSpecialRegexEscape(value: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default LokiDatasource;
|
export default LokiDatasource;
|
||||||
|
|
||||||
|
function isTimeSeries(data: any): data is TimeSeries {
|
||||||
|
return data.hasOwnProperty('datapoints');
|
||||||
|
}
|
||||||
|
@ -10,6 +10,18 @@ import { beforeEach } from 'test/lib/common';
|
|||||||
import { makeMockLokiDatasource } from './mocks';
|
import { makeMockLokiDatasource } from './mocks';
|
||||||
import LokiDatasource from './datasource';
|
import LokiDatasource from './datasource';
|
||||||
|
|
||||||
|
jest.mock('app/store/store', () => ({
|
||||||
|
store: {
|
||||||
|
getState: jest.fn().mockReturnValue({
|
||||||
|
explore: {
|
||||||
|
left: {
|
||||||
|
mode: 'Logs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Language completion provider', () => {
|
describe('Language completion provider', () => {
|
||||||
const datasource = makeMockLokiDatasource({});
|
const datasource = makeMockLokiDatasource({});
|
||||||
|
|
||||||
|
@ -3,15 +3,18 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
||||||
import syntax from './syntax';
|
import { store } from 'app/store/store';
|
||||||
|
import syntax, { FUNCTIONS } from './syntax';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { LokiQuery } from './types';
|
import { LokiQuery } from './types';
|
||||||
import { dateTime, AbsoluteTimeRange, LanguageProvider, HistoryItem } from '@grafana/data';
|
import { dateTime, AbsoluteTimeRange, LanguageProvider, HistoryItem } from '@grafana/data';
|
||||||
import { PromQuery } from '../prometheus/types';
|
import { PromQuery } from '../prometheus/types';
|
||||||
|
import { RATE_RANGES } from '../prometheus/promql';
|
||||||
|
|
||||||
import LokiDatasource from './datasource';
|
import LokiDatasource from './datasource';
|
||||||
import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
|
import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
|
||||||
|
import { ExploreMode } from 'app/types/explore';
|
||||||
|
|
||||||
const DEFAULT_KEYS = ['job', 'namespace'];
|
const DEFAULT_KEYS = ['job', 'namespace'];
|
||||||
const EMPTY_SELECTOR = '{}';
|
const EMPTY_SELECTOR = '{}';
|
||||||
@ -32,14 +35,15 @@ type TypeaheadContext = {
|
|||||||
|
|
||||||
export function addHistoryMetadata(item: CompletionItem, history: LokiHistoryItem[]): CompletionItem {
|
export function addHistoryMetadata(item: CompletionItem, history: LokiHistoryItem[]): CompletionItem {
|
||||||
const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
||||||
const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label);
|
const historyForItem = history.filter(h => h.ts > cutoffTs && h.query.expr === item.label);
|
||||||
const count = historyForItem.length;
|
let hint = `Queried ${historyForItem.length} times in the last 24h.`;
|
||||||
const recent = historyForItem[0];
|
const recent = historyForItem[0];
|
||||||
let hint = `Queried ${count} times in the last 24h.`;
|
|
||||||
if (recent) {
|
if (recent) {
|
||||||
const lastQueried = dateTime(recent.ts).fromNow();
|
const lastQueried = dateTime(recent.ts).fromNow();
|
||||||
hint = `${hint} Last queried ${lastQueried}.`;
|
hint = `${hint} Last queried ${lastQueried}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
documentation: hint,
|
documentation: hint,
|
||||||
@ -72,7 +76,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return syntax;
|
return syntax;
|
||||||
}
|
}
|
||||||
|
|
||||||
request = (url: string, params?: any) => {
|
request = (url: string, params?: any): Promise<{ data: { data: string[] } }> => {
|
||||||
return this.datasource.metadataRequest(url, params);
|
return this.datasource.metadataRequest(url, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -87,6 +91,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.startTask;
|
return this.startTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,15 +113,53 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
* @param context.history Optional used only in getEmptyCompletionItems
|
* @param context.history Optional used only in getEmptyCompletionItems
|
||||||
*/
|
*/
|
||||||
async provideCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): Promise<TypeaheadOutput> {
|
async provideCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): Promise<TypeaheadOutput> {
|
||||||
const { wrapperClasses, value } = input;
|
const exploreMode = store.getState().explore.left.mode;
|
||||||
|
|
||||||
|
if (exploreMode === ExploreMode.Logs) {
|
||||||
|
return this.provideLogCompletionItems(input, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.provideMetricsCompletionItems(input, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async provideMetricsCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): Promise<TypeaheadOutput> {
|
||||||
|
const { wrapperClasses, value, prefix, text } = input;
|
||||||
|
|
||||||
// Local text properties
|
// Local text properties
|
||||||
const empty = value.document.text.length === 0;
|
const empty = value.document.text.length === 0;
|
||||||
|
const selectedLines = value.document.getTextsAtRange(value.selection);
|
||||||
|
const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null;
|
||||||
|
|
||||||
|
const nextCharacter = currentLine ? currentLine[value.selection.anchor.offset] : null;
|
||||||
|
|
||||||
|
// Syntax spans have 3 classes by default. More indicate a recognized token
|
||||||
|
const tokenRecognized = wrapperClasses.length > 3;
|
||||||
|
|
||||||
|
// Non-empty prefix, but not inside known token
|
||||||
|
const prefixUnrecognized = prefix && !tokenRecognized;
|
||||||
|
|
||||||
|
// Prevent suggestions in `function(|suffix)`
|
||||||
|
const noSuffix = !nextCharacter || nextCharacter === ')';
|
||||||
|
|
||||||
|
// Empty prefix is safe if it does not immediately follow a complete expression and has no text after it
|
||||||
|
const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix;
|
||||||
|
|
||||||
|
// About to type next operand if preceded by binary operator
|
||||||
|
const operatorsPattern = /[+\-*/^%]/;
|
||||||
|
const isNextOperand = text.match(operatorsPattern);
|
||||||
|
|
||||||
// Determine candidates by CSS context
|
// Determine candidates by CSS context
|
||||||
if (_.includes(wrapperClasses, 'context-labels')) {
|
if (wrapperClasses.includes('context-range')) {
|
||||||
|
// Suggestions for metric[|]
|
||||||
|
return this.getRangeCompletionItems();
|
||||||
|
} else if (wrapperClasses.includes('context-labels')) {
|
||||||
// Suggestions for {|} and {foo=|}
|
// Suggestions for {|} and {foo=|}
|
||||||
return await this.getLabelCompletionItems(input, context);
|
return await this.getLabelCompletionItems(input, context);
|
||||||
} else if (empty) {
|
} else if (empty) {
|
||||||
return this.getEmptyCompletionItems(context || {});
|
return this.getEmptyCompletionItems(context || {}, ExploreMode.Metrics);
|
||||||
|
} else if ((prefixUnrecognized && noSuffix) || safeEmptyPrefix || isNextOperand) {
|
||||||
|
// Show term suggestions in a couple of scenarios
|
||||||
|
return this.getTermCompletionItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -124,13 +167,30 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmptyCompletionItems(context: any): TypeaheadOutput {
|
async provideLogCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): Promise<TypeaheadOutput> {
|
||||||
|
const { wrapperClasses, value } = input;
|
||||||
|
// Local text properties
|
||||||
|
const empty = value.document.text.length === 0;
|
||||||
|
// Determine candidates by CSS context
|
||||||
|
if (wrapperClasses.includes('context-labels')) {
|
||||||
|
// Suggestions for {|} and {foo=|}
|
||||||
|
return await this.getLabelCompletionItems(input, context);
|
||||||
|
} else if (empty) {
|
||||||
|
return this.getEmptyCompletionItems(context || {}, ExploreMode.Logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getEmptyCompletionItems(context: TypeaheadContext, mode?: ExploreMode): TypeaheadOutput {
|
||||||
const { history } = context;
|
const { history } = context;
|
||||||
const suggestions = [];
|
const suggestions = [];
|
||||||
|
|
||||||
if (history && history.length > 0) {
|
if (history && history.length) {
|
||||||
const historyItems = _.chain(history)
|
const historyItems = _.chain(history)
|
||||||
.map((h: any) => h.query.expr)
|
.map(h => h.query.expr)
|
||||||
.filter()
|
.filter()
|
||||||
.uniq()
|
.uniq()
|
||||||
.take(HISTORY_ITEM_COUNT)
|
.take(HISTORY_ITEM_COUNT)
|
||||||
@ -146,9 +206,38 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode === ExploreMode.Metrics) {
|
||||||
|
const termCompletionItems = this.getTermCompletionItems();
|
||||||
|
suggestions.push(...termCompletionItems.suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
return { suggestions };
|
return { suggestions };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTermCompletionItems = (): TypeaheadOutput => {
|
||||||
|
const suggestions = [];
|
||||||
|
|
||||||
|
suggestions.push({
|
||||||
|
prefixMatch: true,
|
||||||
|
label: 'Functions',
|
||||||
|
items: FUNCTIONS.map(suggestion => ({ ...suggestion, kind: 'function' })),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { suggestions };
|
||||||
|
};
|
||||||
|
|
||||||
|
getRangeCompletionItems(): TypeaheadOutput {
|
||||||
|
return {
|
||||||
|
context: 'context-range',
|
||||||
|
suggestions: [
|
||||||
|
{
|
||||||
|
label: 'Range vector',
|
||||||
|
items: [...RATE_RANGES],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async getLabelCompletionItems(
|
async getLabelCompletionItems(
|
||||||
{ text, wrapperClasses, labelKey, value }: TypeaheadInput,
|
{ text, wrapperClasses, labelKey, value }: TypeaheadInput,
|
||||||
{ absoluteRange }: any
|
{ absoluteRange }: any
|
||||||
@ -186,7 +275,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
const labelKeys = this.labelKeys[selector] || DEFAULT_KEYS;
|
const labelKeys = this.labelKeys[selector] || DEFAULT_KEYS;
|
||||||
if (labelKeys) {
|
if (labelKeys) {
|
||||||
const possibleKeys = _.difference(labelKeys, existingKeys);
|
const possibleKeys = _.difference(labelKeys, existingKeys);
|
||||||
if (possibleKeys.length > 0) {
|
if (possibleKeys.length) {
|
||||||
context = 'context-labels';
|
context = 'context-labels';
|
||||||
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
||||||
}
|
}
|
||||||
@ -223,7 +312,10 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
|
|
||||||
// Consider only first selector in query
|
// Consider only first selector in query
|
||||||
const selectorMatch = query.match(selectorRegexp);
|
const selectorMatch = query.match(selectorRegexp);
|
||||||
if (selectorMatch) {
|
if (!selectorMatch) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const selector = selectorMatch[0];
|
const selector = selectorMatch[0];
|
||||||
const labels: { [key: string]: { value: any; operator: any } } = {};
|
const labels: { [key: string]: { value: any; operator: any } } = {};
|
||||||
selector.replace(labelRegexp, (_, key, operator, value) => {
|
selector.replace(labelRegexp, (_, key, operator, value) => {
|
||||||
@ -235,7 +327,7 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
await this.start(); // fetches all existing label keys
|
await this.start(); // fetches all existing label keys
|
||||||
const existingKeys = this.labelKeys[EMPTY_SELECTOR];
|
const existingKeys = this.labelKeys[EMPTY_SELECTOR];
|
||||||
let labelsToKeep: { [key: string]: { value: any; operator: any } } = {};
|
let labelsToKeep: { [key: string]: { value: any; operator: any } } = {};
|
||||||
if (existingKeys && existingKeys.length > 0) {
|
if (existingKeys && existingKeys.length) {
|
||||||
// Check for common labels
|
// Check for common labels
|
||||||
for (const key in labels) {
|
for (const key in labels) {
|
||||||
if (existingKeys && existingKeys.includes(key)) {
|
if (existingKeys && existingKeys.includes(key)) {
|
||||||
@ -256,17 +348,14 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
return ['{', cleanSelector, '}'].join('');
|
return ['{', cleanSelector, '}'].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchLogLabels(absoluteRange: AbsoluteTimeRange): Promise<any> {
|
async fetchLogLabels(absoluteRange: AbsoluteTimeRange): Promise<any> {
|
||||||
const url = '/api/prom/label';
|
const url = '/api/prom/label';
|
||||||
try {
|
try {
|
||||||
this.logLabelFetchTs = Date.now();
|
this.logLabelFetchTs = Date.now();
|
||||||
|
|
||||||
const res = await this.request(url, rangeToParams(absoluteRange));
|
const res = await this.request(url, rangeToParams(absoluteRange));
|
||||||
const body = await (res.data || res.json());
|
const labelKeys = res.data.data.slice().sort();
|
||||||
const labelKeys = body.data.slice().sort();
|
|
||||||
this.labelKeys = {
|
this.labelKeys = {
|
||||||
...this.labelKeys,
|
...this.labelKeys,
|
||||||
[EMPTY_SELECTOR]: labelKeys,
|
[EMPTY_SELECTOR]: labelKeys,
|
||||||
@ -291,15 +380,14 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
const url = `/api/prom/label/${key}/values`;
|
const url = `/api/prom/label/${key}/values`;
|
||||||
try {
|
try {
|
||||||
const res = await this.request(url, rangeToParams(absoluteRange));
|
const res = await this.request(url, rangeToParams(absoluteRange));
|
||||||
const body = await (res.data || res.json());
|
const values = res.data.data.slice().sort();
|
||||||
const values = body.data.slice().sort();
|
|
||||||
|
|
||||||
// Add to label options
|
// Add to label options
|
||||||
this.logLabelOptions = this.logLabelOptions.map(keyOption => {
|
this.logLabelOptions = this.logLabelOptions.map(keyOption => {
|
||||||
if (keyOption.value === key) {
|
if (keyOption.value === key) {
|
||||||
return {
|
return {
|
||||||
...keyOption,
|
...keyOption,
|
||||||
children: values.map((value: string) => ({ label: value, value })),
|
children: values.map(value => ({ label: value, value })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return keyOption;
|
return keyOption;
|
||||||
|
@ -36,7 +36,7 @@ describe('Live Stream Tests', () => {
|
|||||||
fakeSocket = new Subject<any>();
|
fakeSocket = new Subject<any>();
|
||||||
const labels: Labels = { job: 'varlogs' };
|
const labels: Labels = { job: 'varlogs' };
|
||||||
const target = makeTarget('fake', labels);
|
const target = makeTarget('fake', labels);
|
||||||
const stream = new LiveStreams().getStream(target);
|
const stream = new LiveStreams().getLegacyStream(target);
|
||||||
expect.assertions(4);
|
expect.assertions(4);
|
||||||
|
|
||||||
const tests = [
|
const tests = [
|
||||||
@ -74,21 +74,21 @@ describe('Live Stream Tests', () => {
|
|||||||
it('returns the same subscription if the url matches existing one', () => {
|
it('returns the same subscription if the url matches existing one', () => {
|
||||||
fakeSocket = new Subject<any>();
|
fakeSocket = new Subject<any>();
|
||||||
const liveStreams = new LiveStreams();
|
const liveStreams = new LiveStreams();
|
||||||
const stream1 = liveStreams.getStream(makeTarget('url_to_match'));
|
const stream1 = liveStreams.getLegacyStream(makeTarget('url_to_match'));
|
||||||
const stream2 = liveStreams.getStream(makeTarget('url_to_match'));
|
const stream2 = liveStreams.getLegacyStream(makeTarget('url_to_match'));
|
||||||
expect(stream1).toBe(stream2);
|
expect(stream1).toBe(stream2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns new subscription when the previous unsubscribed', () => {
|
it('returns new subscription when the previous unsubscribed', () => {
|
||||||
fakeSocket = new Subject<any>();
|
fakeSocket = new Subject<any>();
|
||||||
const liveStreams = new LiveStreams();
|
const liveStreams = new LiveStreams();
|
||||||
const stream1 = liveStreams.getStream(makeTarget('url_to_match'));
|
const stream1 = liveStreams.getLegacyStream(makeTarget('url_to_match'));
|
||||||
const subscription = stream1.subscribe({
|
const subscription = stream1.subscribe({
|
||||||
next: noop,
|
next: noop,
|
||||||
});
|
});
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
|
|
||||||
const stream2 = liveStreams.getStream(makeTarget('url_to_match'));
|
const stream2 = liveStreams.getLegacyStream(makeTarget('url_to_match'));
|
||||||
expect(stream1).not.toBe(stream2);
|
expect(stream1).not.toBe(stream2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ describe('Live Stream Tests', () => {
|
|||||||
spy.and.returnValue(fakeSocket);
|
spy.and.returnValue(fakeSocket);
|
||||||
|
|
||||||
const liveStreams = new LiveStreams();
|
const liveStreams = new LiveStreams();
|
||||||
const stream1 = liveStreams.getStream(makeTarget('url_to_match'));
|
const stream1 = liveStreams.getLegacyStream(makeTarget('url_to_match'));
|
||||||
const subscription = stream1.subscribe({
|
const subscription = stream1.subscribe({
|
||||||
next: noop,
|
next: noop,
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { DataFrame, FieldType, parseLabels, KeyValue, CircularDataFrame } from '@grafana/data';
|
import { DataFrame, FieldType, parseLabels, KeyValue, CircularDataFrame } from '@grafana/data';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { webSocket } from 'rxjs/webSocket';
|
import { webSocket } from 'rxjs/webSocket';
|
||||||
import { LokiResponse } from './types';
|
import { LokiLegacyStreamResponse, LokiTailResponse } from './types';
|
||||||
import { finalize, map } from 'rxjs/operators';
|
import { finalize, map } from 'rxjs/operators';
|
||||||
import { appendResponseToBufferedData } from './result_transformer';
|
import { appendLegacyResponseToBufferedData, appendResponseToBufferedData } from './result_transformer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps directly to a query in the UI (refId is key)
|
* Maps directly to a query in the UI (refId is key)
|
||||||
*/
|
*/
|
||||||
export interface LiveTarget {
|
export interface LegacyTarget {
|
||||||
query: string;
|
query: string;
|
||||||
regexp: string;
|
regexp: string;
|
||||||
url: string;
|
url: string;
|
||||||
@ -16,6 +16,13 @@ export interface LiveTarget {
|
|||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LiveTarget {
|
||||||
|
query: string;
|
||||||
|
delay_for?: string;
|
||||||
|
limit?: string;
|
||||||
|
start?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of websocket streams that can be returned as observable. In case there already is a stream for particular
|
* Cache of websocket streams that can be returned as observable. In case there already is a stream for particular
|
||||||
* target it is returned and on subscription returns the latest dataFrame.
|
* target it is returned and on subscription returns the latest dataFrame.
|
||||||
@ -23,9 +30,13 @@ export interface LiveTarget {
|
|||||||
export class LiveStreams {
|
export class LiveStreams {
|
||||||
private streams: KeyValue<Observable<DataFrame[]>> = {};
|
private streams: KeyValue<Observable<DataFrame[]>> = {};
|
||||||
|
|
||||||
getStream(target: LiveTarget): Observable<DataFrame[]> {
|
getLegacyStream(target: LegacyTarget): Observable<DataFrame[]> {
|
||||||
let stream = this.streams[target.url];
|
let stream = this.streams[target.url];
|
||||||
if (!stream) {
|
|
||||||
|
if (stream) {
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
const data = new CircularDataFrame({ capacity: target.size });
|
const data = new CircularDataFrame({ capacity: target.size });
|
||||||
data.addField({ name: 'ts', type: FieldType.time, config: { title: 'Time' } });
|
data.addField({ name: 'ts', type: FieldType.time, config: { title: 'Time' } });
|
||||||
data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query);
|
data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query);
|
||||||
@ -36,13 +47,42 @@ export class LiveStreams {
|
|||||||
finalize(() => {
|
finalize(() => {
|
||||||
delete this.streams[target.url];
|
delete this.streams[target.url];
|
||||||
}),
|
}),
|
||||||
map((response: LokiResponse) => {
|
|
||||||
|
map((response: LokiLegacyStreamResponse) => {
|
||||||
|
appendLegacyResponseToBufferedData(response, data);
|
||||||
|
return [data];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.streams[target.url] = stream;
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStream(target: LegacyTarget): Observable<DataFrame[]> {
|
||||||
|
let stream = this.streams[target.url];
|
||||||
|
|
||||||
|
if (stream) {
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new CircularDataFrame({ capacity: target.size });
|
||||||
|
data.addField({ name: 'ts', type: FieldType.time, config: { title: 'Time' } });
|
||||||
|
data.addField({ name: 'line', type: FieldType.string }).labels = parseLabels(target.query);
|
||||||
|
data.addField({ name: 'labels', type: FieldType.other }); // The labels for each line
|
||||||
|
data.addField({ name: 'id', type: FieldType.string });
|
||||||
|
|
||||||
|
stream = webSocket(target.url).pipe(
|
||||||
|
finalize(() => {
|
||||||
|
delete this.streams[target.url];
|
||||||
|
}),
|
||||||
|
|
||||||
|
map((response: LokiTailResponse) => {
|
||||||
appendResponseToBufferedData(response, data);
|
appendResponseToBufferedData(response, data);
|
||||||
return [data];
|
return [data];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.streams[target.url] = stream;
|
this.streams[target.url] = stream;
|
||||||
}
|
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
"id": "loki",
|
"id": "loki",
|
||||||
"category": "logging",
|
"category": "logging",
|
||||||
|
|
||||||
|
"logs": true,
|
||||||
"metrics": true,
|
"metrics": true,
|
||||||
"alerting": false,
|
"alerting": false,
|
||||||
"annotations": true,
|
"annotations": true,
|
||||||
"logs": true,
|
|
||||||
"streaming": true,
|
"streaming": true,
|
||||||
|
|
||||||
"queryOptions": {
|
"queryOptions": {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { logStreamToDataFrame, appendResponseToBufferedData } from './result_transformer';
|
import { legacyLogStreamToDataFrame, appendLegacyResponseToBufferedData } from './result_transformer';
|
||||||
import { FieldType, MutableDataFrame } from '@grafana/data';
|
import { FieldType, MutableDataFrame } from '@grafana/data';
|
||||||
import { LokiLogsStream } from './types';
|
import { LokiLegacyStreamResult } from './types';
|
||||||
|
|
||||||
const streams: LokiLogsStream[] = [
|
const streams: LokiLegacyStreamResult[] = [
|
||||||
{
|
{
|
||||||
labels: '{foo="bar"}',
|
labels: '{foo="bar"}',
|
||||||
entries: [
|
entries: [
|
||||||
@ -25,7 +25,7 @@ const streams: LokiLogsStream[] = [
|
|||||||
|
|
||||||
describe('logStreamToDataFrame', () => {
|
describe('logStreamToDataFrame', () => {
|
||||||
it('converts streams to series', () => {
|
it('converts streams to series', () => {
|
||||||
const data = streams.map(stream => logStreamToDataFrame(stream));
|
const data = streams.map(stream => legacyLogStreamToDataFrame(stream));
|
||||||
|
|
||||||
expect(data.length).toBe(2);
|
expect(data.length).toBe(2);
|
||||||
expect(data[0].fields[1].labels['foo']).toEqual('bar');
|
expect(data[0].fields[1].labels['foo']).toEqual('bar');
|
||||||
@ -46,7 +46,7 @@ describe('appendResponseToBufferedData', () => {
|
|||||||
data.addField({ name: 'labels', type: FieldType.other });
|
data.addField({ name: 'labels', type: FieldType.other });
|
||||||
data.addField({ name: 'id', type: FieldType.string });
|
data.addField({ name: 'id', type: FieldType.string });
|
||||||
|
|
||||||
appendResponseToBufferedData({ streams }, data);
|
appendLegacyResponseToBufferedData({ streams }, data);
|
||||||
expect(data.get(0)).toEqual({
|
expect(data.get(0)).toEqual({
|
||||||
ts: '1970-01-01T00:00:00Z',
|
ts: '1970-01-01T00:00:00Z',
|
||||||
line: "foo: [32m'bar'[39m",
|
line: "foo: [32m'bar'[39m",
|
||||||
|
@ -1,18 +1,43 @@
|
|||||||
import { LokiLogsStream, LokiResponse } from './types';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
parseLabels,
|
parseLabels,
|
||||||
FieldType,
|
FieldType,
|
||||||
|
TimeSeries,
|
||||||
Labels,
|
Labels,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
ArrayVector,
|
ArrayVector,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
findUniqueLabels,
|
findUniqueLabels,
|
||||||
|
dateTime,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
|
import TableModel from 'app/core/table_model';
|
||||||
|
import {
|
||||||
|
LokiLegacyStreamResult,
|
||||||
|
LokiRangeQueryRequest,
|
||||||
|
LokiResponse,
|
||||||
|
LokiMatrixResult,
|
||||||
|
LokiVectorResult,
|
||||||
|
TransformerOptions,
|
||||||
|
LokiLegacyStreamResponse,
|
||||||
|
LokiResultType,
|
||||||
|
LokiStreamResult,
|
||||||
|
LokiTailResponse,
|
||||||
|
LokiQuery,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
import { formatQuery, getHighlighterExpressionsFromQuery } from './query_utils';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms LokiLogStream structure into a dataFrame. Used when doing standard queries.
|
* Transforms LokiLogStream structure into a dataFrame. Used when doing standard queries.
|
||||||
*/
|
*/
|
||||||
export function logStreamToDataFrame(stream: LokiLogsStream, reverse?: boolean, refId?: string): DataFrame {
|
export function legacyLogStreamToDataFrame(
|
||||||
|
stream: LokiLegacyStreamResult,
|
||||||
|
reverse?: boolean,
|
||||||
|
refId?: string
|
||||||
|
): DataFrame {
|
||||||
let labels: Labels = stream.parsedLabels;
|
let labels: Labels = stream.parsedLabels;
|
||||||
if (!labels && stream.labels) {
|
if (!labels && stream.labels) {
|
||||||
labels = parseLabels(stream.labels);
|
labels = parseLabels(stream.labels);
|
||||||
@ -44,6 +69,39 @@ export function logStreamToDataFrame(stream: LokiLogsStream, reverse?: boolean,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lokiStreamResultToDataFrame(stream: LokiStreamResult, reverse?: boolean, refId?: string): DataFrame {
|
||||||
|
const labels: Labels = stream.stream;
|
||||||
|
|
||||||
|
const times = new ArrayVector<string>([]);
|
||||||
|
const lines = new ArrayVector<string>([]);
|
||||||
|
const uids = new ArrayVector<string>([]);
|
||||||
|
|
||||||
|
for (const [ts, line] of stream.values) {
|
||||||
|
times.add(dateTime(Number.parseFloat(ts) / 1e6).format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
lines.add(line);
|
||||||
|
uids.add(
|
||||||
|
`${ts}_${Object.entries(labels)
|
||||||
|
.map(([key, val]) => `${key}=${val}`)
|
||||||
|
.join('')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
times.buffer = times.buffer.reverse();
|
||||||
|
lines.buffer = lines.buffer.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
refId,
|
||||||
|
fields: [
|
||||||
|
{ name: 'ts', type: FieldType.time, config: { title: 'Time' }, values: times }, // Time
|
||||||
|
{ name: 'line', type: FieldType.string, config: {}, values: lines, labels }, // Line
|
||||||
|
{ name: 'id', type: FieldType.string, config: {}, values: uids },
|
||||||
|
],
|
||||||
|
length: times.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform LokiResponse data and appends it to MutableDataFrame. Used for streaming where the dataFrame can be
|
* Transform LokiResponse data and appends it to MutableDataFrame. Used for streaming where the dataFrame can be
|
||||||
* a CircularDataFrame creating a fixed size rolling buffer.
|
* a CircularDataFrame creating a fixed size rolling buffer.
|
||||||
@ -51,12 +109,14 @@ export function logStreamToDataFrame(stream: LokiLogsStream, reverse?: boolean,
|
|||||||
* @param response
|
* @param response
|
||||||
* @param data Needs to have ts, line, labels, id as fields
|
* @param data Needs to have ts, line, labels, id as fields
|
||||||
*/
|
*/
|
||||||
export function appendResponseToBufferedData(response: LokiResponse, data: MutableDataFrame) {
|
export function appendLegacyResponseToBufferedData(response: LokiLegacyStreamResponse, data: MutableDataFrame) {
|
||||||
// Should we do anything with: response.dropped_entries?
|
// Should we do anything with: response.dropped_entries?
|
||||||
|
|
||||||
const streams: LokiLogsStream[] = response.streams;
|
const streams: LokiLegacyStreamResult[] = response.streams;
|
||||||
if (streams && streams.length) {
|
if (!streams || !streams.length) {
|
||||||
const { values } = data;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let baseLabels: Labels = {};
|
let baseLabels: Labels = {};
|
||||||
for (const f of data.fields) {
|
for (const f of data.fields) {
|
||||||
if (f.type === FieldType.string) {
|
if (f.type === FieldType.string) {
|
||||||
@ -75,11 +135,278 @@ export function appendResponseToBufferedData(response: LokiResponse, data: Mutab
|
|||||||
// Add each line
|
// Add each line
|
||||||
for (const entry of stream.entries) {
|
for (const entry of stream.entries) {
|
||||||
const ts = entry.ts || entry.timestamp;
|
const ts = entry.ts || entry.timestamp;
|
||||||
values.ts.add(ts);
|
data.values.ts.add(ts);
|
||||||
values.line.add(entry.line);
|
data.values.line.add(entry.line);
|
||||||
values.labels.add(unique);
|
data.values.labels.add(unique);
|
||||||
values.id.add(`${ts}_${stream.labels}`);
|
data.values.id.add(`${ts}_${stream.labels}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function appendResponseToBufferedData(response: LokiTailResponse, data: MutableDataFrame) {
|
||||||
|
// Should we do anything with: response.dropped_entries?
|
||||||
|
|
||||||
|
const streams: LokiStreamResult[] = response.streams;
|
||||||
|
if (!streams || !streams.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseLabels: Labels = {};
|
||||||
|
for (const f of data.fields) {
|
||||||
|
if (f.type === FieldType.string) {
|
||||||
|
if (f.labels) {
|
||||||
|
baseLabels = f.labels;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const stream of streams) {
|
||||||
|
// Find unique labels
|
||||||
|
const unique = findUniqueLabels(stream.stream, baseLabels);
|
||||||
|
|
||||||
|
// Add each line
|
||||||
|
for (const [ts, line] of stream.values) {
|
||||||
|
data.values.ts.add(parseInt(ts, 10) / 1e6);
|
||||||
|
data.values.line.add(line);
|
||||||
|
data.values.labels.add(unique);
|
||||||
|
data.values.id.add(
|
||||||
|
`${ts}_${Object.entries(unique)
|
||||||
|
.map(([key, val]) => `${key}=${val}`)
|
||||||
|
.join('')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function lokiMatrixToTimeSeries(matrixResult: LokiMatrixResult, options: TransformerOptions): TimeSeries {
|
||||||
|
return {
|
||||||
|
target: createMetricLabel(matrixResult.metric, options),
|
||||||
|
datapoints: lokiPointsToTimeseriesPoints(matrixResult.values, options),
|
||||||
|
tags: matrixResult.metric,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function lokiPointsToTimeseriesPoints(
|
||||||
|
data: Array<[number, string]>,
|
||||||
|
options: TransformerOptions
|
||||||
|
): Array<[number, number]> {
|
||||||
|
const stepMs = options.step * 1000;
|
||||||
|
const datapoints: Array<[number, number]> = [];
|
||||||
|
|
||||||
|
let baseTimestampMs = options.start / 1e6;
|
||||||
|
for (const [time, value] of data) {
|
||||||
|
let datapointValue = parseFloat(value);
|
||||||
|
if (isNaN(datapointValue)) {
|
||||||
|
datapointValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = time * 1000;
|
||||||
|
for (let t = baseTimestampMs; t < timestamp; t += stepMs) {
|
||||||
|
datapoints.push([0, t]);
|
||||||
|
}
|
||||||
|
|
||||||
|
baseTimestampMs = timestamp + stepMs;
|
||||||
|
datapoints.push([datapointValue, timestamp]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endTimestamp = options.end / 1e6;
|
||||||
|
for (let t = baseTimestampMs; t <= endTimestamp; t += stepMs) {
|
||||||
|
datapoints.push([0, t]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return datapoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lokiResultsToTableModel(
|
||||||
|
lokiResults: Array<LokiMatrixResult | LokiVectorResult>,
|
||||||
|
resultCount: number,
|
||||||
|
refId: string,
|
||||||
|
valueWithRefId?: boolean
|
||||||
|
): TableModel {
|
||||||
|
if (!lokiResults || lokiResults.length === 0) {
|
||||||
|
return new TableModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all labels across all metrics
|
||||||
|
const metricLabels: Set<string> = new Set<string>(
|
||||||
|
lokiResults.reduce((acc, cur) => acc.concat(Object.keys(cur.metric)), [])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort metric labels, create columns for them and record their index
|
||||||
|
const sortedLabels = [...metricLabels.values()].sort();
|
||||||
|
const table = new TableModel();
|
||||||
|
table.columns = [
|
||||||
|
{ text: 'Time', type: FieldType.time },
|
||||||
|
...sortedLabels.map(label => ({ text: label, filterable: true })),
|
||||||
|
{ text: resultCount > 1 || valueWithRefId ? `Value #${refId}` : 'Value', type: FieldType.time },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Populate rows, set value to empty string when label not present.
|
||||||
|
lokiResults.forEach(series => {
|
||||||
|
const newSeries: LokiMatrixResult = {
|
||||||
|
metric: series.metric,
|
||||||
|
values: (series as LokiVectorResult).value
|
||||||
|
? [(series as LokiVectorResult).value]
|
||||||
|
: (series as LokiMatrixResult).values,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!newSeries.values) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newSeries.metric) {
|
||||||
|
table.rows.concat(newSeries.values.map(([a, b]) => [a * 1000, parseFloat(b)]));
|
||||||
|
} else {
|
||||||
|
table.rows.push(
|
||||||
|
...newSeries.values.map(([a, b]) => [
|
||||||
|
a * 1000,
|
||||||
|
...sortedLabels.map(label => newSeries.metric[label] || ''),
|
||||||
|
parseFloat(b),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMetricLabel(labelData: { [key: string]: string }, options?: TransformerOptions) {
|
||||||
|
let label =
|
||||||
|
options === undefined || _.isEmpty(options.legendFormat)
|
||||||
|
? getOriginalMetricName(labelData)
|
||||||
|
: renderTemplate(templateSrv.replace(options.legendFormat), labelData);
|
||||||
|
|
||||||
|
if (!label || label === '{}') {
|
||||||
|
label = options.query;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) {
|
||||||
|
const aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
|
||||||
|
return aliasPattern.replace(aliasRegex, (_, g1) => (aliasData[g1] ? aliasData[g1] : g1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOriginalMetricName(labelData: { [key: string]: string }) {
|
||||||
|
const metricName = labelData.__name__ || '';
|
||||||
|
delete labelData.__name__;
|
||||||
|
const labelPart = Object.entries(labelData)
|
||||||
|
.map(label => `${label[0]}="${label[1]}"`)
|
||||||
|
.join(',');
|
||||||
|
return `${metricName}{${labelPart}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lokiStreamsToDataframes(
|
||||||
|
data: LokiStreamResult[],
|
||||||
|
target: { refId: string; expr?: string; regexp?: string },
|
||||||
|
limit: number,
|
||||||
|
reverse = false
|
||||||
|
): DataFrame[] {
|
||||||
|
const series: DataFrame[] = data.map(stream => ({
|
||||||
|
...lokiStreamResultToDataFrame(stream, reverse),
|
||||||
|
refId: target.refId,
|
||||||
|
meta: {
|
||||||
|
searchWords: getHighlighterExpressionsFromQuery(formatQuery(target.expr, target.regexp)),
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lokiLegacyStreamsToDataframes(
|
||||||
|
data: LokiLegacyStreamResult | LokiLegacyStreamResponse,
|
||||||
|
target: { refId: string; query?: string; regexp?: string },
|
||||||
|
limit: number,
|
||||||
|
reverse = false
|
||||||
|
): DataFrame[] {
|
||||||
|
if (Object.keys(data).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLokiLogsStream(data)) {
|
||||||
|
return [legacyLogStreamToDataFrame(data, reverse, target.refId)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const series: DataFrame[] = data.streams.map(stream => ({
|
||||||
|
...legacyLogStreamToDataFrame(stream, reverse),
|
||||||
|
refId: target.refId,
|
||||||
|
meta: {
|
||||||
|
searchWords: getHighlighterExpressionsFromQuery(formatQuery(target.query, target.regexp)),
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rangeQueryResponseToTimeSeries(
|
||||||
|
response: LokiResponse,
|
||||||
|
query: LokiRangeQueryRequest,
|
||||||
|
target: LokiQuery,
|
||||||
|
responseListLength: number
|
||||||
|
): TimeSeries[] {
|
||||||
|
const transformerOptions: TransformerOptions = {
|
||||||
|
format: target.format,
|
||||||
|
legendFormat: target.legendFormat,
|
||||||
|
start: query.start,
|
||||||
|
end: query.end,
|
||||||
|
step: query.step,
|
||||||
|
query: query.query,
|
||||||
|
responseListLength,
|
||||||
|
refId: target.refId,
|
||||||
|
valueWithRefId: target.valueWithRefId,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (response.data.resultType) {
|
||||||
|
case LokiResultType.Vector:
|
||||||
|
return response.data.result.map(vecResult =>
|
||||||
|
lokiMatrixToTimeSeries({ metric: vecResult.metric, values: [vecResult.value] }, transformerOptions)
|
||||||
|
);
|
||||||
|
case LokiResultType.Matrix:
|
||||||
|
return response.data.result.map(matrixResult => lokiMatrixToTimeSeries(matrixResult, transformerOptions));
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processRangeQueryResponse(
|
||||||
|
response: LokiResponse,
|
||||||
|
target: LokiQuery,
|
||||||
|
query: LokiRangeQueryRequest,
|
||||||
|
responseListLength: number,
|
||||||
|
limit: number,
|
||||||
|
reverse = false
|
||||||
|
) {
|
||||||
|
switch (response.data.resultType) {
|
||||||
|
case LokiResultType.Stream:
|
||||||
|
return of({
|
||||||
|
data: lokiStreamsToDataframes(response.data.result, target, limit, reverse),
|
||||||
|
key: `${target.refId}_log`,
|
||||||
|
});
|
||||||
|
|
||||||
|
case LokiResultType.Vector:
|
||||||
|
case LokiResultType.Matrix:
|
||||||
|
return of({
|
||||||
|
data: rangeQueryResponseToTimeSeries(
|
||||||
|
response,
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
...target,
|
||||||
|
format: 'time_series',
|
||||||
|
},
|
||||||
|
responseListLength
|
||||||
|
),
|
||||||
|
key: target.refId,
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown result type "${(response.data as any).resultType}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLokiLogsStream(
|
||||||
|
data: LokiLegacyStreamResult | LokiLegacyStreamResponse
|
||||||
|
): data is LokiLegacyStreamResult {
|
||||||
|
return !data.hasOwnProperty('streams');
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,90 @@
|
|||||||
import { Grammar } from 'prismjs';
|
import { Grammar } from 'prismjs';
|
||||||
|
import { CompletionItem } from '@grafana/ui';
|
||||||
|
|
||||||
/* tslint:disable max-line-length */
|
const AGGREGATION_OPERATORS: CompletionItem[] = [
|
||||||
|
{
|
||||||
|
label: 'sum',
|
||||||
|
insertText: 'sum',
|
||||||
|
documentation: 'Calculate sum over dimensions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'min',
|
||||||
|
insertText: 'min',
|
||||||
|
documentation: 'Select minimum over dimensions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'max',
|
||||||
|
insertText: 'max',
|
||||||
|
documentation: 'Select maximum over dimensions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'avg',
|
||||||
|
insertText: 'avg',
|
||||||
|
documentation: 'Calculate the average over dimensions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'stddev',
|
||||||
|
insertText: 'stddev',
|
||||||
|
documentation: 'Calculate population standard deviation over dimensions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'stdvar',
|
||||||
|
insertText: 'stdvar',
|
||||||
|
documentation: 'Calculate population standard variance over dimensions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'count',
|
||||||
|
insertText: 'count',
|
||||||
|
documentation: 'Count number of elements in the vector',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'bottomk',
|
||||||
|
insertText: 'bottomk',
|
||||||
|
documentation: 'Smallest k elements by sample value',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'topk',
|
||||||
|
insertText: 'topk',
|
||||||
|
documentation: 'Largest k elements by sample value',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RANGE_VEC_FUNCTIONS = [
|
||||||
|
{
|
||||||
|
insertText: 'count_over_time',
|
||||||
|
label: 'count_over_time',
|
||||||
|
detail: 'count_over_time(range-vector)',
|
||||||
|
documentation: 'The count of all values in the specified interval.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
insertText: 'rate',
|
||||||
|
label: 'rate',
|
||||||
|
detail: 'rate(v range-vector)',
|
||||||
|
documentation:
|
||||||
|
"Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FUNCTIONS = [...AGGREGATION_OPERATORS, ...RANGE_VEC_FUNCTIONS];
|
||||||
|
|
||||||
const tokenizer: Grammar = {
|
const tokenizer: Grammar = {
|
||||||
comment: {
|
comment: {
|
||||||
pattern: /(^|[^\n])#.*/,
|
pattern: /(^|[^\n])#.*/,
|
||||||
lookbehind: true,
|
lookbehind: true,
|
||||||
},
|
},
|
||||||
|
'context-aggregation': {
|
||||||
|
pattern: /((without|by)\s*)\([^)]*\)/, // by ()
|
||||||
|
lookbehind: true,
|
||||||
|
inside: {
|
||||||
|
'label-key': {
|
||||||
|
pattern: /[^(),\s][^,)]*[^),\s]*/,
|
||||||
|
alias: 'attr-name',
|
||||||
|
},
|
||||||
|
punctuation: /[()]/,
|
||||||
|
},
|
||||||
|
},
|
||||||
'context-labels': {
|
'context-labels': {
|
||||||
pattern: /(^|\s)\{[^}]*(?=})/,
|
pattern: /\{[^}]*(?=})/,
|
||||||
lookbehind: true,
|
lookbehind: true,
|
||||||
inside: {
|
inside: {
|
||||||
'label-key': {
|
'label-key': {
|
||||||
@ -23,9 +99,31 @@ const tokenizer: Grammar = {
|
|||||||
punctuation: /[{]/,
|
punctuation: /[{]/,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// number: /\b-?\d+((\.\d*)?([eE][+-]?\d+)?)?\b/,
|
function: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.label).join('|')})(?=\\s*\\()`, 'i'),
|
||||||
|
'context-range': [
|
||||||
|
{
|
||||||
|
pattern: /\[[^\]]*(?=\])/, // [1m]
|
||||||
|
inside: {
|
||||||
|
'range-duration': {
|
||||||
|
pattern: /\b\d+[smhdwy]\b/i,
|
||||||
|
alias: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /(offset\s+)\w+/, // offset 1m
|
||||||
|
lookbehind: true,
|
||||||
|
inside: {
|
||||||
|
'range-duration': {
|
||||||
|
pattern: /\b\d+[smhdwy]\b/i,
|
||||||
|
alias: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
number: /\b-?\d+((\.\d*)?([eE][+-]?\d+)?)?\b/,
|
||||||
operator: new RegExp(`/&&?|\\|?\\||!=?|<(?:=>?|<|>)?|>[>=]?`, 'i'),
|
operator: new RegExp(`/&&?|\\|?\\||!=?|<(?:=>?|<|>)?|>[>=]?`, 'i'),
|
||||||
punctuation: /[{}`,.]/,
|
punctuation: /[{}()`,.]/,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default tokenizer;
|
export default tokenizer;
|
||||||
|
@ -1,10 +1,47 @@
|
|||||||
import { Labels, DataQuery, DataSourceJsonData } from '@grafana/data';
|
import { Labels, DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface LokiLegacyQueryRequest {
|
||||||
|
query: string;
|
||||||
|
limit?: number;
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
direction?: 'BACKWARD' | 'FORWARD';
|
||||||
|
regexp?: string;
|
||||||
|
|
||||||
|
refId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiInstantQueryRequest {
|
||||||
|
query: string;
|
||||||
|
limit?: number;
|
||||||
|
time?: string;
|
||||||
|
direction?: 'BACKWARD' | 'FORWARD';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiRangeQueryRequest {
|
||||||
|
query: string;
|
||||||
|
limit?: number;
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
step?: number;
|
||||||
|
direction?: 'BACKWARD' | 'FORWARD';
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LokiResultType {
|
||||||
|
Stream = 'streams',
|
||||||
|
Vector = 'vector',
|
||||||
|
Matrix = 'matrix',
|
||||||
|
}
|
||||||
|
|
||||||
export interface LokiQuery extends DataQuery {
|
export interface LokiQuery extends DataQuery {
|
||||||
expr: string;
|
expr: string;
|
||||||
liveStreaming?: boolean;
|
liveStreaming?: boolean;
|
||||||
query?: string;
|
query?: string;
|
||||||
regexp?: string;
|
regexp?: string;
|
||||||
|
format?: string;
|
||||||
|
reverse?: boolean;
|
||||||
|
legendFormat?: string;
|
||||||
|
valueWithRefId?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LokiOptions extends DataSourceJsonData {
|
export interface LokiOptions extends DataSourceJsonData {
|
||||||
@ -12,11 +49,46 @@ export interface LokiOptions extends DataSourceJsonData {
|
|||||||
derivedFields?: DerivedFieldConfig[];
|
derivedFields?: DerivedFieldConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LokiResponse {
|
export interface LokiVectorResult {
|
||||||
streams: LokiLogsStream[];
|
metric: { [label: string]: string };
|
||||||
|
value: [number, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LokiLogsStream {
|
export interface LokiVectorResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
resultType: LokiResultType.Vector;
|
||||||
|
result: LokiVectorResult[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiMatrixResult {
|
||||||
|
metric: { [label: string]: string };
|
||||||
|
values: Array<[number, string]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiMatrixResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
resultType: LokiResultType.Matrix;
|
||||||
|
result: LokiMatrixResult[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiStreamResult {
|
||||||
|
stream: Record<string, string>;
|
||||||
|
values: Array<[string, string]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiStreamResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
resultType: LokiResultType.Stream;
|
||||||
|
result: LokiStreamResult[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiLegacyStreamResult {
|
||||||
labels: string;
|
labels: string;
|
||||||
entries: LokiLogsStreamEntry[];
|
entries: LokiLogsStreamEntry[];
|
||||||
search?: string;
|
search?: string;
|
||||||
@ -24,6 +96,21 @@ export interface LokiLogsStream {
|
|||||||
uniqueLabels?: Labels;
|
uniqueLabels?: Labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LokiLegacyStreamResponse {
|
||||||
|
streams: LokiLegacyStreamResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LokiTailResponse {
|
||||||
|
streams: LokiStreamResult[];
|
||||||
|
dropped_entries?: Array<{
|
||||||
|
labels: Record<string, string>;
|
||||||
|
timestamp: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LokiResult = LokiVectorResult | LokiMatrixResult | LokiStreamResult | LokiLegacyStreamResult;
|
||||||
|
export type LokiResponse = LokiVectorResponse | LokiMatrixResponse | LokiStreamResponse;
|
||||||
|
|
||||||
export interface LokiLogsStreamEntry {
|
export interface LokiLogsStreamEntry {
|
||||||
line: string;
|
line: string;
|
||||||
ts: string;
|
ts: string;
|
||||||
@ -41,3 +128,15 @@ export type DerivedFieldConfig = {
|
|||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface TransformerOptions {
|
||||||
|
format: string;
|
||||||
|
legendFormat: string;
|
||||||
|
step: number;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
query: string;
|
||||||
|
responseListLength: number;
|
||||||
|
refId: string;
|
||||||
|
valueWithRefId?: boolean;
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { setStore } from './store';
|
|||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
import { toggleLogActionsMiddleware } from 'app/core/middlewares/application';
|
import { toggleLogActionsMiddleware } from 'app/core/middlewares/application';
|
||||||
import { addReducer, createRootReducer } from '../core/reducers/root';
|
import { addReducer, createRootReducer } from '../core/reducers/root';
|
||||||
|
import { ActionOf } from 'app/core/redux';
|
||||||
|
|
||||||
export function addRootReducer(reducers: any) {
|
export function addRootReducer(reducers: any) {
|
||||||
// this is ok now because we add reducers before configureStore is called
|
// this is ok now because we add reducers before configureStore is called
|
||||||
@ -27,7 +28,11 @@ export function configureStore() {
|
|||||||
? applyMiddleware(toggleLogActionsMiddleware, thunk, logger)
|
? applyMiddleware(toggleLogActionsMiddleware, thunk, logger)
|
||||||
: applyMiddleware(thunk);
|
: applyMiddleware(thunk);
|
||||||
|
|
||||||
const store: any = createStore(createRootReducer(), {}, composeEnhancers(storeEnhancers));
|
const store = createStore<StoreState, ActionOf<any>, any, any>(
|
||||||
|
createRootReducer(),
|
||||||
|
{},
|
||||||
|
composeEnhancers(storeEnhancers)
|
||||||
|
);
|
||||||
setStore(store);
|
setStore(store);
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
export let store: any;
|
import { StoreState } from 'app/types';
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
export function setStore(newStore: any) {
|
export let store: Store<StoreState>;
|
||||||
|
|
||||||
|
export function setStore(newStore: Store<StoreState>) {
|
||||||
store = newStore;
|
store = newStore;
|
||||||
}
|
}
|
||||||
|
@ -228,6 +228,7 @@ export interface QueryOptions {
|
|||||||
liveStreaming?: boolean;
|
liveStreaming?: boolean;
|
||||||
showingGraph?: boolean;
|
showingGraph?: boolean;
|
||||||
showingTable?: boolean;
|
showingTable?: boolean;
|
||||||
|
mode?: ExploreMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryTransaction {
|
export interface QueryTransaction {
|
||||||
|
Loading…
Reference in New Issue
Block a user