How to use template variables in your data source

Template variables enable users to create dashboards that change dynamically based on user input. Since variables have been around in Grafana for a long time, many users expect them to be supported for any data sources they install. In this post, I’ll share some tips on how you can use template variables in your data source plugin.

First off, to interpolate template variables, you need to import the getTemplateSrv() function from the @grafana/runtime package:

import { getTemplateSrv } from '@grafana/runtime'; 

The getTemplateSrv() function returns an instance of TemplateSrv which provides methods for working with template variables. The most important one, replace(), accepts a string containing variables as input and returns an interpolated string, where the variables have been replaced with the values the users have selected.

For example, if you have a variable called instance, the following code replaces the variable with its corresponding value:

getTemplateSrv().replace("I'd like $instance, please!");

// I'd like server-1, please!

The replace() even handles built-in variables such as $__from and $__to.

And that’s it! For most use cases, that’s all you need to do to add support for variables in your data source. Note that it’s up to you to decide which fields that should support variables. For example, to interpolate a single property, rawQuery, in your query, add the following:

const interpolatedQuery: MyQuery = {
  ...query,
  rawQuery: getTemplateSrv().replace(query.rawQuery),
};

Format multi-value variables

In the previous example, the variable only had one value, server-1. However, if the user instead creates a multi-value variable, it can hold multiple values at the same time. Multi-value variables pose a new challenge: How do you decide how to format a collection of values?

For example, which of these different formats would suit your use case?

  • {server-1, server-2, server-3} (Graphite)
  • ["server-1", "server-2", "server-3"] (JSON)
  • ("server-1" OR "server-2" OR "server-3") (Lucene)

Fortunately, the replace() method lets you pass a third argument to allow you to choose from a set of predefined formats, such as the csv format:

getTemplateSrv().replace("I'd like $instance, please!", {}, "csv");

// I'd like server-1, server-2, server-3, please!

Note: The second argument to the replace() method lets you configure sets of custom variables, or scoped variables, to include when interpolating the string. Unless this interests you, feel free to pass an empty object, {}.

Grafana supports a range of format options. To browse the available formats, check out Advanced variable format options.

Format variables using interpolation functions

“But Grafana doesn’t have a format option for my query language”, you might exclaim. Lucky for you, Grafana gives you full control of how replace() formats variables.

Instead of using a string as the third argument, you can pass an interpolation function. The following example uses a custom formatter function to add an and before the last element:

const formatter = (value: string | string[]): string => {
  if (typeof value == 'string') {
    return value;
  }
  
  // Add 'and' before the last element.
  if (value.length > 1) {
    return value.slice(0, -1).join(', ') + ' and ' + value[value.length - 1];
  }

  return value[0];
};

getTemplateSrv().replace("I'd like $instance, please!", {}, formatter);

// I'd like server-1, server-2, and server-3, please!

The argument to the function can be a string or an array of strings (string | string[]) depending on whether the variable supports multiple values, so make sure to check the type of the value before you use it.

Backend data sources

Plugins can only access template variables in the browser. Since backend data sources run their queries on the server, this means that you need to interpolate variables before they are sent to the backend.

To interpolate templated properties in a backend query:

  1. Override the applyTemplateVariables() method from the DataSourceWithBackend class.
  2. Interpolate any properties in the query (using replace()) that may contain variables, and then return the modified query.
export class DataSource extends DataSourceWithBackend<MyQuery, MyDataSourceOptions> {
  constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
    super(instanceSettings);
  }

  applyTemplateVariables(query: MyQuery, scopedVars: ScopedVars): Record<string, any> {
    const interpolatedQuery: MyQuery = {
      ...query,
      rawQuery: getTemplateSrv().replace(rawQuery, scopedVars),
    };

    return interpolatedQuery;
  }
}

Note: Because variables can only be interpolated in the browser, be aware that any alerting queries that contain template variables can result in undefined behavior.

Bonus: Using variables outside of templates

Finally, I want to show you a snippet that lets you use variables and their values outside of templates, for example to validate the number of selected values, or to add them to a drop-down.

Please note that this section is just my own shenanigans I’ve discovered after playing with the APIs :grimacing: That being said, I’ve found them quite useful, and I’m curious to hear what you think!

This helper function (ab)uses the replace() method to return the values as an array:

function getValuesForVariable(name: string): string[] {
  const values: string[] = [];

  // Instead of interpolating the string, we collect the values in an array.
  getTemplateSrv().replace(`$${name}`, {}, (value: string | string[]) => {
    if (Array.isArray(value)) {
      values.push(...value);
    } else {
      values.push(value);
    }

    // We don't really care about the string here.
    return '';
  });

  return values;
}
const instances = getValuesForVariable("instance");

for (var instance of instances) {
  console.log(instance);
}

// server-1 
// server-2
// server-3

Note: Another option would be to split the interpolated string based on a predictable delimiter (format option). In this snippet, I went for what I believe is a more robust solution, but feel free to adapt it to what makes sense to you!

You can even go a step further and create an object that neatly contains all variables and their values:

function getAllVariables(): Record<string, string[]> {
  const entries = getTemplateSrv()
    .getVariables()
    .map((v) => [v.name, getValuesForVariable(v.name)]);

  return Object.fromEntries(entries);
}
const vars = getAllVariables();

console.log(vars.instance);

// ["server-1", "server-2", "server-3"]

Note: Use getTemplateSrv().getVariables() to list all configured variables for the current dashboard.

Learn more about template variables

Template variables supercharge your data source and let your users create dynamic dashboards to drill down into the data that matters to them in the moment.

To learn more about how to use template variables in your plugins, refer to Add support for variables in plugins.

How are you using template variables in your data source? Reply to this post to share insights or code snippets!

2 Likes