diff --git a/docs/sources/datasources/_index.md b/docs/sources/datasources/_index.md index ff574a84bb3..99cf0301bb1 100644 --- a/docs/sources/datasources/_index.md +++ b/docs/sources/datasources/_index.md @@ -33,6 +33,9 @@ The following data sources are officially supported: - [OpenTSDB]({{< relref "opentsdb.md" >}}) - [PostgreSQL]({{< relref "postgres.md" >}}) - [Prometheus]({{< relref "prometheus.md" >}}) +- [Jaeger]({{< relref "jaeger.md" >}}) +- [Zipkin]({{< relref "zipkin.md" >}}) +- [Tempo]({{< relref "tempo.md" >}}) - [Testdata]({{< relref "testdata.md" >}}) In addition to the data sources that you have configured in your Grafana, there are three special data sources available: diff --git a/docs/sources/datasources/tempo.md b/docs/sources/datasources/tempo.md new file mode 100644 index 00000000000..06e949df772 --- /dev/null +++ b/docs/sources/datasources/tempo.md @@ -0,0 +1,38 @@ ++++ +title = "Tempo" +description = "High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs." +keywords = ["grafana", "tempo", "guide", "tracing"] +type = "docs" +aliases = ["/docs/grafana/latest/features/datasources/tempo"] +[menu.docs] +name = "Tempo" +parent = "datasources" +weight = 800 ++++ + +# Tempo data source + +Grafana ships with built-in support for Tempo a high volume, minimal dependency trace storage, OSS tracing solution from Grafana Labs. Add it as a data source, and you are ready to query your traces in [Explore]({{< relref "../explore/index.md" >}}). + +## Adding the data source +To access Tempo settings, click the **Configuration** (gear) icon, then click **Data Sources** > **Tempo**. + +| Name | Description | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| _Name_ | The data source name using which you will refer to the data source in panels, queries, and Explore. | +| _Default_ | The default data source will be pre-selected for new panels. | +| _URL_ | The URL of the Tempo instance, e.g., `http://localhost:16686` | +| _Basic Auth_ | Enable basic authentication to the Tempo data source. | +| _User_ | User name for basic authentication. | +| _Password_ | Password for basic authentication. | + +## Query traces + +You can query and display traces from Tempo via [Explore]({{< relref "../explore/index.md" >}}). +To query a particular trace, insert its trace ID into the query text input. + +{{< docs-imagebox img="/img/docs/v73/tempo-query-editor.png" class="docs-image--no-shadow" caption="Screenshot of the Tempo query editor" >}} + +## Linking Trace ID from logs + +You can link to Tempo trace from logs in Loki or Elastic by configuring an internal link. See the [Derived fields]({{< relref "loki.md#derived-fields" >}}) section in the [Loki data source]({{< relref "loki.md" >}}) or [Data links]({{< relref "elasticsearch.md#data-links" >}}) section in the [Elastic data source]({{< relref "elasticsearch.md" >}}) for configuration instructions. diff --git a/docs/sources/menu.yaml b/docs/sources/menu.yaml index 88bf1ba10a0..15032417457 100644 --- a/docs/sources/menu.yaml +++ b/docs/sources/menu.yaml @@ -156,6 +156,8 @@ name: PostgreSQL - link: /datasources/prometheus/ name: Prometheus + - link: /datasources/tempo/ + name: Tempo - link: /datasources/testdata/ name: TestData DB - link: /datasources/zipkin/ diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 9ef69c5f7e2..adbf001f267 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -35,6 +35,8 @@ const azureMonitorPlugin = async () => await import( /* webpackChunkName: "azureMonitorPlugin" */ 'app/plugins/datasource/grafana-azure-monitor-datasource/module' ); +const tempoPlugin = async () => + await import(/* webpackChunkName: "tempoPlugin" */ 'app/plugins/datasource/tempo/module'); import * as textPanel from 'app/plugins/panel/text/module'; import * as graph2Panel from 'app/plugins/panel/graph2/module'; @@ -78,6 +80,7 @@ const builtInPlugins: any = { 'app/plugins/datasource/testdata/module': testDataDSPlugin, 'app/plugins/datasource/cloud-monitoring/module': cloudMonitoringPlugin, 'app/plugins/datasource/grafana-azure-monitor-datasource/module': azureMonitorPlugin, + 'app/plugins/datasource/tempo/module': tempoPlugin, 'app/plugins/panel/text/module': textPanel, 'app/plugins/panel/graph2/module': graph2Panel, diff --git a/public/app/plugins/datasource/tempo/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/ConfigEditor.tsx new file mode 100644 index 00000000000..3f903cd1226 --- /dev/null +++ b/public/app/plugins/datasource/tempo/ConfigEditor.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; +import { DataSourceHttpSettings } from '@grafana/ui'; + +export type Props = DataSourcePluginOptionsEditorProps; + +export const ConfigEditor: React.FC = ({ options, onOptionsChange }) => { + return ( + + ); +}; diff --git a/public/app/plugins/datasource/tempo/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryField.tsx new file mode 100644 index 00000000000..36823235b7c --- /dev/null +++ b/public/app/plugins/datasource/tempo/QueryField.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { TempoDatasource, TempoQuery } from './datasource'; + +import { ExploreQueryFieldProps } from '@grafana/data'; +import { LegacyForms } from '@grafana/ui'; + +type Props = ExploreQueryFieldProps; +export class TempoQueryField extends React.PureComponent { + render() { + const { query, onChange } = this.props; + + return ( + +
+ + onChange({ + ...query, + query: e.currentTarget.value, + }) + } + /> +
+ + } + /> + ); + } +} diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts new file mode 100644 index 00000000000..eb9f477d057 --- /dev/null +++ b/public/app/plugins/datasource/tempo/datasource.test.ts @@ -0,0 +1,119 @@ +import { TempoDatasource, TempoQuery } from './datasource'; +import { DataQueryRequest, DataSourceInstanceSettings, FieldType, PluginType, dateTime } from '@grafana/data'; +import { BackendSrv, BackendSrvRequest, getBackendSrv, setBackendSrv } from '@grafana/runtime'; + +describe('JaegerDatasource', () => { + it('returns trace when queried', async () => { + await withMockedBackendSrv(makeBackendSrvMock('12345'), async () => { + const ds = new TempoDatasource(defaultSettings); + const response = await ds.query(defaultQuery).toPromise(); + const field = response.data[0].fields[0]; + expect(field.name).toBe('trace'); + expect(field.type).toBe(FieldType.trace); + expect(field.values.get(0)).toEqual({ + traceId: '12345', + }); + }); + }); + + it('returns trace when traceId with special characters is queried', async () => { + await withMockedBackendSrv(makeBackendSrvMock('a/b'), async () => { + const ds = new TempoDatasource(defaultSettings); + const query = { + ...defaultQuery, + targets: [ + { + query: 'a/b', + refId: '1', + }, + ], + }; + const response = await ds.query(query).toPromise(); + const field = response.data[0].fields[0]; + expect(field.name).toBe('trace'); + expect(field.type).toBe(FieldType.trace); + expect(field.values.get(0)).toEqual({ + traceId: 'a/b', + }); + }); + }); + + it('returns empty response if trace id is not specified', async () => { + const ds = new TempoDatasource(defaultSettings); + const response = await ds + .query({ + ...defaultQuery, + targets: [], + }) + .toPromise(); + const field = response.data[0].fields[0]; + expect(field.name).toBe('trace'); + expect(field.type).toBe(FieldType.trace); + expect(field.values.length).toBe(0); + }); +}); + +function makeBackendSrvMock(traceId: string) { + return { + datasourceRequest(options: BackendSrvRequest): Promise { + expect(options.url.substr(options.url.length - 17, options.url.length)).toBe( + `/api/traces/${encodeURIComponent(traceId)}` + ); + return Promise.resolve({ + data: { + data: [ + { + traceId, + }, + ], + }, + }); + }, + } as any; +} + +async function withMockedBackendSrv(srv: BackendSrv, fn: () => Promise) { + const oldSrv = getBackendSrv(); + setBackendSrv(srv); + await fn(); + setBackendSrv(oldSrv); +} + +const defaultSettings: DataSourceInstanceSettings = { + id: 0, + uid: '0', + type: 'tracing', + name: 'jaeger', + meta: { + id: 'jaeger', + name: 'jaeger', + type: PluginType.datasource, + info: {} as any, + module: '', + baseUrl: '', + }, + jsonData: {}, +}; + +const defaultQuery: DataQueryRequest = { + requestId: '1', + dashboardId: 0, + interval: '0', + intervalMs: 10, + panelId: 0, + scopedVars: {}, + range: { + from: dateTime().subtract(1, 'h'), + to: dateTime(), + raw: { from: '1h', to: 'now' }, + }, + timezone: 'browser', + app: 'explore', + startTime: 0, + targets: [ + { + query: '12345', + refId: '1', + }, + ], +}; diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts new file mode 100644 index 00000000000..016725658c5 --- /dev/null +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -0,0 +1,122 @@ +import { + dateMath, + DateTime, + MutableDataFrame, + DataSourceApi, + DataSourceInstanceSettings, + DataQueryRequest, + DataQueryResponse, + DataQuery, + FieldType, +} from '@grafana/data'; +import { getBackendSrv, BackendSrvRequest } from '@grafana/runtime'; +import { Observable, from, of } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { serializeParams } from 'app/core/utils/fetch'; + +export type TempoQuery = { + query: string; +} & DataQuery; + +export class TempoDatasource extends DataSourceApi { + constructor(private instanceSettings: DataSourceInstanceSettings, private readonly timeSrv: TimeSrv = getTimeSrv()) { + super(instanceSettings); + } + + async metadataRequest(url: string, params?: Record): Promise { + const res = await this._request(url, params, { hideFromInspector: true }).toPromise(); + return res.data.data; + } + + query(options: DataQueryRequest): Observable { + // At this moment we expect only one target. In case we somehow change the UI to be able to show multiple + // traces at one we need to change this. + const id = options.targets[0]?.query; + if (id) { + return this._request(`/api/traces/${encodeURIComponent(id)}`).pipe( + map(response => { + return { + data: [ + new MutableDataFrame({ + fields: [ + { + name: 'trace', + type: FieldType.trace, + values: response?.data?.data || [], + }, + ], + meta: { + preferredVisualisationType: 'trace', + }, + }), + ], + }; + }) + ); + } else { + return of({ + data: [ + new MutableDataFrame({ + fields: [ + { + name: 'trace', + type: FieldType.trace, + values: [], + }, + ], + meta: { + preferredVisualisationType: 'trace', + }, + }), + ], + }); + } + } + + async testDatasource(): Promise { + try { + await this._request(`/api/traces/random`).toPromise(); + } catch (e) { + // As we are not searching for a valid trace here this will definitely fail but we should return 502 if it's + // unreachable. 500 should otherwise be from tempo it self but probably makes sense to report them here. + if (e?.status >= 500 && e?.status < 600) { + throw e; + } + } + return true; + } + + getTimeRange(): { start: number; end: number } { + const range = this.timeSrv.timeRange(); + return { + start: getTime(range.from, false), + end: getTime(range.to, true), + }; + } + + getQueryDisplayText(query: TempoQuery) { + return query.query; + } + + private _request(apiUrl: string, data?: any, options?: Partial): Observable> { + // Hack for proxying metadata requests + const baseUrl = `/api/datasources/proxy/${this.instanceSettings.id}`; + const params = data ? serializeParams(data) : ''; + const url = `${baseUrl}${apiUrl}${params.length ? `?${params}` : ''}`; + const req = { + ...options, + url, + }; + + return from(getBackendSrv().datasourceRequest(req)); + } +} + +function getTime(date: string | DateTime, roundUp: boolean) { + if (typeof date === 'string') { + date = dateMath.parse(date, roundUp)!; + } + return date.valueOf() * 1000; +} diff --git a/public/app/plugins/datasource/tempo/img/tempo_logo.svg b/public/app/plugins/datasource/tempo/img/tempo_logo.svg new file mode 100644 index 00000000000..c1f0ea76e56 --- /dev/null +++ b/public/app/plugins/datasource/tempo/img/tempo_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/app/plugins/datasource/tempo/module.ts b/public/app/plugins/datasource/tempo/module.ts new file mode 100644 index 00000000000..f8356dc424d --- /dev/null +++ b/public/app/plugins/datasource/tempo/module.ts @@ -0,0 +1,8 @@ +import { DataSourcePlugin } from '@grafana/data'; +import { TempoDatasource } from './datasource'; +import { TempoQueryField } from './QueryField'; +import { ConfigEditor } from './ConfigEditor'; + +export const plugin = new DataSourcePlugin(TempoDatasource) + .setConfigEditor(ConfigEditor) + .setExploreQueryField(TempoQueryField); diff --git a/public/app/plugins/datasource/tempo/plugin.json b/public/app/plugins/datasource/tempo/plugin.json new file mode 100644 index 00000000000..4451adc8cd2 --- /dev/null +++ b/public/app/plugins/datasource/tempo/plugin.json @@ -0,0 +1,31 @@ +{ + "type": "datasource", + "name": "Tempo", + "id": "tempo", + "category": "tracing", + + "metrics": false, + "alerting": false, + "annotations": false, + "logs": false, + "streaming": false, + "tracing": true, + + "info": { + "description": "High volume, minimal dependency trace storage. OSS tracing solution from Grafana Labs.", + "author": { + "name": "Grafana Labs", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/tempo_logo.svg", + "large": "img/tempo_logo.svg" + }, + "links": [ + { + "name": "GitHub Project", + "url": "https://github.com/grafana/tempo" + } + ] + } +}