Procházet zdrojové kódy

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
David Kaltschmidt před 7 roky
rodič
revize
c92f5462fe

+ 14 - 0
public/app/core/logs_model.ts

@@ -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 };
+}

+ 22 - 19
public/app/features/explore/Explore.tsx

@@ -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 && (

+ 1 - 1
public/app/features/explore/Graph.tsx

@@ -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 && (

+ 21 - 25
public/app/features/explore/Logs.tsx

@@ -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>
-        ) : null}
-        {!hasData ? (
-          <div className="panel-container">
-            Enter a query like <code>{EXAMPLE_QUERY}</code>
+        <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}
+          {!loading && !hasData && 'No data was returned.'}
+        </div>
       </div>
     );
   }

+ 1 - 4
public/app/features/explore/QueryRows.tsx

@@ -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">

+ 29 - 0
public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx

@@ -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>
+);

+ 205 - 0
public/app/plugins/datasource/logging/components/LoggingQueryField.tsx

@@ -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;

+ 60 - 0
public/app/plugins/datasource/logging/components/LoggingStartPage.tsx

@@ -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>
+    );
+  }
+}

+ 6 - 1
public/app/plugins/datasource/logging/datasource.ts

@@ -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 - 0
public/app/plugins/datasource/logging/language_provider.ts

@@ -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);
+    }
+  }
+}

+ 9 - 1
public/app/plugins/datasource/logging/module.ts

@@ -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,
+};

+ 5 - 31
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx

@@ -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

+ 1 - 37
public/app/plugins/datasource/prometheus/language_provider.ts

@@ -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 {

+ 9 - 3
public/sass/pages/_explore.scss

@@ -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 {