Created
March 18, 2026 15:21
-
-
Save matyax/32db0e35eb9f7aedcabdfe28e6d99cfc to your computer and use it in GitHub Desktop.
Shareable Grafana Logs Drilldown URL builder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Imports are relative to the Logs Drilldown project, but every imported constant and type can be replaced with | |
| // the corresponding string. | |
| import { UrlQueryMap, urlUtil } from '@grafana/data'; | |
| import { AdHocVariableFilter } from '@grafana/data'; | |
| import { AdHocFilterWithLabels } from '@grafana/scenes'; | |
| import { PageSlugs } from './enums'; | |
| import { replaceSlash } from './extensions/links'; | |
| import { isFilterMetadata } from './filters'; | |
| import { VAR_DATASOURCE, VAR_FIELDS, VAR_LABELS, VAR_LEVELS, VAR_LINE_FILTERS, VAR_METADATA } from './variables'; | |
| import { prefixRoute } from './plugin'; | |
| import { | |
| addAdHocFilterUserInputPrefix, | |
| AdHocFiltersWithLabelsAndMeta, | |
| EMPTY_VARIABLE_VALUE, | |
| stripAdHocFilterUserInputPrefix, | |
| } from './variables'; | |
| /** | |
| * Options for building a shareable drilldown URL from current filters. | |
| */ | |
| export interface DrilldownShareUrlOptions { | |
| /** Primary label name (e.g. "service" or "service_name") */ | |
| labelName: string; | |
| /** Primary label value (e.g. service name) */ | |
| labelValue: string; | |
| /** Drilldown page path. Defaults to "logs" */ | |
| path?: PageSlugs; | |
| /** Label/stream selector filters */ | |
| labelFilters?: AdHocVariableFilter[]; | |
| /** Field filters (parsed fields and/or structured metadata). Stored as var-fields and var-metadata. */ | |
| fieldFilters?: AdHocFiltersWithLabelsAndMeta[]; | |
| /** Level filters (e.g. detected_level) */ | |
| levelFilters?: AdHocVariableFilter[]; | |
| /** Line filters (match / regex on log line) */ | |
| lineFilters?: AdHocFilterWithLabels[]; | |
| /** Time range start (e.g. "now-1h" or epoch ms) */ | |
| from?: string; | |
| /** Time range end (e.g. "now" or epoch ms) */ | |
| to?: string; | |
| /** Datasource UID */ | |
| datasourceUid?: string; | |
| } | |
| const VAR_LABELS_KEY = `var-${VAR_LABELS}`; | |
| const VAR_FIELDS_KEY = `var-${VAR_FIELDS}`; | |
| const VAR_METADATA_KEY = `var-${VAR_METADATA}`; | |
| const VAR_LEVELS_KEY = `var-${VAR_LEVELS}`; | |
| const VAR_LINE_FILTERS_KEY = `var-${VAR_LINE_FILTERS}`; | |
| const VAR_DATASOURCE_KEY = `var-${VAR_DATASOURCE}`; | |
| function escapeUrlPipe(value: string | undefined): string { | |
| if (value == null) return ''; | |
| return value.replace(/\|/g, '__gfp__'); | |
| } | |
| function escapeUrlComma(value: string | undefined): string { | |
| if (value == null) return ''; | |
| return value.replace(/,/g, '__gfc__'); | |
| } | |
| function escapeUrlDelimiters(value: string | undefined): string { | |
| return escapeUrlComma(escapeUrlPipe(value)); | |
| } | |
| function stringifyValue(value: string | undefined): string { | |
| return value ?? EMPTY_VARIABLE_VALUE; | |
| } | |
| function labelFilterToUrlSegment(filter: AdHocVariableFilter & { valueLabels?: string[] }): string { | |
| const value = filter.value ?? ''; | |
| const valueLabel = filter.valueLabels?.[0]; | |
| const useCustomPrefix = valueLabel != null && valueLabel !== value; | |
| const serializedValue = useCustomPrefix | |
| ? addAdHocFilterUserInputPrefix(value) | |
| : stripAdHocFilterUserInputPrefix(value); | |
| const escaped = escapeUrlDelimiters(serializedValue); | |
| const labelPart = valueLabel != null ? `,${escapeUrlDelimiters(valueLabel)}` : ''; | |
| return `${filter.key}|${filter.operator}|${escaped}${labelPart}`; | |
| } | |
| function fieldFilterToUrlSegment(filter: AdHocFiltersWithLabelsAndMeta): string { | |
| const value = filter.value ?? ''; | |
| const valueLabel = filter.valueLabels?.[0]; | |
| if (isFilterMetadata(filter)) { | |
| const serializedValue = value ? addAdHocFilterUserInputPrefix(value) : value; | |
| return `${filter.key}|${filter.operator}|${escapeUrlDelimiters(serializedValue)},${escapeUrlDelimiters(valueLabel ?? value)}`; | |
| } | |
| const fieldValue = JSON.stringify({ | |
| parser: filter.meta?.parser ?? 'mixed', | |
| value: stripAdHocFilterUserInputPrefix(value) || value, | |
| }); | |
| const serializedValue = value ? addAdHocFilterUserInputPrefix(fieldValue) : fieldValue; | |
| return `${filter.key}|${filter.operator}|${escapeUrlDelimiters(serializedValue)},${escapeUrlDelimiters(valueLabel ?? value)}`; | |
| } | |
| function levelFilterToUrlSegment(filter: AdHocVariableFilter): string { | |
| const value = stringifyValue(filter.value); | |
| return `${filter.key}|${filter.operator}|${escapeUrlDelimiters(value)}`; | |
| } | |
| function lineFilterToUrlSegment(filter: AdHocFilterWithLabels): string { | |
| const value = stringifyValue(filter.value); | |
| return `${filter.key}|${escapeUrlDelimiters(filter.operator)}|${escapeUrlDelimiters(value)}`; | |
| } | |
| /** | |
| * Builds a drilldown URL that can be shared. The URL includes path and all given filters | |
| * as query parameters so that opening it restores the same view. | |
| * | |
| * @param options - Label identity, page path, and all filter arrays | |
| * @returns Full URL string (path + query) for the drilldown app | |
| */ | |
| export function createDrilldownShareUrl(options: DrilldownShareUrlOptions): string { | |
| const { | |
| labelName, | |
| labelValue, | |
| path = PageSlugs.logs, | |
| labelFilters = [], | |
| fieldFilters = [], | |
| levelFilters = [], | |
| lineFilters = [], | |
| from, | |
| to, | |
| datasourceUid, | |
| } = options; | |
| const pathSegment = `${PageSlugs.explore}/${labelName}/${replaceSlash(labelValue)}/${path}`; | |
| const fullPath = prefixRoute(pathSegment); | |
| const params: UrlQueryMap = {}; | |
| if (from != null) params['from'] = from; | |
| if (to != null) params['to'] = to; | |
| if (datasourceUid != null) params[VAR_DATASOURCE_KEY] = datasourceUid; | |
| if (labelFilters.length > 0) { | |
| params[VAR_LABELS_KEY] = labelFilters.map(labelFilterToUrlSegment); | |
| } | |
| const metadataFilters = fieldFilters.filter((f) => isFilterMetadata(f)); | |
| const parsedFieldFilters = fieldFilters.filter((f) => !isFilterMetadata(f)); | |
| if (parsedFieldFilters.length > 0) { | |
| params[VAR_FIELDS_KEY] = parsedFieldFilters.map(fieldFilterToUrlSegment); | |
| } | |
| if (metadataFilters.length > 0) { | |
| params[VAR_METADATA_KEY] = metadataFilters.map(fieldFilterToUrlSegment); | |
| } | |
| if (levelFilters.length > 0) { | |
| params[VAR_LEVELS_KEY] = levelFilters.map(levelFilterToUrlSegment); | |
| } | |
| if (lineFilters.length > 0) { | |
| params[VAR_LINE_FILTERS_KEY] = lineFilters.map(lineFilterToUrlSegment); | |
| } | |
| return urlUtil.renderUrl(fullPath, params); | |
| } | |
| // Usage: | |
| createDrilldownShareUrl({ | |
| labelName: 'service', | |
| labelValue: 'my-service', | |
| path: PageSlugs.fields, | |
| labelFilters: [ | |
| { key: 'service_name', operator: '=', value: 'my-service' }, | |
| { key: 'env', operator: '=~', value: 'staging|prod', valueLabels: ['staging|prod'] }, | |
| ], | |
| fieldFilters: [ | |
| { | |
| key: 'status', | |
| operator: '=', | |
| value: JSON.stringify({ parser: 'logfmt', value: '200' }), | |
| valueLabels: ['200'], | |
| }, | |
| { | |
| key: 'level', | |
| operator: '=', | |
| value: 'error', | |
| valueLabels: ['error'], | |
| meta: { parser: 'structuredMetadata' }, | |
| }, | |
| ], | |
| levelFilters: [ | |
| { key: 'detected_level', operator: '=', value: 'error' }, | |
| { key: 'detected_level', operator: '=~', value: 'warn__gfp__info' }, | |
| ], | |
| lineFilters: [ | |
| { | |
| key: LineFilterCaseSensitive.caseInsensitive, | |
| operator: LineFilterOp.regex, | |
| value: '(?i)error', | |
| }, | |
| { | |
| key: LineFilterCaseSensitive.caseSensitive, | |
| operator: LineFilterOp.match, | |
| value: 'exact', | |
| }, | |
| ], | |
| from: 'now-3h', | |
| to: 'now', | |
| datasourceUid: 'gdev-loki', | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment