Skip to content

Instantly share code, notes, and snippets.

@matyax
Created March 18, 2026 15:21
Show Gist options
  • Select an option

  • Save matyax/32db0e35eb9f7aedcabdfe28e6d99cfc to your computer and use it in GitHub Desktop.

Select an option

Save matyax/32db0e35eb9f7aedcabdfe28e6d99cfc to your computer and use it in GitHub Desktop.
Shareable Grafana Logs Drilldown URL builder
// 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