mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 00:25:46 -06:00
257 lines
7.7 KiB
TypeScript
257 lines
7.7 KiB
TypeScript
import {
|
|
DataFrame,
|
|
DataFrameType,
|
|
DataQueryResponse,
|
|
DataQueryResponseData,
|
|
Field,
|
|
FieldType,
|
|
isValidGoDuration,
|
|
Labels,
|
|
QueryResultMetaStat,
|
|
shallowCompare,
|
|
} from '@grafana/data';
|
|
|
|
import { isBytesString } from './languageUtils';
|
|
import { isLogLineJSON, isLogLineLogfmt, isLogLinePacked } from './lineParser';
|
|
|
|
export function dataFrameHasLokiError(frame: DataFrame): boolean {
|
|
const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values ?? [];
|
|
return labelSets.some((labels) => labels.__error__ !== undefined);
|
|
}
|
|
|
|
export function dataFrameHasLevelLabel(frame: DataFrame): boolean {
|
|
const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values ?? [];
|
|
return labelSets.some((labels) => labels.level !== undefined);
|
|
}
|
|
|
|
export function extractLogParserFromDataFrame(frame: DataFrame): {
|
|
hasLogfmt: boolean;
|
|
hasJSON: boolean;
|
|
hasPack: boolean;
|
|
} {
|
|
const lineField = frame.fields.find((field) => field.type === FieldType.string);
|
|
if (lineField == null) {
|
|
return { hasJSON: false, hasLogfmt: false, hasPack: false };
|
|
}
|
|
|
|
const logLines: string[] = lineField.values;
|
|
|
|
let hasJSON = false;
|
|
let hasLogfmt = false;
|
|
let hasPack = false;
|
|
|
|
logLines.forEach((line) => {
|
|
if (isLogLineJSON(line)) {
|
|
hasJSON = true;
|
|
|
|
hasPack = isLogLinePacked(line);
|
|
}
|
|
if (isLogLineLogfmt(line)) {
|
|
hasLogfmt = true;
|
|
}
|
|
});
|
|
|
|
return { hasLogfmt, hasJSON, hasPack };
|
|
}
|
|
|
|
export function extractLabelKeysFromDataFrame(frame: DataFrame): string[] {
|
|
const labelsArray: Array<{ [key: string]: string }> | undefined =
|
|
frame?.fields?.find((field) => field.name === 'labels')?.values ?? [];
|
|
|
|
if (!labelsArray?.length) {
|
|
return [];
|
|
}
|
|
|
|
return Object.keys(labelsArray[0]);
|
|
}
|
|
|
|
export function extractUnwrapLabelKeysFromDataFrame(frame: DataFrame): string[] {
|
|
const labelsArray: Array<{ [key: string]: string }> | undefined =
|
|
frame?.fields?.find((field) => field.name === 'labels')?.values ?? [];
|
|
|
|
if (!labelsArray?.length) {
|
|
return [];
|
|
}
|
|
|
|
// We do this only for first label object, because we want to consider only labels that are present in all log lines
|
|
// possibleUnwrapLabels are labels with 1. number value OR 2. value that is valid go duration OR 3. bytes string value
|
|
const possibleUnwrapLabels = Object.keys(labelsArray[0]).filter((key) => {
|
|
const value = labelsArray[0][key];
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
return !isNaN(Number(value)) || isValidGoDuration(value) || isBytesString(value);
|
|
});
|
|
|
|
// Add only labels that are present in every line to unwrapLabels
|
|
return possibleUnwrapLabels.filter((label) => labelsArray.every((obj) => obj[label]));
|
|
}
|
|
|
|
export function extractHasErrorLabelFromDataFrame(frame: DataFrame): boolean {
|
|
const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other);
|
|
if (labelField == null) {
|
|
return false;
|
|
}
|
|
|
|
const labels: Array<{ [key: string]: string }> = labelField.values;
|
|
return labels.some((label) => label['__error__']);
|
|
}
|
|
|
|
export function extractLevelLikeLabelFromDataFrame(frame: DataFrame): string | null {
|
|
const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other);
|
|
if (labelField == null) {
|
|
return null;
|
|
}
|
|
|
|
// Depending on number of labels, this can be pretty heavy operation.
|
|
// Let's just look at first 2 lines If needed, we can introduce more later.
|
|
const labelsArray: Array<{ [key: string]: string }> = labelField.values.slice(0, 2);
|
|
let levelLikeLabel: string | null = null;
|
|
|
|
// Find first level-like label
|
|
for (let labels of labelsArray) {
|
|
const label = Object.keys(labels).find((label) => label === 'lvl' || label.includes('level'));
|
|
if (label) {
|
|
levelLikeLabel = label;
|
|
break;
|
|
}
|
|
}
|
|
return levelLikeLabel;
|
|
}
|
|
|
|
function shouldCombine(frame1: DataFrame, frame2: DataFrame): boolean {
|
|
if (frame1.refId !== frame2.refId) {
|
|
return false;
|
|
}
|
|
|
|
const frameType1 = frame1.meta?.type;
|
|
const frameType2 = frame2.meta?.type;
|
|
|
|
if (frameType1 !== frameType2) {
|
|
// we do not join things that have a different type
|
|
return false;
|
|
}
|
|
|
|
// metric range query data
|
|
if (frameType1 === DataFrameType.TimeSeriesMulti) {
|
|
const field1 = frame1.fields.find((f) => f.type === FieldType.number);
|
|
const field2 = frame2.fields.find((f) => f.type === FieldType.number);
|
|
if (field1 === undefined || field2 === undefined) {
|
|
// should never happen
|
|
return false;
|
|
}
|
|
|
|
return shallowCompare(field1.labels ?? {}, field2.labels ?? {});
|
|
}
|
|
|
|
// logs query data
|
|
// logs use a special attribute in the dataframe's "custom" section
|
|
// because we do not have a good "frametype" value for them yet.
|
|
const customType1 = frame1.meta?.custom?.frameType;
|
|
const customType2 = frame2.meta?.custom?.frameType;
|
|
|
|
if (customType1 === 'LabeledTimeValues' && customType2 === 'LabeledTimeValues') {
|
|
return true;
|
|
}
|
|
|
|
// should never reach here
|
|
return false;
|
|
}
|
|
|
|
export function combineResponses(currentResult: DataQueryResponse | null, newResult: DataQueryResponse) {
|
|
if (!currentResult) {
|
|
return cloneQueryResponse(newResult);
|
|
}
|
|
|
|
newResult.data.forEach((newFrame) => {
|
|
const currentFrame = currentResult.data.find((frame) => shouldCombine(frame, newFrame));
|
|
if (!currentFrame) {
|
|
currentResult.data.push(cloneDataFrame(newFrame));
|
|
return;
|
|
}
|
|
combineFrames(currentFrame, newFrame);
|
|
});
|
|
|
|
const mergedErrors = [...(currentResult.errors ?? []), ...(newResult.errors ?? [])];
|
|
|
|
// we make sure to have `.errors` as undefined, instead of empty-array
|
|
// when no errors.
|
|
|
|
if (mergedErrors.length > 0) {
|
|
currentResult.errors = mergedErrors;
|
|
}
|
|
|
|
// the `.error` attribute is obsolete now,
|
|
// but we have to maintain it, otherwise
|
|
// some grafana parts do not behave well.
|
|
// we just choose the old error, if it exists,
|
|
// otherwise the new error, if it exists.
|
|
const mergedError = currentResult.error ?? newResult.error;
|
|
if (mergedError != null) {
|
|
currentResult.error = mergedError;
|
|
}
|
|
|
|
const mergedTraceIds = [...(currentResult.traceIds ?? []), ...(newResult.traceIds ?? [])];
|
|
if (mergedTraceIds.length > 0) {
|
|
currentResult.traceIds = mergedTraceIds;
|
|
}
|
|
|
|
return currentResult;
|
|
}
|
|
|
|
function combineFrames(dest: DataFrame, source: DataFrame) {
|
|
const totalFields = dest.fields.length;
|
|
for (let i = 0; i < totalFields; i++) {
|
|
dest.fields[i].values = [].concat.apply(source.fields[i].values, dest.fields[i].values);
|
|
}
|
|
dest.length += source.length;
|
|
dest.meta = {
|
|
...dest.meta,
|
|
stats: getCombinedMetadataStats(dest.meta?.stats ?? [], source.meta?.stats ?? []),
|
|
};
|
|
}
|
|
|
|
const TOTAL_BYTES_STAT = 'Summary: total bytes processed';
|
|
|
|
function getCombinedMetadataStats(
|
|
destStats: QueryResultMetaStat[],
|
|
sourceStats: QueryResultMetaStat[]
|
|
): QueryResultMetaStat[] {
|
|
// in the current approach, we only handle a single stat
|
|
const destStat = destStats.find((s) => s.displayName === TOTAL_BYTES_STAT);
|
|
const sourceStat = sourceStats.find((s) => s.displayName === TOTAL_BYTES_STAT);
|
|
|
|
if (sourceStat != null && destStat != null) {
|
|
return [{ value: sourceStat.value + destStat.value, displayName: TOTAL_BYTES_STAT, unit: destStat.unit }];
|
|
}
|
|
|
|
// maybe one of them exist
|
|
const eitherStat = sourceStat ?? destStat;
|
|
if (eitherStat != null) {
|
|
return [eitherStat];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Deep clones a DataQueryResponse
|
|
*/
|
|
export function cloneQueryResponse(response: DataQueryResponse): DataQueryResponse {
|
|
const newResponse = {
|
|
...response,
|
|
data: response.data.map(cloneDataFrame),
|
|
};
|
|
return newResponse;
|
|
}
|
|
|
|
function cloneDataFrame(frame: DataQueryResponseData): DataQueryResponseData {
|
|
return {
|
|
...frame,
|
|
fields: frame.fields.map((field: Field) => ({
|
|
...field,
|
|
values: field.values,
|
|
})),
|
|
};
|
|
}
|