import Prism, { Grammar } from 'prismjs'; import { Observable, of } from 'rxjs'; import { AbstractQuery, CoreApp, DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, ScopedVars, } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { VariableSupport } from './VariableSupport'; import { defaultGrafanaPyroscope, defaultPyroscopeQueryType } from './dataquery.gen'; import { PyroscopeDataSourceOptions, Query, ProfileTypeMessage } from './types'; import { extractLabelMatchers, toPromLikeExpr } from './utils'; export class PyroscopeDataSource extends DataSourceWithBackend { constructor( instanceSettings: DataSourceInstanceSettings, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); this.variables = new VariableSupport(this); } query(request: DataQueryRequest): Observable { const validTargets = request.targets .filter((t) => t.profileTypeId) .map((t) => { // Empty string errors out but honestly seems like we can just normalize it this way if (t.labelSelector === '') { return { ...t, labelSelector: '{}', }; } return normalizeQuery(t, request.app); }); if (!validTargets.length) { return of({ data: [] }); } return super.query({ ...request, targets: validTargets, }); } async getProfileTypes(start: number, end: number): Promise { return await this.getResource('profileTypes', { start, end, }); } async getAllProfileTypes(): Promise { return await this.getResource('profileTypes'); } async getLabelNames(query: string, start: number, end: number): Promise { return await this.getResource('labelNames', { query: this.templateSrv.replace(query), start, end }); } async getLabelValues(query: string, label: string, start: number, end: number): Promise { return await this.getResource('labelValues', { label: this.templateSrv.replace(label), query: this.templateSrv.replace(query), start, end, }); } applyTemplateVariables(query: Query, scopedVars: ScopedVars): Query { return { ...query, labelSelector: this.templateSrv.replace(query.labelSelector ?? '', scopedVars), profileTypeId: this.templateSrv.replace(query.profileTypeId ?? '', scopedVars), }; } async importFromAbstractQueries(abstractQueries: AbstractQuery[]): Promise { return abstractQueries.map((abstractQuery) => this.importFromAbstractQuery(abstractQuery)); } importFromAbstractQuery(labelBasedQuery: AbstractQuery): Query { return { refId: labelBasedQuery.refId, labelSelector: toPromLikeExpr(labelBasedQuery), queryType: 'both', profileTypeId: '', groupBy: [], }; } async exportToAbstractQueries(queries: Query[]): Promise { return queries.map((query) => this.exportToAbstractQuery(query)); } exportToAbstractQuery(query: Query): AbstractQuery { const pyroscopeQuery = query.labelSelector; if (!pyroscopeQuery || pyroscopeQuery.length === 0) { return { refId: query.refId, labelMatchers: [] }; } const tokens = Prism.tokenize(pyroscopeQuery, grammar); return { refId: query.refId, labelMatchers: extractLabelMatchers(tokens), }; } getDefaultQuery(app: CoreApp): Partial { return defaultQuery; } } export const defaultQuery: Partial = { ...defaultGrafanaPyroscope, queryType: defaultPyroscopeQueryType, }; export function normalizeQuery(query: Query, app?: CoreApp | string) { let normalized = { ...defaultQuery, ...query }; if (app !== CoreApp.Explore && normalized.queryType === 'both') { // In dashboards and other places, we can't show both types of graphs at the same time. // This will also be a default when having 'both' query and adding it from explore to dashboard normalized.queryType = 'profile'; } return normalized; } const grammar: Grammar = { 'context-labels': { pattern: /\{[^}]*(?=}?)/, greedy: true, inside: { comment: { pattern: /#.*/, }, 'label-key': { pattern: /[a-zA-Z_]\w*(?=\s*(=|!=|=~|!~))/, alias: 'attr-name', greedy: true, }, 'label-value': { pattern: /"(?:\\.|[^\\"])*"/, greedy: true, alias: 'attr-value', }, punctuation: /[{]/, }, }, punctuation: /[{}(),.]/, };