mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Logging label filtering
- adds a custom label renderer to Logs viewer in Explore
- labels are no longer treated as strings, they are passed as parsed objects to the log row
- label renderer supports onClick handler for an action
- renamed Explore's `onClickTableCell` to `onClickLabel` and wired up log label renderers
- reuse Prometheus `addLabelToSelector` to modify Logging queries via click on label
- added tests to `addLabelToSelector`, changed to include the surrounding `{}`
- use label render also for common labels in the controls panel
- logging meta data section has now a custom renderer that can render numbers, strings, and labels
- style adjustments
This commit is contained in:
@@ -35,19 +35,26 @@ export interface LogRow {
|
|||||||
duplicates?: number;
|
duplicates?: number;
|
||||||
entry: string;
|
entry: string;
|
||||||
key: string; // timestamp + labels
|
key: string; // timestamp + labels
|
||||||
labels: string;
|
labels: LogsStreamLabels;
|
||||||
logLevel: LogLevel;
|
logLevel: LogLevel;
|
||||||
searchWords?: string[];
|
searchWords?: string[];
|
||||||
timestamp: string; // ISO with nanosec precision
|
timestamp: string; // ISO with nanosec precision
|
||||||
timeFromNow: string;
|
timeFromNow: string;
|
||||||
timeEpochMs: number;
|
timeEpochMs: number;
|
||||||
timeLocal: string;
|
timeLocal: string;
|
||||||
uniqueLabels?: string;
|
uniqueLabels?: LogsStreamLabels;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LogsMetaKind {
|
||||||
|
Number,
|
||||||
|
String,
|
||||||
|
LabelsMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogsMetaItem {
|
export interface LogsMetaItem {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string | number | LogsStreamLabels;
|
||||||
|
kind: LogsMetaKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogsModel {
|
export interface LogsModel {
|
||||||
@@ -61,7 +68,7 @@ export interface LogsStream {
|
|||||||
entries: LogsStreamEntry[];
|
entries: LogsStreamEntry[];
|
||||||
search?: string;
|
search?: string;
|
||||||
parsedLabels?: LogsStreamLabels;
|
parsedLabels?: LogsStreamLabels;
|
||||||
uniqueLabels?: string;
|
uniqueLabels?: LogsStreamLabels;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogsStreamEntry {
|
export interface LogsStreamEntry {
|
||||||
|
|||||||
@@ -429,8 +429,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickTableCell = (columnKey: string, rowValue: string) => {
|
onClickLabel = (key: string, value: string) => {
|
||||||
this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue });
|
this.onModifyQueries({ type: 'ADD_FILTER', key, value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onModifyQueries = (action, index?: number) => {
|
onModifyQueries = (action, index?: number) => {
|
||||||
@@ -931,7 +931,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
isOpen={showingTable}
|
isOpen={showingTable}
|
||||||
onToggle={this.onClickTableButton}
|
onToggle={this.onClickTableButton}
|
||||||
>
|
>
|
||||||
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
|
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
{supportsLogs && (
|
{supportsLogs && (
|
||||||
@@ -941,6 +941,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
loading={logsLoading}
|
loading={logsLoading}
|
||||||
position={position}
|
position={position}
|
||||||
onChangeTime={this.onChangeTime}
|
onChangeTime={this.onChangeTime}
|
||||||
|
onClickLabel={this.onClickLabel}
|
||||||
onStartScanning={this.onStartScanning}
|
onStartScanning={this.onStartScanning}
|
||||||
onStopScanning={this.onStopScanning}
|
onStopScanning={this.onStopScanning}
|
||||||
range={range}
|
range={range}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
import React, { Fragment, PureComponent } from 'react';
|
import React, { Fragment, PureComponent } from 'react';
|
||||||
import Highlighter from 'react-highlight-words';
|
import Highlighter from 'react-highlight-words';
|
||||||
|
|
||||||
import * as rangeUtil from 'app/core/utils/rangeutil';
|
import * as rangeUtil from 'app/core/utils/rangeutil';
|
||||||
import { RawTimeRange } from 'app/types/series';
|
import { RawTimeRange } from 'app/types/series';
|
||||||
import { LogsDedupStrategy, LogsModel, dedupLogRows, filterLogLevels, LogLevel } from 'app/core/logs_model';
|
import {
|
||||||
|
LogsDedupStrategy,
|
||||||
|
LogsModel,
|
||||||
|
dedupLogRows,
|
||||||
|
filterLogLevels,
|
||||||
|
LogLevel,
|
||||||
|
LogsStreamLabels,
|
||||||
|
LogsMetaKind,
|
||||||
|
} from 'app/core/logs_model';
|
||||||
import { findHighlightChunksInText } from 'app/core/utils/text';
|
import { findHighlightChunksInText } from 'app/core/utils/text';
|
||||||
import { Switch } from 'app/core/components/Switch/Switch';
|
import { Switch } from 'app/core/components/Switch/Switch';
|
||||||
|
|
||||||
@@ -23,6 +32,51 @@ const graphOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function renderMetaItem(value: any, kind: LogsMetaKind) {
|
||||||
|
if (kind === LogsMetaKind.LabelsMap) {
|
||||||
|
return (
|
||||||
|
<span className="logs-meta-item__value-labels">
|
||||||
|
<Labels labels={value} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Label extends PureComponent<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
|
}> {
|
||||||
|
onClickLabel = () => {
|
||||||
|
const { onClickLabel, label, value } = this.props;
|
||||||
|
if (onClickLabel) {
|
||||||
|
onClickLabel(label, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, value } = this.props;
|
||||||
|
const tooltip = `${label}: ${value}`;
|
||||||
|
return (
|
||||||
|
<span className="logs-label" title={tooltip} onClick={this.onClickLabel}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Labels extends PureComponent<{
|
||||||
|
labels: LogsStreamLabels;
|
||||||
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
|
}> {
|
||||||
|
render() {
|
||||||
|
const { labels, onClickLabel } = this.props;
|
||||||
|
return Object.keys(labels).map(key => (
|
||||||
|
<Label key={key} label={key} value={labels[key]} onClickLabel={onClickLabel} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface LogsProps {
|
interface LogsProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
data: LogsModel;
|
data: LogsModel;
|
||||||
@@ -32,6 +86,7 @@ interface LogsProps {
|
|||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
onChangeTime?: (range: RawTimeRange) => void;
|
onChangeTime?: (range: RawTimeRange) => void;
|
||||||
|
onClickLabel?: (label: string, value: string) => void;
|
||||||
onStartScanning?: () => void;
|
onStartScanning?: () => void;
|
||||||
onStopScanning?: () => void;
|
onStopScanning?: () => void;
|
||||||
}
|
}
|
||||||
@@ -39,7 +94,7 @@ interface LogsProps {
|
|||||||
interface LogsState {
|
interface LogsState {
|
||||||
dedup: LogsDedupStrategy;
|
dedup: LogsDedupStrategy;
|
||||||
hiddenLogLevels: Set<LogLevel>;
|
hiddenLogLevels: Set<LogLevel>;
|
||||||
showLabels: boolean;
|
showLabels: boolean | null; // Tristate: null means auto
|
||||||
showLocalTime: boolean;
|
showLocalTime: boolean;
|
||||||
showUtc: boolean;
|
showUtc: boolean;
|
||||||
}
|
}
|
||||||
@@ -48,7 +103,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
state = {
|
state = {
|
||||||
dedup: LogsDedupStrategy.none,
|
dedup: LogsDedupStrategy.none,
|
||||||
hiddenLogLevels: new Set(),
|
hiddenLogLevels: new Set(),
|
||||||
showLabels: true,
|
showLabels: null,
|
||||||
showLocalTime: true,
|
showLocalTime: true,
|
||||||
showUtc: false,
|
showUtc: false,
|
||||||
};
|
};
|
||||||
@@ -99,9 +154,12 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className = '', data, loading = false, position, range, scanning, scanRange } = this.props;
|
const { className = '', data, loading = false, onClickLabel, position, range, scanning, scanRange } = this.props;
|
||||||
const { dedup, hiddenLogLevels, showLabels, showLocalTime, showUtc } = this.state;
|
const { dedup, hiddenLogLevels, showLocalTime, showUtc } = this.state;
|
||||||
|
let { showLabels } = this.state;
|
||||||
const hasData = data && data.rows && data.rows.length > 0;
|
const hasData = data && data.rows && data.rows.length > 0;
|
||||||
|
|
||||||
|
// Filtering
|
||||||
const filteredData = filterLogLevels(data, hiddenLogLevels);
|
const filteredData = filterLogLevels(data, hiddenLogLevels);
|
||||||
const dedupedData = dedupLogRows(filteredData, dedup);
|
const dedupedData = dedupLogRows(filteredData, dedup);
|
||||||
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
|
const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0);
|
||||||
@@ -109,9 +167,17 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
if (dedup !== LogsDedupStrategy.none) {
|
if (dedup !== LogsDedupStrategy.none) {
|
||||||
meta.push({
|
meta.push({
|
||||||
label: 'Dedup count',
|
label: 'Dedup count',
|
||||||
value: String(dedupCount),
|
value: dedupCount,
|
||||||
|
kind: LogsMetaKind.Number,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for labels
|
||||||
|
if (showLabels === null && hasData) {
|
||||||
|
showLabels = data.rows.some(row => _.size(row.uniqueLabels) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid options
|
||||||
const cssColumnSizes = ['3px']; // Log-level indicator line
|
const cssColumnSizes = ['3px']; // Log-level indicator line
|
||||||
if (showUtc) {
|
if (showUtc) {
|
||||||
cssColumnSizes.push('minmax(100px, max-content)');
|
cssColumnSizes.push('minmax(100px, max-content)');
|
||||||
@@ -177,7 +243,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
{meta.map(item => (
|
{meta.map(item => (
|
||||||
<div className="logs-meta-item" key={item.label}>
|
<div className="logs-meta-item" key={item.label}>
|
||||||
<span className="logs-meta-item__label">{item.label}:</span>
|
<span className="logs-meta-item__label">{item.label}:</span>
|
||||||
<span className="logs-meta-item__value">{item.value}</span>
|
<span className="logs-meta-item__value">{renderMetaItem(item.value, item.kind)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -201,8 +267,8 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
|
{showUtc && <div title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>{row.timestamp}</div>}
|
||||||
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
|
{showLocalTime && <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>}
|
||||||
{showLabels && (
|
{showLabels && (
|
||||||
<div className="max-width" title={row.labels}>
|
<div className="logs-row-labels">
|
||||||
{row.labels}
|
<Labels labels={row.uniqueLabels} onClickLabel={onClickLabel} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import _ from 'lodash';
|
|||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
|
import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model';
|
||||||
import { PluginMeta, DataQuery } from 'app/types';
|
import { PluginMeta, DataQuery } from 'app/types';
|
||||||
|
import { addLabelToSelector } from 'app/plugins/datasource/prometheus/add_label_to_query';
|
||||||
|
|
||||||
import LanguageProvider from './language_provider';
|
import LanguageProvider from './language_provider';
|
||||||
import { mergeStreamsToLogs } from './result_transformer';
|
import { mergeStreamsToLogs } from './result_transformer';
|
||||||
|
import { formatQuery, parseQuery } from './query_utils';
|
||||||
|
|
||||||
export const DEFAULT_LIMIT = 1000;
|
export const DEFAULT_LIMIT = 1000;
|
||||||
|
|
||||||
@@ -16,20 +18,6 @@ const DEFAULT_QUERY_PARAMS = {
|
|||||||
query: '',
|
query: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectorRegexp = /{[^{]*}/g;
|
|
||||||
export function parseQuery(input: string) {
|
|
||||||
const match = input.match(selectorRegexp);
|
|
||||||
let query = '';
|
|
||||||
let regexp = input;
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
query = match[0];
|
|
||||||
regexp = input.replace(selectorRegexp, '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { query, regexp };
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeParams(data: any) {
|
function serializeParams(data: any) {
|
||||||
return Object.keys(data)
|
return Object.keys(data)
|
||||||
.map(k => {
|
.map(k => {
|
||||||
@@ -114,6 +102,21 @@ export default class LoggingDatasource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modifyQuery(query: DataQuery, action: any): DataQuery {
|
||||||
|
const parsed = parseQuery(query.expr || '');
|
||||||
|
let selector = parsed.query;
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_FILTER': {
|
||||||
|
selector = addLabelToSelector(selector, action.key, action.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const expression = formatQuery(selector, parsed.regexp);
|
||||||
|
return { ...query, expr: expression };
|
||||||
|
}
|
||||||
|
|
||||||
getTime(date, roundUp) {
|
getTime(date, roundUp) {
|
||||||
if (_.isString(date)) {
|
if (_.isString(date)) {
|
||||||
date = dateMath.parse(date, roundUp);
|
date = dateMath.parse(date, roundUp);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { parseQuery } from './datasource';
|
import { parseQuery } from './query_utils';
|
||||||
|
|
||||||
describe('parseQuery', () => {
|
describe('parseQuery', () => {
|
||||||
it('returns empty for empty string', () => {
|
it('returns empty for empty string', () => {
|
||||||
17
public/app/plugins/datasource/logging/query_utils.ts
Normal file
17
public/app/plugins/datasource/logging/query_utils.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const selectorRegexp = /{[^{]*}/g;
|
||||||
|
export function parseQuery(input: string) {
|
||||||
|
const match = input.match(selectorRegexp);
|
||||||
|
let query = '';
|
||||||
|
let regexp = input;
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
query = match[0];
|
||||||
|
regexp = input.replace(selectorRegexp, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { query, regexp };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatQuery(selector: string, search: string): string {
|
||||||
|
return `${selector || ''} ${search || ''}`.trim();
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ describe('parseLabels()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns labels on labels string', () => {
|
it('returns labels on labels string', () => {
|
||||||
expect(parseLabels('{foo="bar", baz="42"}')).toEqual({ foo: '"bar"', baz: '"42"' });
|
expect(parseLabels('{foo="bar", baz="42"}')).toEqual({ foo: 'bar', baz: '42' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ describe('formatLabels()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns label string on label set', () => {
|
it('returns label string on label set', () => {
|
||||||
expect(formatLabels({ foo: '"bar"', baz: '"42"' })).toEqual('{baz="42", foo="bar"}');
|
expect(formatLabels({ foo: 'bar', baz: '42' })).toEqual('{baz="42", foo="bar"}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,14 +63,14 @@ describe('findCommonLabels()', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns no common labels on differing sets', () => {
|
it('returns no common labels on differing sets', () => {
|
||||||
expect(findCommonLabels([{ foo: '"bar"' }, {}])).toEqual({});
|
expect(findCommonLabels([{ foo: 'bar' }, {}])).toEqual({});
|
||||||
expect(findCommonLabels([{}, { foo: '"bar"' }])).toEqual({});
|
expect(findCommonLabels([{}, { foo: 'bar' }])).toEqual({});
|
||||||
expect(findCommonLabels([{ baz: '42' }, { foo: '"bar"' }])).toEqual({});
|
expect(findCommonLabels([{ baz: '42' }, { foo: 'bar' }])).toEqual({});
|
||||||
expect(findCommonLabels([{ foo: '42', baz: '"bar"' }, { foo: '"bar"' }])).toEqual({});
|
expect(findCommonLabels([{ foo: '42', baz: 'bar' }, { foo: 'bar' }])).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the single labels set as common labels', () => {
|
it('returns the single labels set as common labels', () => {
|
||||||
expect(findCommonLabels([{ foo: '"bar"' }])).toEqual({ foo: '"bar"' });
|
expect(findCommonLabels([{ foo: 'bar' }])).toEqual({ foo: 'bar' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,10 +106,10 @@ describe('mergeStreamsToLogs()', () => {
|
|||||||
expect(mergeStreamsToLogs([stream1]).rows).toMatchObject([
|
expect(mergeStreamsToLogs([stream1]).rows).toMatchObject([
|
||||||
{
|
{
|
||||||
entry: 'WARN boooo',
|
entry: 'WARN boooo',
|
||||||
labels: '{foo="bar"}',
|
labels: { foo: 'bar' },
|
||||||
key: 'EK1970-01-01T00:00:00Z{foo="bar"}',
|
key: 'EK1970-01-01T00:00:00Z{foo="bar"}',
|
||||||
logLevel: 'warning',
|
logLevel: 'warning',
|
||||||
uniqueLabels: '',
|
uniqueLabels: {},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -140,21 +140,21 @@ describe('mergeStreamsToLogs()', () => {
|
|||||||
expect(mergeStreamsToLogs([stream1, stream2]).rows).toMatchObject([
|
expect(mergeStreamsToLogs([stream1, stream2]).rows).toMatchObject([
|
||||||
{
|
{
|
||||||
entry: 'INFO 2',
|
entry: 'INFO 2',
|
||||||
labels: '{foo="bar", baz="2"}',
|
labels: { foo: 'bar', baz: '2' },
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
uniqueLabels: '{baz="2"}',
|
uniqueLabels: { baz: '2' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'WARN boooo',
|
entry: 'WARN boooo',
|
||||||
labels: '{foo="bar", baz="1"}',
|
labels: { foo: 'bar', baz: '1' },
|
||||||
logLevel: 'warning',
|
logLevel: 'warning',
|
||||||
uniqueLabels: '{baz="1"}',
|
uniqueLabels: { baz: '1' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'INFO 1',
|
entry: 'INFO 1',
|
||||||
labels: '{foo="bar", baz="2"}',
|
labels: { foo: 'bar', baz: '2' },
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
uniqueLabels: '{baz="2"}',
|
uniqueLabels: { baz: '2' },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
LogsStream,
|
LogsStream,
|
||||||
LogsStreamEntry,
|
LogsStreamEntry,
|
||||||
LogsStreamLabels,
|
LogsStreamLabels,
|
||||||
|
LogsMetaKind,
|
||||||
} from 'app/core/logs_model';
|
} from 'app/core/logs_model';
|
||||||
import { DEFAULT_LIMIT } from './datasource';
|
import { DEFAULT_LIMIT } from './datasource';
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export function getLogLevel(line: string): LogLevel {
|
|||||||
/**
|
/**
|
||||||
* Regexp to extract Prometheus-style labels
|
* Regexp to extract Prometheus-style labels
|
||||||
*/
|
*/
|
||||||
const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g;
|
const labelRegexp = /\b(\w+)(!?=~?)"([^"\n]*?)"/g;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a map of label keys to value from an input selector string.
|
* Returns a map of label keys to value from an input selector string.
|
||||||
@@ -104,11 +105,17 @@ export function formatLabels(labels: LogsStreamLabels, defaultValue = ''): strin
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
const labelKeys = Object.keys(labels).sort();
|
const labelKeys = Object.keys(labels).sort();
|
||||||
const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(', ');
|
const cleanSelector = labelKeys.map(key => `${key}="${labels[key]}"`).join(', ');
|
||||||
return ['{', cleanSelector, '}'].join('');
|
return ['{', cleanSelector, '}'].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processEntry(entry: LogsStreamEntry, labels: string, uniqueLabels: string, search: string): LogRow {
|
export function processEntry(
|
||||||
|
entry: LogsStreamEntry,
|
||||||
|
labels: string,
|
||||||
|
parsedLabels: LogsStreamLabels,
|
||||||
|
uniqueLabels: LogsStreamLabels,
|
||||||
|
search: string
|
||||||
|
): LogRow {
|
||||||
const { line, timestamp } = entry;
|
const { line, timestamp } = entry;
|
||||||
// Assumes unique-ness, needs nanosec precision for timestamp
|
// Assumes unique-ness, needs nanosec precision for timestamp
|
||||||
const key = `EK${timestamp}${labels}`;
|
const key = `EK${timestamp}${labels}`;
|
||||||
@@ -120,13 +127,13 @@ export function processEntry(entry: LogsStreamEntry, labels: string, uniqueLabel
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
labels,
|
|
||||||
logLevel,
|
logLevel,
|
||||||
timeFromNow,
|
timeFromNow,
|
||||||
timeEpochMs,
|
timeEpochMs,
|
||||||
timeLocal,
|
timeLocal,
|
||||||
uniqueLabels,
|
uniqueLabels,
|
||||||
entry: line,
|
entry: line,
|
||||||
|
labels: parsedLabels,
|
||||||
searchWords: search ? [search] : [],
|
searchWords: search ? [search] : [],
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
};
|
};
|
||||||
@@ -141,7 +148,7 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
|
|||||||
const commonLabels = findCommonLabels(streams.map(model => model.parsedLabels));
|
const commonLabels = findCommonLabels(streams.map(model => model.parsedLabels));
|
||||||
streams = streams.map(stream => ({
|
streams = streams.map(stream => ({
|
||||||
...stream,
|
...stream,
|
||||||
uniqueLabels: formatLabels(findUniqueLabels(stream.parsedLabels, commonLabels)),
|
uniqueLabels: findUniqueLabels(stream.parsedLabels, commonLabels),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Merge stream entries into single list of log rows
|
// Merge stream entries into single list of log rows
|
||||||
@@ -149,7 +156,9 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
|
|||||||
.reduce(
|
.reduce(
|
||||||
(acc: LogRow[], stream: LogsStream) => [
|
(acc: LogRow[], stream: LogsStream) => [
|
||||||
...acc,
|
...acc,
|
||||||
...stream.entries.map(entry => processEntry(entry, stream.labels, stream.uniqueLabels, stream.search)),
|
...stream.entries.map(entry =>
|
||||||
|
processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search)
|
||||||
|
),
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -162,13 +171,15 @@ export function mergeStreamsToLogs(streams: LogsStream[], limit = DEFAULT_LIMIT)
|
|||||||
if (_.size(commonLabels) > 0) {
|
if (_.size(commonLabels) > 0) {
|
||||||
meta.push({
|
meta.push({
|
||||||
label: 'Common labels',
|
label: 'Common labels',
|
||||||
value: formatLabels(commonLabels),
|
value: commonLabels,
|
||||||
|
kind: LogsMetaKind.LabelsMap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (limit) {
|
if (limit) {
|
||||||
meta.push({
|
meta.push({
|
||||||
label: 'Limit',
|
label: 'Limit',
|
||||||
value: `${limit} (${sortedRows.length} returned)`,
|
value: `${limit} (${sortedRows.length} returned)`,
|
||||||
|
kind: LogsMetaKind.String,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera
|
|||||||
const selectorWithLabel = addLabelToSelector(selector, key, value, operator);
|
const selectorWithLabel = addLabelToSelector(selector, key, value, operator);
|
||||||
lastIndex = match.index + match[1].length + 2;
|
lastIndex = match.index + match[1].length + 2;
|
||||||
suffix = query.slice(match.index + match[0].length);
|
suffix = query.slice(match.index + match[0].length);
|
||||||
parts.push(prefix, '{', selectorWithLabel, '}');
|
parts.push(prefix, selectorWithLabel);
|
||||||
match = selectorRegexp.exec(query);
|
match = selectorRegexp.exec(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export function addLabelToQuery(query: string, key: string, value: string, opera
|
|||||||
|
|
||||||
const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g;
|
const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g;
|
||||||
|
|
||||||
function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) {
|
export function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) {
|
||||||
const parsedLabels = [];
|
const parsedLabels = [];
|
||||||
|
|
||||||
// Split selector into labels
|
// Split selector into labels
|
||||||
@@ -76,13 +76,15 @@ function addLabelToSelector(selector: string, labelKey: string, labelValue: stri
|
|||||||
parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });
|
parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });
|
||||||
|
|
||||||
// Sort labels by key and put them together
|
// Sort labels by key and put them together
|
||||||
return _.chain(parsedLabels)
|
const formatted = _.chain(parsedLabels)
|
||||||
.uniqWith(_.isEqual)
|
.uniqWith(_.isEqual)
|
||||||
.compact()
|
.compact()
|
||||||
.sortBy('key')
|
.sortBy('key')
|
||||||
.map(({ key, operator, value }) => `${key}${operator}${value}`)
|
.map(({ key, operator, value }) => `${key}${operator}${value}`)
|
||||||
.value()
|
.value()
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
|
return `{${formatted}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
|
function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import addLabelToQuery from '../add_label_to_query';
|
import { addLabelToQuery, addLabelToSelector } from '../add_label_to_query';
|
||||||
|
|
||||||
describe('addLabelToQuery()', () => {
|
describe('addLabelToQuery()', () => {
|
||||||
it('should add label to simple query', () => {
|
it('should add label to simple query', () => {
|
||||||
@@ -56,3 +56,16 @@ describe('addLabelToQuery()', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('addLabelToSelector()', () => {
|
||||||
|
test('should add a label to an empty selector', () => {
|
||||||
|
expect(addLabelToSelector('{}', 'foo', 'bar')).toBe('{foo="bar"}');
|
||||||
|
expect(addLabelToSelector('', 'foo', 'bar')).toBe('{foo="bar"}');
|
||||||
|
});
|
||||||
|
test('should add a label to a selector', () => {
|
||||||
|
expect(addLabelToSelector('{foo="bar"}', 'baz', '42')).toBe('{baz="42",foo="bar"}');
|
||||||
|
});
|
||||||
|
test('should add a label to a selector with custom operator', () => {
|
||||||
|
expect(addLabelToSelector('{}', 'baz', '42', '!=')).toBe('{baz!="42"}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -261,6 +261,8 @@
|
|||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
margin: 2*$panel-margin 0;
|
margin: 2*$panel-margin 0;
|
||||||
border: $panel-border;
|
border: $panel-border;
|
||||||
|
justify-items: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
@@ -276,11 +278,11 @@
|
|||||||
.logs-meta {
|
.logs-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: $text-color-weak;
|
color: $text-color-weak;
|
||||||
padding: 2px 0;
|
// Align first line with controls labels
|
||||||
|
margin-top: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logs-meta-item {
|
.logs-meta-item {
|
||||||
display: inline-block;
|
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +296,12 @@
|
|||||||
font-family: $font-family-monospace;
|
font-family: $font-family-monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-meta-item__value-labels {
|
||||||
|
// compensate for the labels padding
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.logs-row-match-highlight {
|
.logs-row-match-highlight {
|
||||||
// Undoing mark styling
|
// Undoing mark styling
|
||||||
background: inherit;
|
background: inherit;
|
||||||
@@ -356,6 +364,25 @@
|
|||||||
background-color: #1f78c1;
|
background-color: #1f78c1;
|
||||||
margin: 0 1px 1px 0;
|
margin: 0 1px 1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 2px;
|
||||||
|
background-color: $btn-inverse-bg;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
margin-right: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-row-labels {
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
|
.logs-label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user