mirror of https://github.com/grafana/grafana
Tracing: Add Tempo data source (#28204)
* Add tempo datasource, mostly copy of jaeger datasource code * Add label to input field * Add logo * Remove access option from configuration * Add white space to field label * Add documentation * Fix link in docs * Update public/app/plugins/datasource/tempo/ConfigEditor.tsx Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com> * Update public/app/plugins/datasource/tempo/QueryField.tsx Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com> * Add data source to the docs menu * Add simple implementation for testDatasource * Wording updates to the docs. Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>pull/28231/head
parent
25393e20aa
commit
b7fee71049
@ -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. |
@ -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<Props> = ({ options, onOptionsChange }) => { |
||||
return ( |
||||
<DataSourceHttpSettings |
||||
defaultUrl="http://localhost:16686" |
||||
dataSourceConfig={options} |
||||
showAccessOptions={false} |
||||
onChange={onOptionsChange} |
||||
/> |
||||
); |
||||
}; |
@ -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<TempoDatasource, TempoQuery>; |
||||
export class TempoQueryField extends React.PureComponent<Props> { |
||||
render() { |
||||
const { query, onChange } = this.props; |
||||
|
||||
return ( |
||||
<LegacyForms.FormField |
||||
label="Trace ID" |
||||
labelWidth={4} |
||||
inputEl={ |
||||
<div className="slate-query-field__wrapper"> |
||||
<div className="slate-query-field"> |
||||
<input |
||||
style={{ width: '100%' }} |
||||
value={query.query || ''} |
||||
onChange={e => |
||||
onChange({ |
||||
...query, |
||||
query: e.currentTarget.value, |
||||
}) |
||||
} |
||||
/> |
||||
</div> |
||||
</div> |
||||
} |
||||
/> |
||||
); |
||||
} |
||||
} |
@ -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<any> { |
||||
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<void>) { |
||||
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<TempoQuery> = { |
||||
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', |
||||
}, |
||||
], |
||||
}; |
@ -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<TempoQuery> { |
||||
constructor(private instanceSettings: DataSourceInstanceSettings, private readonly timeSrv: TimeSrv = getTimeSrv()) { |
||||
super(instanceSettings); |
||||
} |
||||
|
||||
async metadataRequest(url: string, params?: Record<string, any>): Promise<any> { |
||||
const res = await this._request(url, params, { hideFromInspector: true }).toPromise(); |
||||
return res.data.data; |
||||
} |
||||
|
||||
query(options: DataQueryRequest<TempoQuery>): Observable<DataQueryResponse> { |
||||
// 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<any> { |
||||
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<BackendSrvRequest>): Observable<Record<string, any>> { |
||||
// 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; |
||||
} |
After Width: | Height: | Size: 1.3 KiB |
@ -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); |
@ -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" |
||||
} |
||||
] |
||||
} |
||||
} |
Loading…
Reference in new issue