2025-04-01 10:38:02 +09:00

123 lines
4.4 KiB
TypeScript

import { groupBy } from 'lodash';
import { FieldType, DataFrame, DataLink, Field } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { DerivedFieldConfig } from './types';
export function getDerivedFields(dataFrame: DataFrame, derivedFieldConfigs: DerivedFieldConfig[]): Field[] {
if (!derivedFieldConfigs.length) {
return [];
}
const derivedFieldsGrouped = groupBy(derivedFieldConfigs, 'name');
const newFields = Object.values(derivedFieldsGrouped).map(fieldFromDerivedFieldConfig);
// line-field is the first string-field
// NOTE: we should create some common log-frame-extra-string-field code somewhere
const lineField = dataFrame.fields.find((f) => f.type === FieldType.string);
if (lineField === undefined) {
// if this is happening, something went wrong, let's raise an error
throw new Error('invalid logs-dataframe, string-field missing');
}
const labelFields = dataFrame.fields.find((f) => f.type === FieldType.other && f.name === 'labels');
for (let i = 0; i < lineField.values.length; i++) {
for (const field of newFields) {
// `matcherRegex` can be either a RegExp that is used to extract the value from the log line, or it can be a label key to derive the field from the labels
if (derivedFieldsGrouped[field.name][0].matcherType === 'label' && labelFields) {
const label = labelFields.values[i];
if (label) {
// Find the key that matches the regex pattern in `matcherRegex`
const intersectingKey = Object.keys(label).find((key) => {
const regex = new RegExp(derivedFieldsGrouped[field.name][0].matcherRegex);
return regex.test(key);
});
if (intersectingKey) {
field.values.push(label[intersectingKey]);
continue;
}
}
field.values.push(null);
} else if (
derivedFieldsGrouped[field.name][0].matcherType === 'regex' ||
derivedFieldsGrouped[field.name][0].matcherType === undefined
) {
// `matcherRegex` will actually be used as a RegExp here
const line = lineField.values[i];
const logMatch = line.match(derivedFieldsGrouped[field.name][0].matcherRegex);
if (logMatch && logMatch[1]) {
field.values.push(logMatch[1]);
continue;
}
field.values.push(null);
} else {
field.values.push(null);
}
}
}
return newFields;
}
/**
* Transform derivedField config into dataframe field with config that contains link.
*/
function fieldFromDerivedFieldConfig(derivedFieldConfigs: DerivedFieldConfig[]): Field {
const dataSourceSrv = getDataSourceSrv();
const dataLinks = derivedFieldConfigs.reduce<DataLink[]>((acc, derivedFieldConfig) => {
// Having field.datasourceUid means it is an internal link.
if (derivedFieldConfig.datasourceUid) {
const dsSettings = dataSourceSrv.getInstanceSettings(derivedFieldConfig.datasourceUid);
const queryType = (type: string | undefined): string | undefined => {
switch (type) {
case 'tempo':
return 'traceql';
case 'grafana-x-ray-datasource':
return 'getTrace';
default:
return undefined;
}
};
acc.push({
// Will be filled out later
title: derivedFieldConfig.urlDisplayLabel || '',
url: '',
// This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object
internal: {
query: { query: derivedFieldConfig.url, queryType: queryType(dsSettings?.type) },
datasourceUid: derivedFieldConfig.datasourceUid,
datasourceName: dsSettings?.name ?? 'Data source not found',
},
targetBlank: derivedFieldConfig.targetBlank,
});
} else if (derivedFieldConfig.url) {
acc.push({
// We do not know what title to give here so we count on presentation layer to create a title from metadata.
title: derivedFieldConfig.urlDisplayLabel || '',
// This is hardcoded for Jaeger or Zipkin not way right now to specify datasource specific query object
url: derivedFieldConfig.url,
targetBlank: derivedFieldConfig.targetBlank,
});
}
return acc;
}, []);
return {
name: derivedFieldConfigs[0].name,
type: FieldType.string,
config: {
links: dataLinks,
},
// We are adding values later on
values: [],
};
}