mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: repair logging after code restructuring
this is a fix-up PR that cleans up Explore Logging after the recent restructuring. - log results need to be merged since query transactions have been introduced - logging DS has its own language provider, query field, and start page (some of them based on prometheus components) - added loader animation to log viewer - removed logging logic from prometheus components
This commit is contained in:
parent
758ec4bc70
commit
c92f5462fe
@ -1,3 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export enum LogLevel {
|
||||
crit = 'crit',
|
||||
warn = 'warn',
|
||||
@ -27,3 +29,15 @@ export interface LogRow {
|
||||
export interface LogsModel {
|
||||
rows: LogRow[];
|
||||
}
|
||||
|
||||
export function mergeStreams(streams: LogsModel[], limit?: number): LogsModel {
|
||||
const combinedEntries = streams.reduce((acc, stream) => {
|
||||
return [...acc, ...stream.rows];
|
||||
}, []);
|
||||
const sortedEntries = _.chain(combinedEntries)
|
||||
.sortBy('timestamp')
|
||||
.reverse()
|
||||
.slice(0, limit || combinedEntries.length)
|
||||
.value();
|
||||
return { rows: sortedEntries };
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import ErrorBoundary from './ErrorBoundary';
|
||||
import TimePicker from './TimePicker';
|
||||
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||
import { DataSource } from 'app/types/datasources';
|
||||
import { mergeStreams } from 'app/core/logs_model';
|
||||
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
@ -769,11 +770,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
new TableModel(),
|
||||
...queryTransactions.filter(qt => qt.resultType === 'Table' && qt.done && qt.result).map(qt => qt.result)
|
||||
);
|
||||
const logsResult = _.flatten(
|
||||
const logsResult = mergeStreams(
|
||||
queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
|
||||
);
|
||||
const loading = queryTransactions.some(qt => !qt.done);
|
||||
const showStartPages = StartPage && queryTransactions.length === 0;
|
||||
const viewModeCount = [supportsGraph, supportsLogs, supportsTable].filter(m => m).length;
|
||||
|
||||
return (
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
@ -858,7 +860,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
onClickHintFix={this.onModifyQueries}
|
||||
onExecuteQuery={this.onSubmit}
|
||||
onRemoveQueryRow={this.onRemoveQueryRow}
|
||||
supportsLogs={supportsLogs}
|
||||
transactions={queryTransactions}
|
||||
/>
|
||||
<main className="m-t-2">
|
||||
@ -866,23 +867,25 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
{showStartPages && <StartPage onClickQuery={this.onClickQuery} />}
|
||||
{!showStartPages && (
|
||||
<>
|
||||
<div className="result-options">
|
||||
{supportsGraph ? (
|
||||
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
) : null}
|
||||
{supportsTable ? (
|
||||
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
) : null}
|
||||
{supportsLogs ? (
|
||||
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
||||
Logs
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{viewModeCount > 1 && (
|
||||
<div className="result-options">
|
||||
{supportsGraph ? (
|
||||
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
) : null}
|
||||
{supportsTable ? (
|
||||
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
) : null}
|
||||
{supportsLogs ? (
|
||||
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
||||
Logs
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{supportsGraph &&
|
||||
showingGraph && (
|
||||
|
@ -169,7 +169,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
|
||||
|
||||
return (
|
||||
<div className="panel-container">
|
||||
{loading && <div className="explore-graph__loader" />}
|
||||
{loading && <div className="explore-panel__loader" />}
|
||||
{this.props.data &&
|
||||
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
|
||||
!this.state.showAllTimeSeries && (
|
||||
|
@ -10,37 +10,33 @@ interface LogsProps {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const EXAMPLE_QUERY = '{job="default/prometheus"}';
|
||||
|
||||
export default class Logs extends PureComponent<LogsProps, {}> {
|
||||
render() {
|
||||
const { className = '', data } = this.props;
|
||||
const { className = '', data, loading = false } = this.props;
|
||||
const hasData = data && data.rows && data.rows.length > 0;
|
||||
return (
|
||||
<div className={`${className} logs`}>
|
||||
{hasData ? (
|
||||
<div className="logs-entries panel-container">
|
||||
{data.rows.map(row => (
|
||||
<Fragment key={row.key}>
|
||||
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
|
||||
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
|
||||
<div>
|
||||
<Highlighter
|
||||
textToHighlight={row.entry}
|
||||
searchWords={row.searchWords}
|
||||
findChunks={findHighlightChunksInText}
|
||||
highlightClassName="logs-row-match-highlight"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
<div className="panel-container">
|
||||
{loading && <div className="explore-panel__loader" />}
|
||||
<div className="logs-entries">
|
||||
{hasData &&
|
||||
data.rows.map(row => (
|
||||
<Fragment key={row.key}>
|
||||
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
|
||||
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
|
||||
<div>
|
||||
<Highlighter
|
||||
textToHighlight={row.entry}
|
||||
searchWords={row.searchWords}
|
||||
findChunks={findHighlightChunksInText}
|
||||
highlightClassName="logs-row-match-highlight"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{!hasData ? (
|
||||
<div className="panel-container">
|
||||
Enter a query like <code>{EXAMPLE_QUERY}</code>
|
||||
</div>
|
||||
) : null}
|
||||
{!loading && !hasData && 'No data was returned.'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -26,8 +26,6 @@ interface QueryRowCommonProps {
|
||||
className?: string;
|
||||
datasource: DataSource;
|
||||
history: HistoryItem[];
|
||||
// Temporarily
|
||||
supportsLogs?: boolean;
|
||||
transactions: QueryTransaction[];
|
||||
}
|
||||
|
||||
@ -78,7 +76,7 @@ class QueryRow extends PureComponent<QueryRowProps> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasource, history, query, supportsLogs, transactions } = this.props;
|
||||
const { datasource, history, query, transactions } = this.props;
|
||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||
const hint = getFirstHintFromTransactions(transactions);
|
||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||
@ -98,7 +96,6 @@ class QueryRow extends PureComponent<QueryRowProps> {
|
||||
onClickHintFix={this.onClickHintFix}
|
||||
onPressEnter={this.onPressEnter}
|
||||
onQueryChange={this.onChangeQuery}
|
||||
supportsLogs={supportsLogs}
|
||||
/>
|
||||
</div>
|
||||
<div className="query-row-tools">
|
||||
|
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
const CHEAT_SHEET_ITEMS = [
|
||||
{
|
||||
title: 'Logs From a Job',
|
||||
expression: '{job="default/prometheus"}',
|
||||
label: 'Returns all log lines emitted by instances of this job.',
|
||||
},
|
||||
{
|
||||
title: 'Search For Text',
|
||||
expression: '{app="cassandra"} Maximum memory usage',
|
||||
label: 'Returns all log lines for the selector and highlights the given text in the results.',
|
||||
},
|
||||
];
|
||||
|
||||
export default (props: any) => (
|
||||
<div>
|
||||
<h1>Logging Cheat Sheet</h1>
|
||||
{CHEAT_SHEET_ITEMS.map(item => (
|
||||
<div className="cheat-sheet-item" key={item.expression}>
|
||||
<div className="cheat-sheet-item__title">{item.title}</div>
|
||||
<div className="cheat-sheet-item__expression" onClick={e => props.onClickQuery(item.expression)}>
|
||||
<code>{item.expression}</code>
|
||||
</div>
|
||||
<div className="cheat-sheet-item__label">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
@ -0,0 +1,205 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import Cascader from 'rc-cascader';
|
||||
import PluginPrism from 'slate-prism';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import { TypeaheadOutput } from 'app/types/explore';
|
||||
|
||||
// dom also includes Element polyfills
|
||||
import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
|
||||
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
|
||||
import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
|
||||
|
||||
const PRISM_SYNTAX = 'promql';
|
||||
|
||||
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
|
||||
// Modify suggestion based on context
|
||||
switch (typeaheadContext) {
|
||||
case 'context-labels': {
|
||||
const nextChar = getNextCharacter();
|
||||
if (!nextChar || nextChar === '}' || nextChar === ',') {
|
||||
suggestion += '=';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'context-label-values': {
|
||||
// Always add quotes and remove existing ones instead
|
||||
if (!typeaheadText.match(/^(!?=~?"|")/)) {
|
||||
suggestion = `"${suggestion}`;
|
||||
}
|
||||
if (getNextCharacter() !== '"') {
|
||||
suggestion = `${suggestion}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
interface CascaderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: CascaderOption[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface LoggingQueryFieldProps {
|
||||
datasource: any;
|
||||
error?: string | JSX.Element;
|
||||
hint?: any;
|
||||
history?: any[];
|
||||
initialQuery?: string | null;
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: string, override?: boolean) => void;
|
||||
}
|
||||
|
||||
interface LoggingQueryFieldState {
|
||||
logLabelOptions: any[];
|
||||
syntaxLoaded: boolean;
|
||||
}
|
||||
|
||||
class LoggingQueryField extends React.PureComponent<LoggingQueryFieldProps, LoggingQueryFieldState> {
|
||||
plugins: any[];
|
||||
languageProvider: any;
|
||||
|
||||
constructor(props: LoggingQueryFieldProps, context) {
|
||||
super(props, context);
|
||||
|
||||
if (props.datasource.languageProvider) {
|
||||
this.languageProvider = props.datasource.languageProvider;
|
||||
}
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
RunnerPlugin({ handler: props.onPressEnter }),
|
||||
PluginPrism({
|
||||
onlyIn: node => node.type === 'code_block',
|
||||
getSyntax: node => 'promql',
|
||||
}),
|
||||
];
|
||||
|
||||
this.state = {
|
||||
logLabelOptions: [],
|
||||
syntaxLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.languageProvider) {
|
||||
this.languageProvider.start().then(() => this.onReceiveMetrics());
|
||||
}
|
||||
}
|
||||
|
||||
onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||
let query;
|
||||
if (selectedOptions.length === 1) {
|
||||
if (selectedOptions[0].children.length === 0) {
|
||||
query = selectedOptions[0].value;
|
||||
} else {
|
||||
// Ignore click on group
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const key = selectedOptions[0].value;
|
||||
const value = selectedOptions[1].value;
|
||||
query = `{${key}="${value}"}`;
|
||||
}
|
||||
this.onChangeQuery(query, true);
|
||||
};
|
||||
|
||||
onChangeQuery = (value: string, override?: boolean) => {
|
||||
// Send text change to parent
|
||||
const { onQueryChange } = this.props;
|
||||
if (onQueryChange) {
|
||||
onQueryChange(value, override);
|
||||
}
|
||||
};
|
||||
|
||||
onClickHintFix = () => {
|
||||
const { hint, onClickHintFix } = this.props;
|
||||
if (onClickHintFix && hint && hint.fix) {
|
||||
onClickHintFix(hint.fix.action);
|
||||
}
|
||||
};
|
||||
|
||||
onReceiveMetrics = () => {
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
||||
const { logLabelOptions } = this.languageProvider;
|
||||
this.setState({
|
||||
logLabelOptions,
|
||||
syntaxLoaded: true,
|
||||
});
|
||||
};
|
||||
|
||||
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
|
||||
if (!this.languageProvider) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
|
||||
const { history } = this.props;
|
||||
const { prefix, text, value, wrapperNode } = typeahead;
|
||||
|
||||
// Get DOM-dependent context
|
||||
const wrapperClasses = Array.from(wrapperNode.classList);
|
||||
const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
|
||||
const labelKey = labelKeyNode && labelKeyNode.textContent;
|
||||
const nextChar = getNextCharacter();
|
||||
|
||||
const result = this.languageProvider.provideCompletionItems(
|
||||
{ text, value, prefix, wrapperClasses, labelKey },
|
||||
{ history }
|
||||
);
|
||||
|
||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error, hint, initialQuery } = this.props;
|
||||
const { logLabelOptions, syntaxLoaded } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
|
||||
return (
|
||||
<div className="prom-query-field">
|
||||
<div className="prom-query-field-tools">
|
||||
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels}>
|
||||
<button className="btn navbar-button navbar-button--tight">Log labels</button>
|
||||
</Cascader>
|
||||
</div>
|
||||
<div className="prom-query-field-wrapper">
|
||||
<TypeaheadField
|
||||
additionalPlugins={this.plugins}
|
||||
cleanText={cleanText}
|
||||
initialValue={initialQuery}
|
||||
onTypeahead={this.onTypeahead}
|
||||
onWillApplySuggestion={willApplySuggestion}
|
||||
onValueChanged={this.onChangeQuery}
|
||||
placeholder="Enter a PromQL query"
|
||||
portalOrigin="prometheus"
|
||||
syntaxLoaded={syntaxLoaded}
|
||||
/>
|
||||
{error ? <div className="prom-query-field-info text-error">{error}</div> : null}
|
||||
{hint ? (
|
||||
<div className="prom-query-field-info text-warning">
|
||||
{hint.label}{' '}
|
||||
{hint.fix ? (
|
||||
<a className="text-link muted" onClick={this.onClickHintFix}>
|
||||
{hint.fix.label}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LoggingQueryField;
|
@ -0,0 +1,60 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import LoggingCheatSheet from './LoggingCheatSheet';
|
||||
|
||||
const TAB_MENU_ITEMS = [
|
||||
{
|
||||
text: 'Start',
|
||||
id: 'start',
|
||||
icon: 'fa fa-rocket',
|
||||
},
|
||||
];
|
||||
|
||||
export default class LoggingStartPage extends PureComponent<any, { active: string }> {
|
||||
state = {
|
||||
active: 'start',
|
||||
};
|
||||
|
||||
onClickTab = active => {
|
||||
this.setState({ active });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { active } = this.state;
|
||||
const customCss = '';
|
||||
|
||||
return (
|
||||
<div style={{ margin: '45px 0', border: '1px solid #ddd', borderRadius: 5 }}>
|
||||
<div className="page-header-canvas">
|
||||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
<nav>
|
||||
<ul className={`gf-tabs ${customCss}`}>
|
||||
{TAB_MENU_ITEMS.map((tab, idx) => {
|
||||
const tabClasses = classNames({
|
||||
'gf-tabs-link': true,
|
||||
active: tab.id === active,
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="gf-tabs-item" key={tab.id}>
|
||||
<a className={tabClasses} onClick={() => this.onClickTab(tab.id)}>
|
||||
<i className={tab.icon} />
|
||||
{tab.text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-container page-body">
|
||||
{active === 'start' && <LoggingCheatSheet onClickQuery={this.props.onClickQuery} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
|
||||
import LanguageProvider from './language_provider';
|
||||
import { processStreams } from './result_transformer';
|
||||
|
||||
const DEFAULT_LIMIT = 100;
|
||||
@ -48,8 +49,12 @@ function serializeParams(data: any) {
|
||||
}
|
||||
|
||||
export default class LoggingDatasource {
|
||||
languageProvider: LanguageProvider;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private instanceSettings, private backendSrv, private templateSrv) {}
|
||||
constructor(private instanceSettings, private backendSrv, private templateSrv) {
|
||||
this.languageProvider = new LanguageProvider(this);
|
||||
}
|
||||
|
||||
_request(apiUrl: string, data?, options?: any) {
|
||||
const baseUrl = this.instanceSettings.url;
|
||||
|
211
public/app/plugins/datasource/logging/language_provider.ts
Normal file
211
public/app/plugins/datasource/logging/language_provider.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
CompletionItem,
|
||||
CompletionItemGroup,
|
||||
LanguageProvider,
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
} from 'app/types/explore';
|
||||
|
||||
import { parseSelector } from 'app/plugins/datasource/prometheus/language_utils';
|
||||
import PromqlSyntax from 'app/plugins/datasource/prometheus/promql';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'instance'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
const HISTORY_ITEM_COUNT = 5;
|
||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
|
||||
const wrapLabel = (label: string) => ({ label });
|
||||
|
||||
export function addHistoryMetadata(item: CompletionItem, history: any[]): CompletionItem {
|
||||
const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
||||
const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label);
|
||||
const count = historyForItem.length;
|
||||
const recent = historyForItem[0];
|
||||
let hint = `Queried ${count} times in the last 24h.`;
|
||||
if (recent) {
|
||||
const lastQueried = moment(recent.ts).fromNow();
|
||||
hint = `${hint} Last queried ${lastQueried}.`;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
documentation: hint,
|
||||
};
|
||||
}
|
||||
|
||||
export default class LoggingLanguageProvider extends LanguageProvider {
|
||||
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
||||
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||
logLabelOptions: any[];
|
||||
started: boolean;
|
||||
|
||||
constructor(datasource: any, initialValues?: any) {
|
||||
super();
|
||||
|
||||
this.datasource = datasource;
|
||||
this.labelKeys = {};
|
||||
this.labelValues = {};
|
||||
this.started = false;
|
||||
|
||||
Object.assign(this, initialValues);
|
||||
}
|
||||
// Strip syntax chars
|
||||
cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
||||
|
||||
getSyntax() {
|
||||
return PromqlSyntax;
|
||||
}
|
||||
|
||||
request = url => {
|
||||
return this.datasource.metadataRequest(url);
|
||||
};
|
||||
|
||||
start = () => {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
return Promise.all([this.fetchLogLabels()]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
// Keep this DOM-free for testing
|
||||
provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput {
|
||||
// Syntax spans have 3 classes by default. More indicate a recognized token
|
||||
const tokenRecognized = wrapperClasses.length > 3;
|
||||
// Determine candidates by CSS context
|
||||
if (_.includes(wrapperClasses, 'context-labels')) {
|
||||
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
|
||||
return this.getLabelCompletionItems.apply(this, arguments);
|
||||
} else if (
|
||||
// Show default suggestions in a couple of scenarios
|
||||
(prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token
|
||||
(prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace
|
||||
text.match(/[+\-*/^%]/) // Anything after binary operator
|
||||
) {
|
||||
return this.getEmptyCompletionItems(context || {});
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: [],
|
||||
};
|
||||
}
|
||||
|
||||
getEmptyCompletionItems(context: any): TypeaheadOutput {
|
||||
const { history } = context;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
|
||||
if (history && history.length > 0) {
|
||||
const historyItems = _.chain(history)
|
||||
.uniqBy('query')
|
||||
.take(HISTORY_ITEM_COUNT)
|
||||
.map(h => h.query)
|
||||
.map(wrapLabel)
|
||||
.map(item => addHistoryMetadata(item, history))
|
||||
.value();
|
||||
|
||||
suggestions.push({
|
||||
prefixMatch: true,
|
||||
skipSort: true,
|
||||
label: 'History',
|
||||
items: historyItems,
|
||||
});
|
||||
}
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
|
||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
||||
let context: string;
|
||||
const suggestions: CompletionItemGroup[] = [];
|
||||
const line = value.anchorBlock.getText();
|
||||
const cursorOffset: number = value.anchorOffset;
|
||||
|
||||
// Get normalized selector
|
||||
let selector;
|
||||
let parsedSelector;
|
||||
try {
|
||||
parsedSelector = parseSelector(line, cursorOffset);
|
||||
selector = parsedSelector.selector;
|
||||
} catch {
|
||||
selector = EMPTY_SELECTOR;
|
||||
}
|
||||
const containsMetric = selector.indexOf('__name__=') > -1;
|
||||
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
||||
|
||||
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
|
||||
// Label values
|
||||
if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) {
|
||||
const labelValues = this.labelValues[selector][labelKey];
|
||||
context = 'context-label-values';
|
||||
suggestions.push({
|
||||
label: `Label values for "${labelKey}"`,
|
||||
items: labelValues.map(wrapLabel),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Label keys
|
||||
const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
|
||||
if (labelKeys) {
|
||||
const possibleKeys = _.difference(labelKeys, existingKeys);
|
||||
if (possibleKeys.length > 0) {
|
||||
context = 'context-labels';
|
||||
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { context, suggestions };
|
||||
}
|
||||
|
||||
async fetchLogLabels() {
|
||||
const url = '/api/prom/label';
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const labelKeys = body.data.slice().sort();
|
||||
const labelKeysBySelector = {
|
||||
...this.labelKeys,
|
||||
[EMPTY_SELECTOR]: labelKeys,
|
||||
};
|
||||
const labelValuesByKey = {};
|
||||
this.logLabelOptions = [];
|
||||
for (const key of labelKeys) {
|
||||
const valuesUrl = `/api/prom/label/${key}/values`;
|
||||
const res = await this.request(valuesUrl);
|
||||
const body = await (res.data || res.json());
|
||||
const values = body.data.slice().sort();
|
||||
labelValuesByKey[key] = values;
|
||||
this.logLabelOptions.push({
|
||||
label: key,
|
||||
value: key,
|
||||
children: values.map(value => ({ label: value, value })),
|
||||
});
|
||||
}
|
||||
this.labelValues = { [EMPTY_SELECTOR]: labelValuesByKey };
|
||||
this.labelKeys = labelKeysBySelector;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLabelValues(key: string) {
|
||||
const url = `/api/prom/label/${key}/values`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const exisingValues = this.labelValues[EMPTY_SELECTOR];
|
||||
const values = {
|
||||
...exisingValues,
|
||||
[key]: body.data,
|
||||
};
|
||||
this.labelValues = {
|
||||
...this.labelValues,
|
||||
[EMPTY_SELECTOR]: values,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,15 @@
|
||||
import Datasource from './datasource';
|
||||
|
||||
import LoggingStartPage from './components/LoggingStartPage';
|
||||
import LoggingQueryField from './components/LoggingQueryField';
|
||||
|
||||
export class LoggingConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
}
|
||||
|
||||
export { Datasource, LoggingConfigCtrl as ConfigCtrl };
|
||||
export {
|
||||
Datasource,
|
||||
LoggingConfigCtrl as ConfigCtrl,
|
||||
LoggingQueryField as ExploreQueryField,
|
||||
LoggingStartPage as ExploreStartPage,
|
||||
};
|
||||
|
@ -94,11 +94,9 @@ interface PromQueryFieldProps {
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: string, override?: boolean) => void;
|
||||
supportsLogs?: boolean; // To be removed after Logging gets its own query field
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
logLabelOptions: any[];
|
||||
metricsOptions: any[];
|
||||
metricsByPrefix: CascaderOption[];
|
||||
syntaxLoaded: boolean;
|
||||
@ -125,7 +123,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
];
|
||||
|
||||
this.state = {
|
||||
logLabelOptions: [],
|
||||
metricsByPrefix: [],
|
||||
metricsOptions: [],
|
||||
syntaxLoaded: false,
|
||||
@ -138,23 +135,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
}
|
||||
}
|
||||
|
||||
onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||
let query;
|
||||
if (selectedOptions.length === 1) {
|
||||
if (selectedOptions[0].children.length === 0) {
|
||||
query = selectedOptions[0].value;
|
||||
} else {
|
||||
// Ignore click on group
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const key = selectedOptions[0].value;
|
||||
const value = selectedOptions[1].value;
|
||||
query = `{${key}="${value}"}`;
|
||||
}
|
||||
this.onChangeQuery(query, true);
|
||||
};
|
||||
|
||||
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||
let query;
|
||||
if (selectedOptions.length === 1) {
|
||||
@ -239,22 +219,16 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error, hint, initialQuery, supportsLogs } = this.props;
|
||||
const { logLabelOptions, metricsOptions, syntaxLoaded } = this.state;
|
||||
const { error, hint, initialQuery } = this.props;
|
||||
const { metricsOptions, syntaxLoaded } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
|
||||
return (
|
||||
<div className="prom-query-field">
|
||||
<div className="prom-query-field-tools">
|
||||
{supportsLogs ? (
|
||||
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels}>
|
||||
<button className="btn navbar-button navbar-button--tight">Log labels</button>
|
||||
</Cascader>
|
||||
) : (
|
||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
||||
</Cascader>
|
||||
)}
|
||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
||||
</Cascader>
|
||||
</div>
|
||||
<div className="prom-query-field-wrapper">
|
||||
<TypeaheadField
|
||||
|
@ -46,8 +46,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
||||
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
||||
metrics?: string[];
|
||||
logLabelOptions: any[];
|
||||
supportsLogs?: boolean;
|
||||
started: boolean;
|
||||
|
||||
constructor(datasource: any, initialValues?: any) {
|
||||
@ -58,7 +56,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
this.labelKeys = {};
|
||||
this.labelValues = {};
|
||||
this.metrics = [];
|
||||
this.supportsLogs = false;
|
||||
this.started = false;
|
||||
|
||||
Object.assign(this, initialValues);
|
||||
@ -243,8 +240,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
|
||||
// Query labels for selector
|
||||
// Temporarily add skip for logging
|
||||
if (selector && !this.labelValues[selector] && !this.supportsLogs) {
|
||||
if (selector && !this.labelValues[selector]) {
|
||||
if (selector === EMPTY_SELECTOR) {
|
||||
// Query label values for default labels
|
||||
refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
|
||||
@ -275,38 +271,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily here while reusing this field for logging
|
||||
async fetchLogLabels() {
|
||||
const url = '/api/prom/label';
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
const body = await (res.data || res.json());
|
||||
const labelKeys = body.data.slice().sort();
|
||||
const labelKeysBySelector = {
|
||||
...this.labelKeys,
|
||||
[EMPTY_SELECTOR]: labelKeys,
|
||||
};
|
||||
const labelValuesByKey = {};
|
||||
this.logLabelOptions = [];
|
||||
for (const key of labelKeys) {
|
||||
const valuesUrl = `/api/prom/label/${key}/values`;
|
||||
const res = await this.request(valuesUrl);
|
||||
const body = await (res.data || res.json());
|
||||
const values = body.data.slice().sort();
|
||||
labelValuesByKey[key] = values;
|
||||
this.logLabelOptions.push({
|
||||
label: key,
|
||||
value: key,
|
||||
children: values.map(value => ({ label: value, value })),
|
||||
});
|
||||
}
|
||||
this.labelValues = { [EMPTY_SELECTOR]: labelValuesByKey };
|
||||
this.labelKeys = labelKeysBySelector;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchLabelValues(key: string) {
|
||||
const url = `/api/v1/label/${key}/values`;
|
||||
try {
|
||||
|
@ -87,7 +87,7 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.explore-graph__loader {
|
||||
.explore-panel__loader {
|
||||
height: 2px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@ -95,7 +95,7 @@
|
||||
margin: $panel-margin / 2;
|
||||
}
|
||||
|
||||
.explore-graph__loader:after {
|
||||
.explore-panel__loader:after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
width: 25%;
|
||||
@ -219,7 +219,13 @@
|
||||
}
|
||||
|
||||
.logs-row-match-highlight {
|
||||
background-color: lighten($blue, 20%);
|
||||
// Undoing mark styling
|
||||
background: inherit;
|
||||
padding: inherit;
|
||||
|
||||
color: $typeahead-selected-color;
|
||||
border-bottom: 1px solid $typeahead-selected-color;
|
||||
background-color: lighten($typeahead-selected-color, 60%);
|
||||
}
|
||||
|
||||
.logs-row-level {
|
||||
|
Loading…
Reference in New Issue
Block a user