Browse Source

Merge pull request #14414 from grafana/davkal/explore-logging-search

Explore: Split logging query into selector and search
David 7 years ago
parent
commit
b2401e37fa

+ 9 - 2
public/app/features/explore/QueryField.tsx

@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
 import { Change, Value } from 'slate';
 import { Editor } from 'slate-react';
 import Plain from 'slate-plain-serializer';
+import classnames from 'classnames';
 
 import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore';
 
@@ -30,6 +31,7 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
 export interface QueryFieldProps {
   additionalPlugins?: any[];
   cleanText?: (text: string) => string;
+  disabled?: boolean;
   initialQuery: string | null;
   onBlur?: () => void;
   onFocus?: () => void;
@@ -78,7 +80,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
     this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || '');
 
     // Base plugins
-    this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p);
+    this.plugins = [ClearPlugin(), NewlinePlugin(), ...(props.additionalPlugins || [])].filter(p => p);
 
     this.state = {
       suggestions: [],
@@ -440,12 +442,17 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
   };
 
   render() {
+    const { disabled } = this.props;
+    const wrapperClassName = classnames('slate-query-field__wrapper', {
+      'slate-query-field__wrapper--disabled': disabled,
+    });
     return (
-      <div className="slate-query-field-wrapper">
+      <div className={wrapperClassName}>
         <div className="slate-query-field">
           {this.renderMenu()}
           <Editor
             autoCorrect={false}
+            readOnly={this.props.disabled}
             onBlur={this.handleBlur}
             onKeyDown={this.onKeyDown}
             onChange={this.onChange}

+ 19 - 8
public/app/plugins/datasource/loki/components/LokiCheatSheet.tsx

@@ -1,15 +1,24 @@
 import React from 'react';
 
 const CHEAT_SHEET_ITEMS = [
+  {
+    title: 'See your logs',
+    label: 'Start by selecting a log stream from the Log Labels selector.',
+  },
   {
     title: 'Logs From a Job',
     expression: '{job="default/prometheus"}',
     label: 'Returns all log lines emitted by instances of this job.',
   },
+  {
+    title: 'Combine Stream Selectors',
+    expression: '{app="cassandra",namespace="prod"}',
+    label: 'Returns all log lines from streams that have both labels.',
+  },
   {
     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.',
+    expression: '{app="cassandra"} (duration|latency)\\s*(=|is|of)\\s*[\\d\\.]+',
+    label: 'The right search field takes a regular expression to search for.',
   },
 ];
 
@@ -19,12 +28,14 @@ export default (props: any) => (
     {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.onClickExample({ refId: '1', expr: item.expression })}
-        >
-          <code>{item.expression}</code>
-        </div>
+        {item.expression && (
+          <div
+            className="cheat-sheet-item__expression"
+            onClick={e => props.onClickExample({ refId: '1', expr: item.expression })}
+          >
+            <code>{item.expression}</code>
+          </div>
+        )}
         <div className="cheat-sheet-item__label">{item.label}</div>
       </div>
     ))}

+ 50 - 3
public/app/plugins/datasource/loki/components/LokiQueryField.tsx

@@ -12,9 +12,15 @@ import BracesPlugin from 'app/features/explore/slate-plugins/braces';
 import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
 import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
 import { DataQuery } from 'app/types';
+import { parseQuery, formatQuery } from '../query_utils';
 
 const PRISM_SYNTAX = 'promql';
 
+const SEARCH_FIELD_STYLES = {
+  width: '66%',
+  marginLeft: 3,
+};
+
 export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
   // Modify suggestion based on context
   switch (typeaheadContext) {
@@ -67,7 +73,10 @@ interface LokiQueryFieldState {
 
 class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
   plugins: any[];
+  pluginsSearch: any[];
   languageProvider: any;
+  modifiedSearch: string;
+  modifiedQuery: string;
 
   constructor(props: LokiQueryFieldProps, context) {
     super(props, context);
@@ -85,6 +94,8 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
       }),
     ];
 
+    this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })];
+
     this.state = {
       logLabelOptions: [],
       syntaxLoaded: false,
@@ -134,12 +145,35 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
   };
 
   onChangeQuery = (value: string, override?: boolean) => {
+    const enableSearchField = !this.modifiedQuery && value;
+    this.modifiedQuery = value;
     // Send text change to parent
     const { initialQuery, onQueryChange } = this.props;
     if (onQueryChange) {
+      const search = this.modifiedSearch || parseQuery(initialQuery.expr).regexp;
+      const expr = formatQuery(value, search);
       const query = {
         ...initialQuery,
-        expr: value,
+        expr,
+      };
+      onQueryChange(query, override);
+    }
+    // Enable the search field if we have a selector query
+    if (enableSearchField) {
+      this.forceUpdate();
+    }
+  };
+
+  onChangeSearch = (value: string, override?: boolean) => {
+    this.modifiedSearch = value;
+    // Send text change to parent
+    const { initialQuery, onQueryChange } = this.props;
+    if (onQueryChange) {
+      const selector = this.modifiedQuery || parseQuery(initialQuery.expr).query;
+      const expr = formatQuery(selector, value);
+      const query = {
+        ...initialQuery,
+        expr,
       };
       onQueryChange(query, override);
     }
@@ -190,6 +224,9 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
     const { logLabelOptions, syntaxLoaded } = this.state;
     const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
     const chooserText = syntaxLoaded ? 'Log labels' : 'Loading labels...';
+    const parsedInitialQuery = parseQuery(initialQuery.expr);
+    // Disable search field to make clear that we need a selector query first
+    const searchDisabled = !parsedInitialQuery.query && !this.modifiedQuery;
 
     return (
       <div className="prom-query-field">
@@ -204,11 +241,11 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
           <QueryField
             additionalPlugins={this.plugins}
             cleanText={cleanText}
-            initialQuery={initialQuery.expr}
+            initialQuery={parsedInitialQuery.query}
             onTypeahead={this.onTypeahead}
             onWillApplySuggestion={willApplySuggestion}
             onValueChanged={this.onChangeQuery}
-            placeholder="Enter a Loki Log query"
+            placeholder="Start with a Loki stream selector"
             portalOrigin="loki"
             syntaxLoaded={syntaxLoaded}
           />
@@ -224,6 +261,16 @@ class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryF
             </div>
           ) : null}
         </div>
+        <div className="prom-query-field-wrapper" style={SEARCH_FIELD_STYLES}>
+          <QueryField
+            additionalPlugins={this.pluginsSearch}
+            disabled={searchDisabled}
+            initialQuery={parsedInitialQuery.regexp}
+            onValueChanged={this.onChangeSearch}
+            placeholder="Search by regular expression"
+            portalOrigin="loki"
+          />
+        </div>
       </div>
     );
   }

+ 1 - 0
public/app/plugins/datasource/loki/query_utils.ts

@@ -1,5 +1,6 @@
 const selectorRegexp = /(?:^|\s){[^{]*}/g;
 export function parseQuery(input: string) {
+  input = input || '';
   const match = input.match(selectorRegexp);
   let query = '';
   let regexp = input;

+ 5 - 1
public/sass/components/_slate_editor.scss

@@ -5,7 +5,7 @@
   word-break: break-word;
 }
 
-.slate-query-field-wrapper {
+.slate-query-field__wrapper {
   position: relative;
   display: inline-block;
   padding: 6px 7px 4px;
@@ -20,6 +20,10 @@
   transition: all 0.3s;
 }
 
+.slate-query-field__wrapper--disabled {
+  background-color: inherit;
+}
+
 .slate-typeahead {
   .typeahead {
     position: absolute;