Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tempo/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Tempo Datasource Plugin

This plugin supports Tempo v2.6.0 and later.

### How to install

This plugin requires react and react-dom 18
Expand Down
46 changes: 2 additions & 44 deletions tempo/src/model/tempo-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@
// limitations under the License.

import { UserFriendlyError } from '@perses-dev/client';
import {
MOCK_SEARCH_RESPONSE_MIXED_VPARQUET3_AND_4,
MOCK_SEARCH_RESPONSE_VPARQUET3,
MOCK_SEARCH_RESPONSE_VPARQUET4,
MOCK_TRACE_RESPONSE,
} from '../test';
import { query, search, searchTagValues, searchWithFallback } from './tempo-client';
import { MOCK_TRACE_RESPONSE } from '../test';
import { query, search, searchTagValues } from './tempo-client';

const fetchMock = (global.fetch = jest.fn());

Expand All @@ -40,43 +35,6 @@ describe('tempo-client', () => {
fetchMock.mockReset();
});

it('should return query results as-is when serviceStats are present', async () => {
fetchMock.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(MOCK_SEARCH_RESPONSE_VPARQUET4) });

const results = await searchWithFallback({ q: '{}' }, { datasourceUrl: '' });
expect(results).toEqual(MOCK_SEARCH_RESPONSE_VPARQUET4);
});

it('should augment query results with serviceStats if they are not present', async () => {
fetchMock.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(MOCK_SEARCH_RESPONSE_VPARQUET3) });
fetchMock.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(MOCK_TRACE_RESPONSE) });

const results = await searchWithFallback({ q: '{}' }, { datasourceUrl: '' });
expect(results).toEqual(MOCK_SEARCH_RESPONSE_VPARQUET4);
});

it('should augment query results with serviceStats if they are partially present', async () => {
fetchMock.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(MOCK_SEARCH_RESPONSE_MIXED_VPARQUET3_AND_4),
});
fetchMock.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(MOCK_TRACE_RESPONSE) });

const results = await searchWithFallback({ q: '{}' }, { datasourceUrl: '' });

// in the mock response, the first trace contains serviceStats but the second trace does not contain serviceStats
expect(results.traces[0]?.serviceStats).toEqual({
telemetrygen: { spanCount: 2 },
});
expect(results.traces[1]?.serviceStats).toEqual({
'article-service': { spanCount: 2 },
'auth-service': { spanCount: 1 },
'cart-service': { spanCount: 2 },
postgres: { spanCount: 1 },
'shop-backend': { spanCount: 4 },
});
});

it('should return v2 query response', async () => {
fetchMock.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(MOCK_TRACE_RESPONSE) });

Expand Down
70 changes: 0 additions & 70 deletions tempo/src/model/tempo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@

import { DatasourceClient } from '@perses-dev/plugin-system';
import { fetchJson, RequestHeaders, UserFriendlyError } from '@perses-dev/client';
import * as otlptracev1 from '@perses-dev/spec/dist/dashboard/query-type/otlp/trace/v1/trace';
import {
QueryRequestParameters,
SearchRequestParameters,
SearchTagsRequestParameters,
SearchTagsResponse,
QueryResponse,
ServiceStats,
SearchResponse,
SearchTagValuesRequestParameters,
SearchTagValuesResponse,
Expand All @@ -36,7 +34,6 @@ export interface TempoClient extends DatasourceClient {
// https://grafana.com/docs/tempo/latest/api_docs/
query(params: QueryRequestParameters, headers?: RequestHeaders): Promise<QueryResponse>;
search(params: SearchRequestParameters, headers?: RequestHeaders): Promise<SearchResponse>;
searchWithFallback(params: SearchRequestParameters, headers?: RequestHeaders): Promise<SearchResponse>;
searchTags(params: SearchTagsRequestParameters, headers?: RequestHeaders): Promise<SearchTagsResponse>;
searchTagValues(params: SearchTagValuesRequestParameters, headers?: RequestHeaders): Promise<SearchTagValuesResponse>;
}
Expand Down Expand Up @@ -123,73 +120,6 @@ export async function query(params: QueryRequestParameters, queryOptions: QueryO
return response;
}

/**
* Returns a summary report of traces that satisfy the query.
*
* If the serviceStats field is missing in the response, fetches all traces
* and calculates the serviceStats.
*
* Tempo computes the serviceStats field during ingestion since vParquet4,
* this fallback is required for older block formats.
*/
export async function searchWithFallback(
params: SearchRequestParameters,
queryOptions: QueryOptions
): Promise<SearchResponse> {
// Get a list of traces that satisfy the query.
const searchResponse = await search(params, queryOptions);
if (!searchResponse.traces || searchResponse.traces.length === 0) {
return { traces: [] };
}

// exit early if fallback is not required (serviceStats are contained in the response)
if (searchResponse.traces.every((t) => t.serviceStats)) {
return searchResponse;
}

// calculate serviceStats (number of spans and errors) per service
return {
traces: await Promise.all(
searchResponse.traces.map(async (trace) => {
if (trace.serviceStats) {
// fallback not required, serviceStats are contained in the response
return trace;
}

const serviceStats: Record<string, ServiceStats> = {};
const searchTraceIDResponse = await query({ traceId: trace.traceID }, queryOptions);

// For every trace, get the full trace, and find the number of spans and errors.
for (const batch of searchTraceIDResponse.trace.resourceSpans) {
let serviceName = 'unknown';
for (const attr of batch.resource?.attributes ?? []) {
if (attr.key === 'service.name' && 'stringValue' in attr.value) {
serviceName = attr.value.stringValue;
break;
}
}

const stats = serviceStats[serviceName] ?? { spanCount: 0 };
for (const scopeSpan of batch.scopeSpans) {
stats.spanCount += scopeSpan.spans.length;
for (const span of scopeSpan.spans) {
if (span.status?.code === otlptracev1.StatusCodeError) {
stats.errorCount = (stats.errorCount ?? 0) + 1;
}
}
}
serviceStats[serviceName] = stats;
}

return {
...trace,
serviceStats,
};
})
),
};
}

/**
* Returns a list of all tag names for a given scope.
*/
Expand Down
2 changes: 1 addition & 1 deletion tempo/src/plugins/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const datasource: TempoDatasourceSpec = {

const tempoStubClient = TempoDatasource.createClient(datasource, {});
tempoStubClient.query = jest.fn(async () => MOCK_TRACE_RESPONSE_SMALL);
tempoStubClient.searchWithFallback = jest.fn(async () => MOCK_SEARCH_RESPONSE_VPARQUET4);
tempoStubClient.search = jest.fn(async () => MOCK_SEARCH_RESPONSE_VPARQUET4);

const getDatasourceClient: jest.Mock = jest.fn(() => {
return tempoStubClient;
Expand Down
4 changes: 1 addition & 3 deletions tempo/src/plugins/tempo-datasource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// limitations under the License.

import { DatasourcePlugin } from '@perses-dev/plugin-system';
import { TempoClient, query, search, searchTagValues, searchTags, searchWithFallback } from '../model/tempo-client';
import { TempoClient, query, search, searchTagValues, searchTags } from '../model/tempo-client';
import { TempoDatasourceSpec } from './tempo-datasource-types';
import { TempoDatasourceEditor } from './TempoDatasourceEditor';

Expand All @@ -37,8 +37,6 @@ const createClient: DatasourcePlugin<TempoDatasourceSpec, TempoClient>['createCl
},
query: (params, headers) => query(params, { datasourceUrl, headers: headers ?? specHeaders }),
search: (params, headers) => search(params, { datasourceUrl, headers: headers ?? specHeaders }),
searchWithFallback: (params, headers) =>
searchWithFallback(params, { datasourceUrl, headers: headers ?? specHeaders }),
searchTags: (params, headers) => searchTags(params, { datasourceUrl, headers: headers ?? specHeaders }),
searchTagValues: (params, headers) => searchTagValues(params, { datasourceUrl, headers: headers ?? specHeaders }),
};
Expand Down
10 changes: 5 additions & 5 deletions tempo/src/plugins/tempo-trace-query/get-trace-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const datasource: TempoDatasourceSpec = {

const createMockClient = (searchResponse: SearchResponse): TempoClient => {
const client = TempoDatasource.createClient(datasource, {});
client.searchWithFallback = jest.fn(async () => searchResponse);
client.search = jest.fn(async () => searchResponse);
return client;
};

Expand Down Expand Up @@ -99,7 +99,7 @@ describe('getTraceData', () => {
const result = await getTraceData({ query: '{}' }, stubContext);

// Verify client was called with limit+1
expect(mockClient.searchWithFallback).toHaveBeenCalledWith(
expect(mockClient.search).toHaveBeenCalledWith(
expect.objectContaining({
limit: DEFAULT_SEARCH_LIMIT + 1,
})
Expand Down Expand Up @@ -130,7 +130,7 @@ describe('getTraceData', () => {
const result = await getTraceData({ query: '{}', limit: customLimit }, stubContext);

// Verify client was called with customLimit+1
expect(mockClient.searchWithFallback).toHaveBeenCalledWith(
expect(mockClient.search).toHaveBeenCalledWith(
expect.objectContaining({
limit: customLimit + 1,
})
Expand Down Expand Up @@ -178,7 +178,7 @@ describe('getTraceData', () => {

const result = await getTraceData({ query: '{resource.service.name="$serviceName"}' }, stubContext);

expect(mockClient.searchWithFallback).toHaveBeenCalledWith(
expect(mockClient.search).toHaveBeenCalledWith(
expect.objectContaining({
q: '{resource.service.name="frontend"}',
})
Expand Down Expand Up @@ -265,7 +265,7 @@ describe('getTraceData', () => {
const result = await getTraceData({ query: rawQuery }, stubContext);

expect(mockedReplaceVariables).toHaveBeenCalledWith(rawQuery, stubContext.variableState);
expect(mockClient.searchWithFallback).toHaveBeenCalledWith(
expect(mockClient.search).toHaveBeenCalledWith(
expect.objectContaining({
q: replacedQuery,
})
Expand Down
2 changes: 1 addition & 1 deletion tempo/src/plugins/tempo-trace-query/get-trace-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const getTraceData: TraceQueryPlugin<TempoTraceQuerySpec>['getTraceData']
const limit = spec.limit ?? DEFAULT_SEARCH_LIMIT;
params.limit = limit + 1;

const response = await client.searchWithFallback(params);
const response = await client.search(params);
const searchResult = parseSearchResponse(response);
const hasMoreResults = searchResult.length > limit;

Expand Down
Loading