Tempo: Migrates tags and tag values to datasource backend CallResource requests (#110511)
* Move tags and tag values request to datasource backend * Remove outdated test * Fix tests * lint * Refactor to use handlers for CallResource request * lint * Fix nit
This commit is contained in:
@@ -235,7 +235,7 @@ func NewPlugin(pluginID string, cfg *setting.Cfg, httpClientProvider *httpclient
|
||||
case Prometheus:
|
||||
svc = prometheus.ProvideService(httpClientProvider)
|
||||
case Tempo:
|
||||
svc = tempo.ProvideService(httpClientProvider)
|
||||
svc = tempo.ProvideService(httpClientProvider, tracer)
|
||||
case PostgreSQL:
|
||||
svc = postgres.ProvideService(cfg, features)
|
||||
case MySQL:
|
||||
|
||||
@@ -389,7 +389,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||
lokiService := loki.ProvideService(httpclientProvider, tracer)
|
||||
opentsdbService := opentsdb.ProvideService(httpclientProvider)
|
||||
prometheusService := prometheus.ProvideService(httpclientProvider)
|
||||
tempoService := tempo.ProvideService(httpclientProvider)
|
||||
tempoService := tempo.ProvideService(httpclientProvider, tracer)
|
||||
testdatasourceService := testdatasource.ProvideService()
|
||||
postgresService := postgres.ProvideService(cfg, featureToggles)
|
||||
mysqlService := mysql.ProvideService()
|
||||
@@ -976,7 +976,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
lokiService := loki.ProvideService(httpclientProvider, tracer)
|
||||
opentsdbService := opentsdb.ProvideService(httpclientProvider)
|
||||
prometheusService := prometheus.ProvideService(httpclientProvider)
|
||||
tempoService := tempo.ProvideService(httpclientProvider)
|
||||
tempoService := tempo.ProvideService(httpclientProvider, tracer)
|
||||
testdatasourceService := testdatasource.ProvideService()
|
||||
postgresService := postgres.ProvideService(cfg, featureToggles)
|
||||
mysqlService := mysql.ProvideService()
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestIntegrationPluginManager(t *testing.T) {
|
||||
lk := loki.ProvideService(hcp, tracer)
|
||||
otsdb := opentsdb.ProvideService(hcp)
|
||||
pr := prometheus.ProvideService(hcp)
|
||||
tmpo := tempo.ProvideService(hcp)
|
||||
tmpo := tempo.ProvideService(hcp, tracer)
|
||||
td := testdatasource.ProvideService()
|
||||
pg := postgres.ProvideService(cfg, features)
|
||||
my := mysql.ProvideService()
|
||||
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.opentelemetry.io/otel/trace/noop"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
@@ -11,8 +13,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
_ backend.QueryDataHandler = (*Datasource)(nil)
|
||||
_ backend.StreamHandler = (*Datasource)(nil)
|
||||
_ backend.QueryDataHandler = (*Datasource)(nil)
|
||||
_ backend.StreamHandler = (*Datasource)(nil)
|
||||
_ backend.CallResourceHandler = (*Datasource)(nil)
|
||||
)
|
||||
|
||||
type Datasource struct {
|
||||
@@ -21,7 +24,7 @@ type Datasource struct {
|
||||
|
||||
func NewDatasource(c context.Context, b backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
return &Datasource{
|
||||
Service: tempo.ProvideService(httpclient.NewProvider()),
|
||||
Service: tempo.ProvideService(httpclient.NewProvider(), noop.NewTracerProvider().Tracer("tempo")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -40,3 +43,7 @@ func (d *Datasource) PublishStream(ctx context.Context, req *backend.PublishStre
|
||||
func (d *Datasource) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
|
||||
return d.Service.RunStream(ctx, req, sender)
|
||||
}
|
||||
|
||||
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return d.Service.CallResource(ctx, req, sender)
|
||||
}
|
||||
|
||||
+143
-4
@@ -3,22 +3,38 @@ package tempo
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
"github.com/grafana/grafana/pkg/tsdb/tempo/kinds/dataquery"
|
||||
"github.com/grafana/tempo/pkg/tempopb"
|
||||
)
|
||||
|
||||
var (
|
||||
_ backend.QueryDataHandler = (*Service)(nil)
|
||||
_ backend.CallResourceHandler = (*Service)(nil)
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
im instancemgmt.InstanceManager
|
||||
logger log.Logger
|
||||
im instancemgmt.InstanceManager
|
||||
logger log.Logger
|
||||
tracer trace.Tracer
|
||||
resourceHandler backend.CallResourceHandler
|
||||
}
|
||||
|
||||
type DatasourceInfo struct {
|
||||
@@ -27,11 +43,20 @@ type DatasourceInfo struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func ProvideService(httpClientProvider *httpclient.Provider) *Service {
|
||||
return &Service{
|
||||
func ProvideService(httpClientProvider *httpclient.Provider, tracer trace.Tracer) *Service {
|
||||
s := &Service{
|
||||
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
|
||||
logger: backend.NewLoggerWith("logger", "tsdb.tempo"),
|
||||
tracer: tracer,
|
||||
}
|
||||
|
||||
// Set up resource routes using httpadapter
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/tags", s.handleTags)
|
||||
mux.HandleFunc("/tag-values", s.handleTagValues)
|
||||
s.resourceHandler = httpadapter.New(mux)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
@@ -43,6 +68,8 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts.ForwardHTTPHeaders = true
|
||||
|
||||
client, err := httpClientProvider.New(opts)
|
||||
if err != nil {
|
||||
ctxLogger.Error("Failed to get HTTP client provider", "error", err, "function", logEntrypoint())
|
||||
@@ -123,6 +150,118 @@ func (s *Service) getDSInfo(ctx context.Context, pluginCtx backend.PluginContext
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return s.resourceHandler.CallResource(ctx, req, sender)
|
||||
}
|
||||
|
||||
// handleTags handles requests to /tags resource
|
||||
func (s *Service) handleTags(rw http.ResponseWriter, req *http.Request) {
|
||||
s.proxyToTempo(rw, req, "api/v2/search/tags")
|
||||
}
|
||||
|
||||
// handleTagValues handles requests to /tag-values resource
|
||||
func (s *Service) handleTagValues(rw http.ResponseWriter, req *http.Request) {
|
||||
// Extract the encoded tag from query parameters
|
||||
encodedTag := req.URL.Query().Get("tag")
|
||||
if encodedTag == "" {
|
||||
http.Error(rw, "Missing required 'tag' parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tempoPath := fmt.Sprintf("api/v2/search/tag/%s/values", encodedTag)
|
||||
s.proxyToTempo(rw, req, tempoPath)
|
||||
}
|
||||
|
||||
// proxyToTempo is the shared function that builds the URL and proxies requests to Tempo
|
||||
func (s *Service) proxyToTempo(rw http.ResponseWriter, req *http.Request, tempoPath string) {
|
||||
ctx := req.Context()
|
||||
pCtx := backend.PluginConfigFromContext(ctx)
|
||||
|
||||
// Get datasource info
|
||||
dsInfo, err := s.getDSInfo(ctx, pCtx)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get data source info", "error", err)
|
||||
http.Error(rw, "Failed to get data source configuration", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, span := s.tracer.Start(ctx, "datasource.tempo.proxyToTempo", trace.WithAttributes(
|
||||
attribute.String("tempoPath", tempoPath),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
// Build the full URL to Tempo
|
||||
parsedURL, err := url.Parse(dsInfo.URL)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("Failed to parse data source URL", "error", err, "url", dsInfo.URL)
|
||||
http.Error(rw, "Invalid data source URL", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Join the tempo path with the base URL
|
||||
parsedURL.Path = path.Join(parsedURL.Path, tempoPath)
|
||||
// Preserve query parameters from the original request
|
||||
parsedURL.RawQuery = req.URL.RawQuery
|
||||
|
||||
s.logger.Debug("Making resource request to Tempo", "url", parsedURL.String())
|
||||
start := time.Now()
|
||||
|
||||
// Create the request to Tempo
|
||||
httpReq, err := http.NewRequestWithContext(ctx, req.Method, parsedURL.String(), req.Body)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("Failed to create HTTP request", "error", err)
|
||||
http.Error(rw, "Failed to create request", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy headers from the original request
|
||||
for name, values := range req.Header {
|
||||
for _, value := range values {
|
||||
httpReq.Header.Add(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Make the request to Tempo
|
||||
resp, err := dsInfo.HTTPClient.Do(httpReq)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("Failed resource call to Tempo", "error", err, "url", parsedURL.String(), "duration", time.Since(start))
|
||||
http.Error(rw, "Failed to connect to Tempo", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
s.logger.Warn("Failed to close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
s.logger.Debug("Response received from Tempo", "statusCode", resp.StatusCode, "contentLength", resp.Header.Get("Content-Length"), "duration", time.Since(start))
|
||||
|
||||
// Copy response headers
|
||||
for name, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
rw.Header().Add(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the status code
|
||||
rw.WriteHeader(resp.StatusCode)
|
||||
|
||||
// Copy the response body
|
||||
_, err = io.Copy(rw, resp.Body)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("Failed to copy response body", "error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Return the file, line, and (full-path) function name of the caller
|
||||
func getRunContext() (string, int, string) {
|
||||
pc := make([]uintptr, 10)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { initTemplateSrv } from '../test/test_utils';
|
||||
import { Scope } from '../types';
|
||||
|
||||
import TagsInput from './TagsInput';
|
||||
import { v1Tags, v2Tags } from './mocks';
|
||||
import { v2Tags } from './mocks';
|
||||
|
||||
describe('TagsInput', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
@@ -33,22 +33,8 @@ describe('TagsInput', () => {
|
||||
});
|
||||
|
||||
describe('should render correct tags', () => {
|
||||
it('for API v1 tags', async () => {
|
||||
renderTagsInput(v1Tags);
|
||||
|
||||
const tag = screen.getByText('Select tag');
|
||||
expect(tag).toBeInTheDocument();
|
||||
await user.click(tag);
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('bar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('for API v2 tags with scope of resource', async () => {
|
||||
renderTagsInput(undefined, v2Tags, TraceqlSearchScope.Resource);
|
||||
renderTagsInput(v2Tags, TraceqlSearchScope.Resource);
|
||||
|
||||
const tag = screen.getByText('Select tag');
|
||||
expect(tag).toBeInTheDocument();
|
||||
@@ -64,7 +50,7 @@ describe('TagsInput', () => {
|
||||
});
|
||||
|
||||
it('for API v2 tags with scope of span', async () => {
|
||||
renderTagsInput(undefined, v2Tags, TraceqlSearchScope.Span);
|
||||
renderTagsInput(v2Tags, TraceqlSearchScope.Span);
|
||||
|
||||
const tag = screen.getByText('Select tag');
|
||||
expect(tag).toBeInTheDocument();
|
||||
@@ -80,7 +66,7 @@ describe('TagsInput', () => {
|
||||
});
|
||||
|
||||
it('for API v2 tags with scope of unscoped', async () => {
|
||||
renderTagsInput(undefined, v2Tags, TraceqlSearchScope.Unscoped);
|
||||
renderTagsInput(v2Tags, TraceqlSearchScope.Unscoped);
|
||||
|
||||
const tag = screen.getByText('Select tag');
|
||||
expect(tag).toBeInTheDocument();
|
||||
@@ -96,7 +82,7 @@ describe('TagsInput', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const renderTagsInput = (tagsV1?: string[], tagsV2?: Scope[], scope?: TraceqlSearchScope) => {
|
||||
const renderTagsInput = (tagsV2?: Scope[], scope?: TraceqlSearchScope) => {
|
||||
const datasource: TempoDatasource = {
|
||||
search: {
|
||||
filters: [],
|
||||
@@ -104,9 +90,7 @@ describe('TagsInput', () => {
|
||||
} as unknown as TempoDatasource;
|
||||
|
||||
const lp = new TempoLanguageProvider(datasource);
|
||||
if (tagsV1) {
|
||||
lp.setV1Tags(tagsV1);
|
||||
} else if (tagsV2) {
|
||||
if (tagsV2) {
|
||||
lp.setV2Tags(tagsV2);
|
||||
}
|
||||
datasource.languageProvider = lp;
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
} from './datasource';
|
||||
import mockJson from './test/mockJsonResponse.json';
|
||||
import mockServiceGraph from './test/mockServiceGraph.json';
|
||||
import { createMetadataRequest, createTempoDatasource } from './test/mocks';
|
||||
import { createTempoDatasource } from './test/mocks';
|
||||
import { initTemplateSrv } from './test/test_utils';
|
||||
import { TempoJsonData, TempoQuery } from './types';
|
||||
|
||||
@@ -355,11 +355,11 @@ describe('Tempo data source', () => {
|
||||
});
|
||||
|
||||
describe('test the metadataRequest function', () => {
|
||||
it('should return the last value from the observed stream', async () => {
|
||||
mockObservable = () => of('321', '123', '456');
|
||||
it('should return the data from getResource', async () => {
|
||||
const ds = new TempoDatasource(defaultSettings);
|
||||
const response = await ds.metadataRequest('/api/search/tags');
|
||||
expect(response).toBe('456');
|
||||
jest.spyOn(ds, 'getResource').mockResolvedValue({ data: 'test-data' });
|
||||
const response = await ds.metadataRequest('api/v2/search/tags');
|
||||
expect(response).toBe('test-data');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1106,13 +1106,9 @@ describe('label names - v2 tags', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
datasource = createTempoDatasource();
|
||||
jest.spyOn(datasource, 'metadataRequest').mockImplementation(
|
||||
createMetadataRequest({
|
||||
data: {
|
||||
scopes: [{ name: 'span', tags: ['label1', 'label2'] }],
|
||||
},
|
||||
})
|
||||
);
|
||||
// Mock the language provider to return v2 tags
|
||||
datasource.languageProvider.tagsV2 = [{ name: 'span', tags: ['label1', 'label2'] }];
|
||||
jest.spyOn(datasource.languageProvider, 'start').mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it('get label names', async () => {
|
||||
@@ -1123,55 +1119,18 @@ describe('label names - v2 tags', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('label names - v1 tags', () => {
|
||||
let datasource: TempoDatasource;
|
||||
|
||||
beforeEach(() => {
|
||||
datasource = createTempoDatasource();
|
||||
jest
|
||||
.spyOn(datasource, 'metadataRequest')
|
||||
.mockImplementationOnce(() => {
|
||||
throw Error;
|
||||
})
|
||||
.mockImplementation(
|
||||
createMetadataRequest({
|
||||
data: {
|
||||
tagNames: ['label1', 'label2'],
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('get label names', async () => {
|
||||
// label_names()
|
||||
const response = await datasource.executeVariableQuery({ refId: 'test', type: TempoVariableQueryType.LabelNames });
|
||||
expect(response).toEqual([{ text: 'label1' }, { text: 'label2' }, { text: 'status.code' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('label values', () => {
|
||||
let datasource: TempoDatasource;
|
||||
|
||||
beforeEach(() => {
|
||||
datasource = createTempoDatasource();
|
||||
jest.spyOn(datasource, 'metadataRequest').mockImplementation(
|
||||
createMetadataRequest({
|
||||
data: {
|
||||
tagValues: [
|
||||
{
|
||||
type: 'value1',
|
||||
value: 'value1',
|
||||
label: 'value1',
|
||||
},
|
||||
{
|
||||
type: 'value2',
|
||||
value: 'value2',
|
||||
label: 'value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
// Mock the language provider to return v2 tags that includes the "label" tag
|
||||
datasource.languageProvider.tagsV2 = [{ name: 'span', tags: ['label'] }];
|
||||
jest.spyOn(datasource.languageProvider, 'start').mockResolvedValue([]);
|
||||
jest.spyOn(datasource.languageProvider, 'getOptionsV2').mockResolvedValue([
|
||||
{ type: 'string', value: 'value1', label: 'value1' },
|
||||
{ type: 'string', value: 'value2', label: 'value2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('get label values for given label', async () => {
|
||||
@@ -1182,10 +1141,7 @@ describe('label values', () => {
|
||||
label: 'label',
|
||||
});
|
||||
|
||||
expect(response).toEqual([
|
||||
{ text: { type: 'value1', value: 'value1', label: 'value1' } },
|
||||
{ text: { type: 'value2', value: 'value2', label: 'value2' } },
|
||||
]);
|
||||
expect(response).toEqual([{ text: 'value1' }, { text: 'value2' }]);
|
||||
});
|
||||
|
||||
it('do not raise error when label is not set', async () => {
|
||||
@@ -1205,25 +1161,13 @@ describe('should provide functionality for ad-hoc filters', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
datasource = createTempoDatasource();
|
||||
jest.spyOn(datasource, 'metadataRequest').mockImplementation(
|
||||
createMetadataRequest({
|
||||
data: {
|
||||
scopes: [{ name: 'span', tags: ['label1', 'label2'] }],
|
||||
tagValues: [
|
||||
{
|
||||
type: 'value1',
|
||||
value: 'value1',
|
||||
label: 'value1',
|
||||
},
|
||||
{
|
||||
type: 'value2',
|
||||
value: 'value2',
|
||||
label: 'value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
// Mock the language provider to return v2 tags
|
||||
datasource.languageProvider.tagsV2 = [{ name: 'span', tags: ['label1', 'label2'] }];
|
||||
jest.spyOn(datasource.languageProvider, 'fetchTags').mockResolvedValue();
|
||||
jest.spyOn(datasource.languageProvider, 'getOptionsV2').mockResolvedValue([
|
||||
{ type: 'string', value: 'value1', label: 'value1' },
|
||||
{ type: 'string', value: 'value2', label: 'value2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('for getTagKeys', async () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
} from '@grafana/runtime';
|
||||
import { BarGaugeDisplayMode, TableCellDisplayMode, VariableFormatID } from '@grafana/schema';
|
||||
|
||||
import { getTagWithoutScope, interpolateFilters } from './SearchTraceQLEditor/utils';
|
||||
import { interpolateFilters } from './SearchTraceQLEditor/utils';
|
||||
import { TempoVariableQuery, TempoVariableQueryType } from './VariableQueryEditor';
|
||||
import { PrometheusDatasource, PromQuery } from './_importedDependencies/datasources/prometheus/types';
|
||||
import { TagLimitOptions } from './configuration/TagLimitSettings';
|
||||
@@ -206,33 +206,27 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
|
||||
await this.languageProvider.start(range, this.timeRangeForTags);
|
||||
|
||||
let options;
|
||||
try {
|
||||
// Retrieve the scope of the tag
|
||||
// Example: given `http.status_code`, we want scope `span`
|
||||
// Note that we ignore possible name clashes, e.g., `http.status_code` in both `span` and `resource`
|
||||
const scope: string | undefined = (this.languageProvider.tagsV2 || [])
|
||||
// flatten the Scope objects
|
||||
.flatMap((tagV2) => tagV2.tags.map((tag) => ({ scope: tagV2.name, name: tag })))
|
||||
// find associated scope
|
||||
.find((tag) => tag.name === labelName)?.scope;
|
||||
if (!scope) {
|
||||
throw Error(`Scope for tag ${labelName} not found`);
|
||||
}
|
||||
|
||||
// For V2, we need to send scope and tag name, e.g. `span.http.status_code`,
|
||||
// unless the tag has intrinsic scope
|
||||
const scopeAndTag = scope === 'intrinsic' ? labelName : `${scope}.${labelName}`;
|
||||
options = await this.languageProvider.getOptionsV2({
|
||||
tag: scopeAndTag,
|
||||
timeRangeForTags: this.timeRangeForTags,
|
||||
range,
|
||||
});
|
||||
} catch {
|
||||
// For V1, the tag name (e.g. `http.status_code`) is enough
|
||||
options = await this.languageProvider.getOptionsV1(labelName);
|
||||
// Retrieve the scope of the tag
|
||||
// Example: given `http.status_code`, we want scope `span`
|
||||
// Note that we ignore possible name clashes, e.g., `http.status_code` in both `span` and `resource`
|
||||
const scope: string | undefined = (this.languageProvider.tagsV2 || [])
|
||||
// flatten the Scope objects
|
||||
.flatMap((tagV2) => tagV2.tags.map((tag) => ({ scope: tagV2.name, name: tag })))
|
||||
// find associated scope
|
||||
.find((tag) => tag.name === labelName)?.scope;
|
||||
if (!scope) {
|
||||
throw Error(`Scope for tag ${labelName} not found`);
|
||||
}
|
||||
|
||||
// For V2, we need to send scope and tag name, e.g. `span.http.status_code`,
|
||||
// unless the tag has intrinsic scope
|
||||
const scopeAndTag = scope === 'intrinsic' ? labelName : `${scope}.${labelName}`;
|
||||
const options = await this.languageProvider.getOptionsV2({
|
||||
tag: scopeAndTag,
|
||||
timeRangeForTags: this.timeRangeForTags,
|
||||
range,
|
||||
});
|
||||
|
||||
return options.flatMap((option: SelectableValue<string>) =>
|
||||
option.value !== undefined ? [{ text: option.value }] : []
|
||||
);
|
||||
@@ -257,20 +251,14 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
}
|
||||
|
||||
async tagValuesQuery(tag: string, query: string, range?: TimeRange): Promise<Array<{ text: string }>> {
|
||||
let options;
|
||||
try {
|
||||
// For V2, we need to send scope and tag name, e.g. `span.http.status_code`,
|
||||
// unless the tag has intrinsic scope
|
||||
options = await this.languageProvider.getOptionsV2({
|
||||
tag,
|
||||
query,
|
||||
timeRangeForTags: this.timeRangeForTags,
|
||||
range,
|
||||
});
|
||||
} catch {
|
||||
// For V1, the tag name (e.g. `http.status_code`) is enough
|
||||
options = await this.languageProvider.getOptionsV1(getTagWithoutScope(tag));
|
||||
}
|
||||
// For V2, we need to send scope and tag name, e.g. `span.http.status_code`,
|
||||
// unless the tag has intrinsic scope
|
||||
const options = await this.languageProvider.getOptionsV2({
|
||||
tag,
|
||||
query,
|
||||
timeRangeForTags: this.timeRangeForTags,
|
||||
range,
|
||||
});
|
||||
|
||||
return options.flatMap((option: SelectableValue<string>) =>
|
||||
option.value !== undefined ? [{ text: option.value }] : []
|
||||
@@ -912,7 +900,14 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
|
||||
}
|
||||
|
||||
async metadataRequest(url: string, params = {}) {
|
||||
return await lastValueFrom(this._request(url, params, { method: 'GET', hideFromInspector: true }));
|
||||
// url must not start with a `/`, otherwise the AJAX-request
|
||||
// going from the browser will contain `//`, which can cause problems.
|
||||
if (url.startsWith('/')) {
|
||||
throw new Error(`invalid metadata request url: ${url}`);
|
||||
}
|
||||
|
||||
const res = await this.getResource(url, params, { method: 'GET', hideFromInspector: true });
|
||||
return res?.data ?? res;
|
||||
}
|
||||
|
||||
_request(apiUrl: string, data?: unknown, options?: Partial<BackendSrvRequest>): Observable<Record<string, any>> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import { v1Tags, v2Tags } from './SearchTraceQLEditor/mocks';
|
||||
import { v2Tags } from './SearchTraceQLEditor/mocks';
|
||||
import { TraceqlSearchScope } from './dataquery.gen';
|
||||
import { TempoDatasource } from './datasource';
|
||||
import TempoLanguageProvider from './language_provider';
|
||||
@@ -9,72 +9,54 @@ import { Scope } from './types';
|
||||
|
||||
describe('Language_provider', () => {
|
||||
describe('should get correct tags', () => {
|
||||
it('for API v1 tags', async () => {
|
||||
const lp = setup(v1Tags);
|
||||
const tags = lp.getTags();
|
||||
expect(tags).toEqual(['bar', 'foo', 'status']);
|
||||
});
|
||||
|
||||
it('for API v2 resource tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getTags(TraceqlSearchScope.Resource);
|
||||
expect(tags).toEqual(['cluster', 'container']);
|
||||
});
|
||||
|
||||
it('for API v2 span tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getTags(TraceqlSearchScope.Span);
|
||||
expect(tags).toEqual(['db']);
|
||||
});
|
||||
|
||||
it('for API v2 unscoped tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getTags(TraceqlSearchScope.Unscoped);
|
||||
expect(tags).toEqual(['cluster', 'container', 'db']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get correct traceql autocomplete tags', () => {
|
||||
it('for API v1 tags', async () => {
|
||||
const lp = setup(v1Tags);
|
||||
const tags = lp.getTraceqlAutocompleteTags();
|
||||
expect(tags).toEqual(['bar', 'foo', 'status']);
|
||||
});
|
||||
|
||||
it('for API v2 resource tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getTraceqlAutocompleteTags(TraceqlSearchScope.Resource);
|
||||
expect(tags).toEqual(['cluster', 'container']);
|
||||
});
|
||||
|
||||
it('for API v2 span tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getTraceqlAutocompleteTags(TraceqlSearchScope.Span);
|
||||
expect(tags).toEqual(['db']);
|
||||
});
|
||||
|
||||
it('for API v2 unscoped tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getTraceqlAutocompleteTags(TraceqlSearchScope.Unscoped);
|
||||
expect(tags).toEqual(['cluster', 'container', 'db']);
|
||||
});
|
||||
|
||||
it('for API v2 tags with no scope', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getTraceqlAutocompleteTags();
|
||||
expect(tags).toEqual(['cluster', 'container', 'db']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get correct autocomplete tags', () => {
|
||||
it('for API v1 tags', async () => {
|
||||
const lp = setup(v1Tags);
|
||||
const tags = lp.getAutocompleteTags();
|
||||
expect(tags).toEqual(['bar', 'foo', 'status', 'status.code']);
|
||||
});
|
||||
|
||||
it('for API v2 tags', async () => {
|
||||
const lp = setup(undefined, v2Tags);
|
||||
const lp = setup(v2Tags);
|
||||
const tags = lp.getAutocompleteTags();
|
||||
expect(tags).toEqual(
|
||||
uniq(['cluster', 'container', 'db', 'duration', 'kind', 'name', 'status'].concat(intrinsics))
|
||||
@@ -85,7 +67,7 @@ describe('Language_provider', () => {
|
||||
describe('generateQueryFromFilters generates the correct query for', () => {
|
||||
let lp: TempoLanguageProvider;
|
||||
beforeEach(() => {
|
||||
lp = setup(v1Tags);
|
||||
lp = setup(v2Tags);
|
||||
});
|
||||
|
||||
it('an empty array', () => {
|
||||
@@ -313,7 +295,7 @@ describe('Language_provider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const setup = (tagsV1?: string[], tagsV2?: Scope[]) => {
|
||||
const setup = (tagsV2?: Scope[]) => {
|
||||
const datasource: TempoDatasource = {
|
||||
search: {
|
||||
filters: [],
|
||||
@@ -321,9 +303,7 @@ describe('Language_provider', () => {
|
||||
} as unknown as TempoDatasource;
|
||||
|
||||
const lp = new TempoLanguageProvider(datasource);
|
||||
if (tagsV1) {
|
||||
lp.setV1Tags(tagsV1);
|
||||
} else if (tagsV2) {
|
||||
if (tagsV2) {
|
||||
lp.setV2Tags(tagsV2);
|
||||
}
|
||||
datasource.languageProvider = lp;
|
||||
|
||||
@@ -30,7 +30,6 @@ interface GetOptionsV2 {
|
||||
|
||||
export default class TempoLanguageProvider extends LanguageProvider {
|
||||
datasource: TempoDatasource;
|
||||
tagsV1?: string[];
|
||||
tagsV2?: Scope[];
|
||||
private previousRange?: TimeRange;
|
||||
|
||||
@@ -41,8 +40,7 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
|
||||
request = async (url: string, params = {}) => {
|
||||
const res = await this.datasource.metadataRequest(url, params);
|
||||
return res?.data;
|
||||
return await this.datasource.metadataRequest(url, params);
|
||||
};
|
||||
|
||||
start = async (range?: TimeRange, timeRangeForTags?: number) => {
|
||||
@@ -85,33 +83,21 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
};
|
||||
|
||||
async fetchTags(timeRangeForTags?: number, range?: TimeRange) {
|
||||
let v1Resp, v2Resp;
|
||||
|
||||
try {
|
||||
const params: { limit: number; start?: number; end?: number } = {
|
||||
limit: this.getTagsLimit(),
|
||||
};
|
||||
if (timeRangeForTags && range && timeRangeForTags !== DEFAULT_TIME_RANGE_FOR_TAGS) {
|
||||
const { start, end } = this.getTimeRangeForTags(timeRangeForTags, range);
|
||||
params.start = start;
|
||||
params.end = end;
|
||||
}
|
||||
v2Resp = await this.request(`/api/v2/search/tags`, params);
|
||||
} catch (error) {
|
||||
v1Resp = await this.request('/api/search/tags', []);
|
||||
const params: { limit: number; start?: number; end?: number } = {
|
||||
limit: this.getTagsLimit(),
|
||||
};
|
||||
if (timeRangeForTags && range && timeRangeForTags !== DEFAULT_TIME_RANGE_FOR_TAGS) {
|
||||
const { start, end } = this.getTimeRangeForTags(timeRangeForTags, range);
|
||||
params.start = start;
|
||||
params.end = end;
|
||||
}
|
||||
const v2Resp = await this.request(`tags`, params);
|
||||
|
||||
if (v2Resp && v2Resp.scopes) {
|
||||
this.setV2Tags(v2Resp.scopes);
|
||||
} else if (v1Resp) {
|
||||
this.setV1Tags(v1Resp.tagNames);
|
||||
}
|
||||
}
|
||||
|
||||
setV1Tags = (tags: string[]) => {
|
||||
this.tagsV1 = tags;
|
||||
};
|
||||
|
||||
setV2Tags = (tags: Scope[]) => {
|
||||
this.tagsV2 = tags;
|
||||
};
|
||||
@@ -129,13 +115,6 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
return getUnscopedTags(this.tagsV2);
|
||||
}
|
||||
return getTagsByScope(this.tagsV2, scope);
|
||||
} else if (this.tagsV1) {
|
||||
// This is needed because the /api/v2/search/tag/${tag}/values API expects "status" and the v1 API expects "status.code"
|
||||
// so Tempo doesn't send anything and we inject it here for the autocomplete
|
||||
if (!this.tagsV1.find((t) => t === 'status')) {
|
||||
this.tagsV1.push('status');
|
||||
}
|
||||
return this.tagsV1;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
@@ -149,13 +128,6 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
return getUnscopedTags(this.tagsV2);
|
||||
}
|
||||
return getTagsByScope(this.tagsV2, scope);
|
||||
} else if (this.tagsV1) {
|
||||
// This is needed because the /api/v2/search/tag/${tag}/values API expects "status" and the v1 API expects "status.code"
|
||||
// so Tempo doesn't send anything and we inject it here for the autocomplete
|
||||
if (!this.tagsV1.find((t) => t === 'status')) {
|
||||
this.tagsV1.push('status');
|
||||
}
|
||||
return this.tagsV1;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
@@ -163,33 +135,13 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
getAutocompleteTags = () => {
|
||||
if (this.tagsV2) {
|
||||
return getAllTags(this.tagsV2);
|
||||
} else if (this.tagsV1) {
|
||||
// This is needed because the /api/search/tag/${tag}/values API expects "status.code" and the v2 API expects "status"
|
||||
// so Tempo doesn't send anything and we inject it here for the autocomplete
|
||||
if (!this.tagsV1.find((t) => t === 'status.code')) {
|
||||
this.tagsV1.push('status.code');
|
||||
}
|
||||
return this.tagsV1;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
async getOptionsV1(tag: string): Promise<Array<SelectableValue<string>>> {
|
||||
const encodedTag = this.encodeTag(tag);
|
||||
const response = await this.request(`/api/search/tag/${encodedTag}/values`);
|
||||
let options: Array<SelectableValue<string>> = [];
|
||||
if (response && response.tagValues) {
|
||||
options = response.tagValues.map((v: string) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
}));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async getOptionsV2({ tag, query, timeRangeForTags, range }: GetOptionsV2): Promise<Array<SelectableValue<string>>> {
|
||||
const encodedTag = this.encodeTag(tag);
|
||||
const params: { q?: string; limit: number; start?: number; end?: number } = {
|
||||
const params: { q?: string; limit: number; start?: number; end?: number; tag?: string } = {
|
||||
limit: this.getTagsLimit(),
|
||||
};
|
||||
|
||||
@@ -203,7 +155,10 @@ export default class TempoLanguageProvider extends LanguageProvider {
|
||||
params.end = end;
|
||||
}
|
||||
|
||||
const response = await this.request(`/api/v2/search/tag/${encodedTag}/values`, params);
|
||||
// Add the encoded tag as a query parameter for the new resource endpoint
|
||||
params.tag = encodedTag;
|
||||
const response = await this.request(`tag-values`, params);
|
||||
|
||||
let options: Array<SelectableValue<string>> = [];
|
||||
if (response && response.tagValues) {
|
||||
response.tagValues.forEach((v: { type: string; value?: string }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataSourceInstanceSettings, PluginMetaInfo, PluginType } from '@grafana/data';
|
||||
import { monacoTypes } from '@grafana/ui';
|
||||
|
||||
import { v1Tags, v2Tags, emptyTags, testIntrinsics } from '../SearchTraceQLEditor/mocks';
|
||||
import { v2Tags, emptyTags, testIntrinsics } from '../SearchTraceQLEditor/mocks';
|
||||
import { TempoDatasource } from '../datasource';
|
||||
import TempoLanguageProvider from '../language_provider';
|
||||
import { Scope, TempoJsonData } from '../types';
|
||||
@@ -16,20 +16,8 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}));
|
||||
|
||||
describe('CompletionProvider', () => {
|
||||
it('suggests tags, intrinsics and scopes (API v1)', async () => {
|
||||
const { provider, model } = setup('{}', 1, v1Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: s })),
|
||||
...intrinsicsV1.map((s) => expect.objectContaining({ label: s, insertText: s })),
|
||||
expect.objectContaining({ label: 'bar', insertText: '.bar' }),
|
||||
expect.objectContaining({ label: 'foo', insertText: '.foo' }),
|
||||
expect.objectContaining({ label: 'status', insertText: '.status' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('suggests tags, intrinsics and scopes (API v2)', async () => {
|
||||
const { provider, model } = setup('{}', 1, undefined, v2Tags);
|
||||
const { provider, model } = setup('{}', 1, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: s })),
|
||||
@@ -41,7 +29,7 @@ describe('CompletionProvider', () => {
|
||||
});
|
||||
|
||||
it('does not wrap the tag value in quotes if the type in the response is something other than "string"', async () => {
|
||||
const { provider, model } = setup('{.foo=}', 6, v1Tags);
|
||||
const { provider, model } = setup('{.foo=}', 6, v2Tags);
|
||||
|
||||
jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation(
|
||||
() =>
|
||||
@@ -63,7 +51,7 @@ describe('CompletionProvider', () => {
|
||||
});
|
||||
|
||||
it('wraps the tag value in quotes if the type in the response is set to "string"', async () => {
|
||||
const { provider, model } = setup('{.foo=}', 6, v1Tags);
|
||||
const { provider, model } = setup('{.foo=}', 6, v2Tags);
|
||||
|
||||
jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation(
|
||||
() =>
|
||||
@@ -85,7 +73,7 @@ describe('CompletionProvider', () => {
|
||||
});
|
||||
|
||||
it('inserts the tag value without quotes if the user has entered quotes', async () => {
|
||||
const { provider, model } = setup('{.foo="}', 6, v1Tags);
|
||||
const { provider, model } = setup('{.foo="}', 6, v2Tags);
|
||||
|
||||
jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation(
|
||||
() =>
|
||||
@@ -106,7 +94,7 @@ describe('CompletionProvider', () => {
|
||||
});
|
||||
|
||||
it('suggests options when inside quotes', async () => {
|
||||
const { provider, model } = setup('{.foo=""}', 7, undefined, v2Tags);
|
||||
const { provider, model } = setup('{.foo=""}', 7, v2Tags);
|
||||
|
||||
jest.spyOn(provider.languageProvider, 'getOptionsV2').mockImplementation(
|
||||
() =>
|
||||
@@ -133,20 +121,8 @@ describe('CompletionProvider', () => {
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([]);
|
||||
});
|
||||
|
||||
it('suggests tags on empty input (API v1)', async () => {
|
||||
const { provider, model } = setup('', 0, v1Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })),
|
||||
...intrinsicsV1.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })),
|
||||
expect.objectContaining({ label: 'bar', insertText: '{ .bar' }),
|
||||
expect.objectContaining({ label: 'foo', insertText: '{ .foo' }),
|
||||
expect.objectContaining({ label: 'status', insertText: '{ .status' }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('suggests tags on empty input (API v2)', async () => {
|
||||
const { provider, model } = setup('', 0, undefined, v2Tags);
|
||||
const { provider, model } = setup('', 0, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...scopes.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })),
|
||||
@@ -157,32 +133,16 @@ describe('CompletionProvider', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('only suggests tags after typing the global attribute scope (API v1)', async () => {
|
||||
const { provider, model } = setup('{.}', 2, v1Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
v1Tags.map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
});
|
||||
|
||||
it('only suggests tags after typing the global attribute scope (API v2)', async () => {
|
||||
const { provider, model } = setup('{.}', 2, undefined, v2Tags);
|
||||
const { provider, model } = setup('{.}', 2, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
['cluster', 'container', 'db'].map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests tags after a scope (API v1)', async () => {
|
||||
const { provider, model } = setup('{ resource. }', 11, v1Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
v1Tags.map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests correct tags after the resource scope (API v2)', async () => {
|
||||
const { provider, model } = setup('{ resource. }', 11, undefined, v2Tags);
|
||||
const { provider, model } = setup('{ resource. }', 11, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
['cluster', 'container'].map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
@@ -190,7 +150,7 @@ describe('CompletionProvider', () => {
|
||||
});
|
||||
|
||||
it('suggests correct tags after the span scope (API v2)', async () => {
|
||||
const { provider, model } = setup('{ span. }', 7, undefined, v2Tags);
|
||||
const { provider, model } = setup('{ span. }', 7, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
['db'].map((s) => expect.objectContaining({ label: s, insertText: s }))
|
||||
@@ -198,7 +158,7 @@ describe('CompletionProvider', () => {
|
||||
});
|
||||
|
||||
it('suggests logical operators and close bracket after the value', async () => {
|
||||
const { provider, model } = setup('{.foo=300 }', 10, v1Tags);
|
||||
const { provider, model } = setup('{.foo=300 }', 10, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual(
|
||||
[...CompletionProvider.logicalOps, ...CompletionProvider.arithmeticOps, ...CompletionProvider.comparisonOps].map(
|
||||
@@ -224,7 +184,7 @@ describe('CompletionProvider', () => {
|
||||
])(
|
||||
'suggests operators that go after `|` (aggregators, selectorts, ...) - %s, %i',
|
||||
async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset, undefined, v2Tags);
|
||||
const { provider, model } = setup(input, offset, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
...CompletionProvider.functions.map((s) =>
|
||||
@@ -342,7 +302,7 @@ describe('CompletionProvider', () => {
|
||||
['{ span.d }', 8],
|
||||
['{ span.db }', 9],
|
||||
])('suggests to complete attribute - %s, %i', async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset, undefined, v2Tags);
|
||||
const { provider, model } = setup(input, offset, v2Tags);
|
||||
const result = await provider.provideCompletionItems(model, emptyPosition);
|
||||
expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([
|
||||
expect.objectContaining({ label: 'db', insertText: 'db' }),
|
||||
@@ -409,7 +369,7 @@ describe('CompletionProvider', () => {
|
||||
])(
|
||||
'suggests attributes when containing trigger characters and missing `}`- %s, %i',
|
||||
async (input: string, offset: number) => {
|
||||
const { provider, model } = setup(input, offset, undefined, [
|
||||
const { provider, model } = setup(input, offset, [
|
||||
{
|
||||
name: 'span',
|
||||
tags: ['http.status_code'],
|
||||
@@ -485,12 +445,10 @@ describe('CompletionProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function setup(value: string, offset: number, tagsV1?: string[], tagsV2?: Scope[]) {
|
||||
function setup(value: string, offset: number, tagsV2?: Scope[]) {
|
||||
const ds = new TempoDatasource(defaultSettings);
|
||||
const lp = new TempoLanguageProvider(ds);
|
||||
if (tagsV1) {
|
||||
lp.setV1Tags(tagsV1);
|
||||
} else if (tagsV2) {
|
||||
if (tagsV2) {
|
||||
lp.setV2Tags(tagsV2);
|
||||
}
|
||||
const provider = new CompletionProvider({ languageProvider: lp, setAlertText: () => {} });
|
||||
|
||||
@@ -3,7 +3,7 @@ import { lastValueFrom } from 'rxjs';
|
||||
import { DataQueryRequest, TimeRange } from '@grafana/data';
|
||||
|
||||
import { TempoVariableQuery } from './VariableQueryEditor';
|
||||
import { createMetadataRequest, createTempoDatasource } from './test/mocks';
|
||||
import { createTempoDatasource } from './test/mocks';
|
||||
import { TempoVariableSupport } from './variables';
|
||||
|
||||
describe('TempoVariableSupport', () => {
|
||||
@@ -11,14 +11,9 @@ describe('TempoVariableSupport', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const datasource = createTempoDatasource();
|
||||
jest.spyOn(datasource, 'metadataRequest').mockImplementation(
|
||||
createMetadataRequest({
|
||||
data: {
|
||||
tagNames: ['label1', 'label2'],
|
||||
scopes: [{ name: 'span', tags: ['label1', 'label2'] }],
|
||||
},
|
||||
})
|
||||
);
|
||||
// Mock the language provider to return v2 tags
|
||||
datasource.languageProvider.tagsV2 = [{ name: 'span', tags: ['label1', 'label2'] }];
|
||||
jest.spyOn(datasource.languageProvider, 'start').mockResolvedValue([]);
|
||||
TempoVariableSupportMock = new TempoVariableSupport(datasource);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user