From c8f2148f7504b393b8f9b32fe19e7f519fa66793 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Wed, 24 Aug 2022 17:57:59 +0100 Subject: [PATCH] Tempo: Basic PoC of the upcoming TraceQL editor in Explore (#54028) * First working version of the TraceQL editor with syntax highlighting and autocomplete * Add feature flag around the new editor option * Fix tests and cleanup * Fix misspelling --- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 5 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../tempo/QueryEditor/QueryField.tsx | 15 +- .../plugins/datasource/tempo/datasource.ts | 5 +- .../datasource/tempo/language_provider.ts | 20 +- .../datasource/tempo/traceql/QueryEditor.tsx | 25 ++ .../tempo/traceql/TraceQLEditor.tsx | 126 ++++++++++ .../tempo/traceql/autocomplete.test.ts | 147 +++++++++++ .../datasource/tempo/traceql/autocomplete.ts | 235 ++++++++++++++++++ .../datasource/tempo/traceql/traceql.ts | 106 ++++++++ public/app/plugins/datasource/tempo/types.ts | 7 + 12 files changed, 687 insertions(+), 9 deletions(-) create mode 100644 public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx create mode 100644 public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx create mode 100644 public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts create mode 100644 public/app/plugins/datasource/tempo/traceql/autocomplete.ts create mode 100644 public/app/plugins/datasource/tempo/traceql/traceql.ts create mode 100644 public/app/plugins/datasource/tempo/types.ts diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e890d70144b..557693e9f8c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -63,4 +63,5 @@ export interface FeatureToggles { internationalization?: boolean; topnav?: boolean; customBranding?: boolean; + traceqlEditor?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 473e3448f20..b05752ba9be 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -262,5 +262,10 @@ var ( Description: "Replaces whitelabeling with the new custom branding feature", State: FeatureStateAlpha, }, + { + Name: "traceqlEditor", + Description: "Show the TraceQL editor in the explore page", + State: FeatureStateAlpha, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 140a58863b9..10dc8cb0dac 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -194,4 +194,8 @@ const ( // FlagCustomBranding // Replaces whitelabeling with the new custom branding feature FlagCustomBranding = "customBranding" + + // FlagTraceqlEditor + // Show the TraceQL editor in the explore page + FlagTraceqlEditor = "traceqlEditor" ) diff --git a/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx index 5c45b7419a2..f9122142f48 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx @@ -3,7 +3,7 @@ import React from 'react'; import useAsync from 'react-use/lib/useAsync'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { FileDropzone, InlineField, @@ -19,6 +19,7 @@ import { LokiQueryField } from '../../loki/components/LokiQueryField'; import { LokiDatasource } from '../../loki/datasource'; import { LokiQuery } from '../../loki/types'; import { TempoDatasource, TempoQuery, TempoQueryType } from '../datasource'; +import { QueryEditor } from '../traceql/QueryEditor'; import NativeSearch from './NativeSearch'; import { ServiceGraphSection } from './ServiceGraphSection'; @@ -95,6 +96,10 @@ class TempoQueryFieldComponent extends React.PureComponent { } } + if (config.featureToggles.traceqlEditor) { + queryTypeOptions.push({ value: 'traceql', label: 'TraceQL' }); + } + return ( <> @@ -173,6 +178,14 @@ class TempoQueryFieldComponent extends React.PureComponent { {query.queryType === 'serviceMap' && ( )} + {query.queryType === 'traceql' && ( + + )} ); } diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 1d856740147..5e81fef6f2b 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -45,6 +45,7 @@ import { errorRateMetric, defaultTableFilter, } from './graphTransform'; +import TempoLanguageProvider from './language_provider'; import { transformTrace, transformTraceList, @@ -53,7 +54,7 @@ import { } from './resultTransformer'; // search = Loki search, nativeSearch = Tempo search for backwards compatibility -export type TempoQueryType = 'search' | 'traceId' | 'serviceMap' | 'upload' | 'nativeSearch' | 'clear'; +export type TempoQueryType = 'traceql' | 'search' | 'traceId' | 'serviceMap' | 'upload' | 'nativeSearch' | 'clear'; export interface TempoJsonData extends DataSourceJsonData { tracesToLogs?: TraceToLogsOptions; @@ -111,6 +112,7 @@ export class TempoDatasource extends DataSourceWithBackend, @@ -122,6 +124,7 @@ export class TempoDatasource extends DataSourceWithBackend): Observable { diff --git a/public/app/plugins/datasource/tempo/language_provider.ts b/public/app/plugins/datasource/tempo/language_provider.ts index 27c13e5516e..1ae59919b41 100644 --- a/public/app/plugins/datasource/tempo/language_provider.ts +++ b/public/app/plugins/datasource/tempo/language_provider.ts @@ -1,6 +1,6 @@ import { Value } from 'slate'; -import { HistoryItem, LanguageProvider, SelectableValue } from '@grafana/data'; +import { LanguageProvider, SelectableValue } from '@grafana/data'; import { CompletionItemGroup, TypeaheadInput, TypeaheadOutput } from '@grafana/ui'; import { TempoDatasource } from './datasource'; @@ -21,8 +21,13 @@ export default class TempoLanguageProvider extends LanguageProvider { }; start = async () => { - await this.fetchTags(); - return []; + if (!this.startTask) { + this.startTask = this.fetchTags().then(() => { + return []; + }); + } + + return this.startTask; }; async fetchTags() { @@ -30,10 +35,11 @@ export default class TempoLanguageProvider extends LanguageProvider { this.tags = response.tagNames; } - provideCompletionItems = async ( - { prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput, - context: { history: Array> } = { history: [] } - ): Promise => { + getTags = () => { + return this.tags; + }; + + provideCompletionItems = async ({ text, value }: TypeaheadInput): Promise => { const emptyResult: TypeaheadOutput = { suggestions: [] }; if (!value) { diff --git a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx new file mode 100644 index 00000000000..054502cc9d6 --- /dev/null +++ b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx @@ -0,0 +1,25 @@ +import { defaults } from 'lodash'; +import React from 'react'; + +import { QueryEditorProps } from '@grafana/data'; + +import { TempoDatasource, TempoQuery } from '../datasource'; +import { defaultQuery, MyDataSourceOptions } from '../types'; + +import { TraceQLEditor } from './TraceQLEditor'; + +type Props = QueryEditorProps; + +export function QueryEditor(props: Props) { + function onEditorChange(value: string) { + props.onChange({ ...props.query, query: value }); + } + + let query = defaults(props.query, defaultQuery); + + return ( +
+ +
+ ); +} diff --git a/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx new file mode 100644 index 00000000000..fa8b2594a74 --- /dev/null +++ b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx @@ -0,0 +1,126 @@ +import { css } from '@emotion/css'; +import type { languages } from 'monaco-editor'; +import React, { useEffect, useRef } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { CodeEditor, Monaco, useStyles2, monacoTypes } from '@grafana/ui'; + +import { createErrorNotification } from '../../../../core/copy/appNotification'; +import { notifyApp } from '../../../../core/reducers/appNotification'; +import { dispatch } from '../../../../store/store'; +import { TempoDatasource } from '../datasource'; + +import { CompletionProvider } from './autocomplete'; +import { languageDefinition } from './traceql'; + +interface Props { + value: string; + onChange: (val: string) => void; + datasource: TempoDatasource; +} + +export function TraceQLEditor(props: Props) { + const setupAutocompleteFn = useAutocomplete(props.datasource); + const styles = useStyles2(getStyles); + return ( + { + setupAutocompleteFn(editor, monaco); + }} + /> + ); +} + +/** + * Hook that returns function that will set up monaco autocomplete for the label selector + * @param datasource + */ +function useAutocomplete(datasource: TempoDatasource) { + // We need the provider ref so we can pass it the label/values data later. This is because we run the call for the + // values here but there is additional setup needed for the provider later on. We could run the getSeries() in the + // returned function but that is run after the monaco is mounted so would delay the request a bit when it does not + // need to. + const providerRef = useRef( + new CompletionProvider({ languageProvider: datasource.languageProvider }) + ); + + useEffect(() => { + const fetchTags = async () => { + try { + await datasource.languageProvider.start(); + const tags = datasource.languageProvider.getTags(); + + if (tags) { + providerRef.current.setTags(tags); + } + } catch (error) { + if (error instanceof Error) { + dispatch(notifyApp(createErrorNotification('Error', error))); + } + } + }; + fetchTags(); + }, [datasource]); + + const autocompleteDisposeFun = useRef<(() => void) | null>(null); + useEffect(() => { + // when we unmount, we unregister the autocomplete-function, if it was registered + return () => { + autocompleteDisposeFun.current?.(); + }; + }, []); + + // This should be run in monaco onEditorDidMount + return (editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => { + providerRef.current.editor = editor; + providerRef.current.monaco = monaco; + + const { dispose } = monaco.languages.registerCompletionItemProvider(langId, providerRef.current); + autocompleteDisposeFun.current = dispose; + }; +} + +// we must only run the setup code once +let traceqlSetupDone = false; +const langId = 'traceql'; + +function ensureTraceQL(monaco: Monaco) { + if (!traceqlSetupDone) { + traceqlSetupDone = true; + const { aliases, extensions, mimetypes, def } = languageDefinition; + monaco.languages.register({ id: langId, aliases, extensions, mimetypes }); + monaco.languages.setMonarchTokensProvider(langId, def.language as languages.IMonarchLanguage); + monaco.languages.setLanguageConfiguration(langId, def.languageConfiguration as languages.LanguageConfiguration); + } +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + queryField: css` + border-radius: ${theme.shape.borderRadius()}; + border: 1px solid ${theme.components.input.borderColor}; + flex: 1; + `, + }; +}; diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts new file mode 100644 index 00000000000..04a1d7dabe7 --- /dev/null +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts @@ -0,0 +1,147 @@ +import { DataSourceInstanceSettings, PluginType } from '@grafana/data/src'; +import { monacoTypes } from '@grafana/ui'; + +import { TempoDatasource, TempoJsonData } from '../datasource'; +import TempoLanguageProvider from '../language_provider'; + +import { CompletionProvider } from './autocomplete'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), +})); + +describe('CompletionProvider', () => { + it('suggests tags', async () => { + const { provider, model } = setup('{}', 1, defaultTags); + const result = await provider.provideCompletionItems(model as any, {} as any); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ + expect.objectContaining({ label: 'foo', insertText: 'foo' }), + expect.objectContaining({ label: 'bar', insertText: 'bar' }), + ]); + }); + + it('suggests tag names with quotes', async () => { + const { provider, model } = setup('{foo=}', 6, defaultTags); + + jest.spyOn(provider.languageProvider, 'getOptions').mockImplementation( + () => + new Promise((resolve) => { + resolve([ + { + value: 'foobar', + label: 'foobar', + }, + ]); + }) + ); + + const result = await provider.provideCompletionItems(model as any, {} as any); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ + expect.objectContaining({ label: 'foobar', insertText: '"foobar"' }), + ]); + }); + + it('suggests tag names without quotes', async () => { + const { provider, model } = setup('{foo="}', 7, defaultTags); + + jest.spyOn(provider.languageProvider, 'getOptions').mockImplementation( + () => + new Promise((resolve) => { + resolve([ + { + value: 'foobar', + label: 'foobar', + }, + ]); + }) + ); + + const result = await provider.provideCompletionItems(model as any, {} as any); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ + expect.objectContaining({ label: 'foobar', insertText: 'foobar' }), + ]); + }); + + it('suggests nothing without tags', async () => { + const { provider, model } = setup('{foo="}', 7, []); + const result = await provider.provideCompletionItems(model as any, {} as any); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([]); + }); + + it('suggests tags on empty input', async () => { + const { provider, model } = setup('', 0, defaultTags); + const result = await provider.provideCompletionItems(model as any, {} as any); + expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ + expect.objectContaining({ label: 'foo', insertText: '{foo="' }), + expect.objectContaining({ label: 'bar', insertText: '{bar="' }), + ]); + }); +}); + +const defaultTags = ['foo', 'bar']; + +function setup(value: string, offset: number, tags?: string[]) { + const ds = new TempoDatasource(defaultSettings); + const provider = new CompletionProvider({ languageProvider: new TempoLanguageProvider(ds) }); + if (tags) { + provider.setTags(tags); + } + const model = makeModel(value, offset); + provider.monaco = { + Range: { + fromPositions() { + return null; + }, + }, + languages: { + CompletionItemKind: { + Enum: 1, + EnumMember: 2, + }, + }, + } as any; + provider.editor = { + getModel() { + return model; + }, + } as any; + + return { provider, model }; +} + +function makeModel(value: string, offset: number) { + return { + id: 'test_monaco', + getWordAtPosition() { + return null; + }, + getOffsetAt() { + return offset; + }, + getValue() { + return value; + }, + }; +} + +const defaultSettings: DataSourceInstanceSettings = { + id: 0, + uid: 'gdev-tempo', + type: 'tracing', + name: 'tempo', + access: 'proxy', + meta: { + id: 'tempo', + name: 'tempo', + type: PluginType.datasource, + info: {} as any, + module: '', + baseUrl: '', + }, + jsonData: { + nodeGraph: { + enabled: true, + }, + }, +}; diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts new file mode 100644 index 00000000000..902272f8eca --- /dev/null +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts @@ -0,0 +1,235 @@ +import type { Monaco, monacoTypes } from '@grafana/ui'; + +import TempoLanguageProvider from '../language_provider'; + +interface Props { + languageProvider: TempoLanguageProvider; +} + +/** + * Class that implements CompletionItemProvider interface and allows us to provide suggestion for the Monaco + * autocomplete system. + */ +export class CompletionProvider implements monacoTypes.languages.CompletionItemProvider { + languageProvider: TempoLanguageProvider; + + constructor(props: Props) { + this.languageProvider = props.languageProvider; + } + + triggerCharacters = ['{', ',', '[', '(', '=', '~', ' ', '"']; + + // We set these directly and ae required for the provider to function. + monaco: Monaco | undefined; + editor: monacoTypes.editor.IStandaloneCodeEditor | undefined; + + private tags: { [tag: string]: Set } = {}; + + provideCompletionItems( + model: monacoTypes.editor.ITextModel, + position: monacoTypes.Position + ): monacoTypes.languages.ProviderResult { + // Should not happen, this should not be called before it is initialized + if (!(this.monaco && this.editor)) { + throw new Error('provideCompletionItems called before CompletionProvider was initialized'); + } + + // if the model-id does not match, then this call is from a different editor-instance, + // not "our instance", so return nothing + if (this.editor.getModel()?.id !== model.id) { + return { suggestions: [] }; + } + + const { range, offset } = getRangeAndOffset(this.monaco, model, position); + const situation = getSituation(model.getValue(), offset); + const completionItems = this.getCompletions(situation); + + return completionItems.then((items) => { + // monaco by-default alphabetically orders the items. + // to stop it, we use a number-as-string sortkey, + // so that monaco keeps the order we use + const maxIndexDigits = items.length.toString().length; + const suggestions: monacoTypes.languages.CompletionItem[] = items.map((item, index) => ({ + kind: getMonacoCompletionItemKind(item.type, this.monaco!), + label: item.label, + insertText: item.insertText, + sortText: index.toString().padStart(maxIndexDigits, '0'), // to force the order we have + range, + })); + return { suggestions }; + }); + } + + /** + * We expect the tags list data directly from the request and assign it an empty set here. + */ + setTags(tags: string[]) { + tags.forEach((t) => (this.tags[t] = new Set())); + } + + /** + * Get suggestion based on the situation we are in like whether we should suggest tag names or values. + * @param situation + * @private + */ + private async getCompletions(situation: Situation): Promise { + if (!Object.keys(this.tags).length) { + return []; + } + switch (situation.type) { + // Not really sure what would make sense to suggest in this case so just leave it + case 'UNKNOWN': { + return []; + } + case 'EMPTY': { + return Object.keys(this.tags).map((key) => { + return { + label: key, + insertText: `{${key}="`, + type: 'TAG_NAME', + }; + }); + } + case 'IN_TAG_NAME': + return Object.keys(this.tags).map((key) => { + return { + label: key, + insertText: key, + type: 'TAG_NAME', + }; + }); + case 'IN_TAG_VALUE': + return await this.languageProvider.getOptions(situation.tagName).then((res) => { + const items: Completion[] = []; + res.forEach((val) => { + if (val?.label) { + items.push({ + label: val.label, + insertText: situation.betweenQuotes ? val.label : `"${val.label}"`, + type: 'TAG_VALUE', + }); + } + }); + return items; + }); + default: + throw new Error(`Unexpected situation ${situation}`); + } + } +} + +/** + * Get item kind which is used for icon next to the suggestion. + * @param type + * @param monaco + */ +function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind { + switch (type) { + case 'TAG_NAME': + return monaco.languages.CompletionItemKind.Enum; + case 'TAG_VALUE': + return monaco.languages.CompletionItemKind.EnumMember; + default: + throw new Error(`Unexpected CompletionType: ${type}`); + } +} + +export type CompletionType = 'TAG_NAME' | 'TAG_VALUE'; +type Completion = { + type: CompletionType; + label: string; + insertText: string; +}; + +export type Tag = { + name: string; + value: string; +}; + +export type Situation = + | { + type: 'UNKNOWN'; + } + | { + type: 'EMPTY'; + } + | { + type: 'IN_TAG_NAME'; + otherTags: Tag[]; + } + | { + type: 'IN_TAG_VALUE'; + tagName: string; + betweenQuotes: boolean; + otherTags: Tag[]; + }; + +/** + * Figure out where is the cursor and what kind of suggestions are appropriate. + * As currently TraceQL handles just a simple {foo="bar", baz="zyx"} kind of values we can do with simple regex to figure + * out where we are with the cursor. + * @param text + * @param offset + */ +function getSituation(text: string, offset: number): Situation { + if (text === '') { + return { + type: 'EMPTY', + }; + } + + // Get all the tags so far in the query so we can do some more filtering. + const matches = text.matchAll(/(\w+)="(\w+)"/g); + const existingTags = Array.from(matches).reduce((acc, match) => { + const [_, name, value] = match[1]; + acc.push({ name, value }); + return acc; + }, [] as Tag[]); + + // Check if we are editing a tag value right now. If so also get name of the tag + const matchTagValue = text.substring(0, offset).match(/([\w.]+)=("?)[^"]*$/); + if (matchTagValue) { + return { + type: 'IN_TAG_VALUE', + tagName: matchTagValue[1], + betweenQuotes: !!matchTagValue[2], + otherTags: existingTags, + }; + } + + // Check if we are editing a tag name + const matchTagName = text.substring(0, offset).match(/[{,]\s*[^"]*$/); + if (matchTagName) { + return { + type: 'IN_TAG_NAME', + otherTags: existingTags, + }; + } + + // Will happen only if user writes something that isn't really a tag selector + return { + type: 'UNKNOWN', + }; +} + +function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel, position: monacoTypes.Position) { + const word = model.getWordAtPosition(position); + const range = + word != null + ? monaco.Range.lift({ + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }) + : monaco.Range.fromPositions(position); + + // documentation says `position` will be "adjusted" in `getOffsetAt` so we clone it here just for sure. + const positionClone = { + column: position.column, + lineNumber: position.lineNumber, + }; + + const offset = model.getOffsetAt(positionClone); + return { offset, range }; +} diff --git a/public/app/plugins/datasource/tempo/traceql/traceql.ts b/public/app/plugins/datasource/tempo/traceql/traceql.ts new file mode 100644 index 00000000000..984d8e7edd3 --- /dev/null +++ b/public/app/plugins/datasource/tempo/traceql/traceql.ts @@ -0,0 +1,106 @@ +export const languageConfiguration = { + // the default separators except `@$` + wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g, + brackets: [['{', '}']], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], + folding: {}, +}; + +export const language = { + ignoreCase: false, + defaultToken: '', + tokenPostfix: '.traceql', + + keywords: [], + operators: [], + + // we include these common regular expressions + symbols: /[=>](?!@symbols)/, '@brackets'], + [ + /@symbols/, + { + cases: { + '@operators': 'delimiter', + '@default': '', + }, + }, + ], + + // numbers + [/\d+/, 'number'], + [/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/, 'number.float'], + [/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/, 'number.float'], + [/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/, 'number.hex'], + [/0[0-7']*[0-7](@integersuffix)/, 'number.octal'], + [/0[bB][0-1']*[0-1](@integersuffix)/, 'number.binary'], + [/\d[\d']*\d(@integersuffix)/, 'number'], + [/\d(@integersuffix)/, 'number'], + ], + + string_double: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, 'string', '@pop'], + ], + + string_single: [ + [/[^\\']+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/'/, 'string', '@pop'], + ], + + clauses: [ + [/[^(,)]/, 'tag'], + [/\)/, 'identifier', '@pop'], + ], + + whitespace: [[/[ \t\r\n]+/, 'white']], + }, +}; + +export const languageDefinition = { + id: 'traceql', + extensions: ['.traceql'], + aliases: ['tempo', 'traceql'], + mimetypes: [], + def: { + language, + languageConfiguration, + }, +}; diff --git a/public/app/plugins/datasource/tempo/types.ts b/public/app/plugins/datasource/tempo/types.ts new file mode 100644 index 00000000000..50096409f9f --- /dev/null +++ b/public/app/plugins/datasource/tempo/types.ts @@ -0,0 +1,7 @@ +import { DataSourceJsonData } from '@grafana/data/src'; + +import { TempoQuery } from './datasource'; + +export interface MyDataSourceOptions extends DataSourceJsonData {} + +export const defaultQuery: Partial = {};