Configure custom Grafana plugins for specialized monitoring requirements

Advanced 45 min Apr 23, 2026
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Build custom Grafana data source and panel plugins from scratch, then deploy them securely in production environments with proper authentication and access controls.

Prerequisites

  • Existing Grafana installation
  • Node.js 18+ development environment
  • Go 1.19+ for backend plugins
  • Basic React and TypeScript knowledge

What this solves

Standard Grafana plugins cover most monitoring scenarios, but specialized environments often need custom data sources or unique visualizations. This tutorial walks you through creating custom Grafana plugins for proprietary systems, internal APIs, or specialized visualization requirements that aren't available in the community plugin catalog.

Prerequisites and environment setup

Install Node.js development environment

Grafana plugins require Node.js 18+ and specific development tools for building and packaging.

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs git
npm install -g yarn @grafana/toolkit
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs npm git
npm install -g yarn @grafana/toolkit

Set up Grafana development instance

Install a local Grafana instance for plugin development and testing.

sudo apt-get install -y apt-transport-https software-properties-common wget
sudo mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install grafana
sudo tee /etc/yum.repos.d/grafana.repo <

Enable development mode

Configure Grafana for plugin development with unsigned plugins enabled and proper permissions.

[paths]
plugins = /var/lib/grafana/plugins

[plugins]
allow_loading_unsigned_plugins = true
enable_alpha = true

[security]
allow_embedding = true

[development]
mode = true
sudo systemctl enable --now grafana-server
sudo systemctl status grafana-server

Creating a custom data source plugin

Initialize the data source plugin project

Use the Grafana toolkit to scaffold a new data source plugin with proper TypeScript structure.

mkdir -p ~/grafana-plugins
cd ~/grafana-plugins
npx @grafana/create-plugin@latest custom-api-datasource
cd custom-api-datasource

Configure plugin metadata

Define the plugin information and capabilities in the plugin.json manifest file.

{
  "type": "datasource",
  "name": "Custom API DataSource",
  "id": "custom-api-datasource",
  "category": "cloud",
  "info": {
    "description": "Custom data source for proprietary API endpoints",
    "author": {
      "name": "Your Organization"
    },
    "version": "1.0.0",
    "updated": "2024-01-01"
  },
  "includes": [
    {
      "type": "datasource",
      "name": "Custom API"
    }
  ],
  "metrics": true,
  "annotations": false,
  "logs": false,
  "alerting": true,
  "backend": true,
  "executable": "gpx_custom-api-datasource"
}

Implement the data source backend

Create the Go backend that handles data queries and authentication with your custom API.

package plugin

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/data"
)

type Datasource struct {
	settings backend.DataSourceInstanceSettings
	httpClient *http.Client
}

func NewDatasource(settings backend.DataSourceInstanceSettings) (backend.DataSourcePlugin, error) {
	return &Datasource{
		settings: settings,
		httpClient: &http.Client{Timeout: 30 * time.Second},
	}, nil
}

func (d Datasource) QueryData(ctx context.Context, req backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
	response := backend.NewQueryDataResponse()

	for _, q := range req.Queries {
		res := d.query(ctx, req.PluginContext, q)
		response.Responses[q.RefID] = res
	}

	return response, nil
}

func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {
	var qm QueryModel
	if err := json.Unmarshal(query.JSON, &qm); err != nil {
		return backend.ErrDataResponse(backend.StatusBadRequest, fmt.Sprintf("json unmarshal: %v", err.Error()))
	}

	// Create data frame for response
	frame := data.NewFrame("response")
	frame.Fields = append(frame.Fields,
		data.NewField("Time", nil, []time.Time{query.TimeRange.From, query.TimeRange.To}),
		data.NewField("Value", nil, []float64{42.0, 84.0}),
	)

	return backend.DataResponse{
		Frames: []*data.Frame{frame},
	}
}

Implement frontend configuration

Create the React components for configuring the data source in Grafana's UI.

import React, { ChangeEvent } from 'react';
import { InlineField, Input, SecretInput } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { CustomDataSourceOptions, CustomSecureJsonData } from './types';

interface Props extends DataSourcePluginOptionsEditorProps {}

export function ConfigEditor(props: Props) {
  const { onOptionsChange, options } = props;
  const { jsonData, secureJsonFields } = options;
  const secureJsonData = (options.secureJsonData || {}) as CustomSecureJsonData;

  const onAPIUrlChange = (event: ChangeEvent) => {
    const jsonData = {
      ...options.jsonData,
      apiUrl: event.target.value,
    };
    onOptionsChange({ ...options, jsonData });
  };

  const onAPIKeyChange = (event: ChangeEvent) => {
    onOptionsChange({
      ...options,
      secureJsonData: {
        apiKey: event.target.value,
      },
    });
  };

  const onResetAPIKey = () => {
    onOptionsChange({
      ...options,
      secureJsonFields: {
        ...options.secureJsonFields,
        apiKey: false,
      },
      secureJsonData: {
        ...options.secureJsonData,
        apiKey: '',
      },
    });
  };

  return (
    <>
      
        
      
      
        
      
    
  );
}

Create query editor component

Build the interface for users to configure queries when creating dashboards.

import React, { ChangeEvent } from 'react';
import { InlineField, Input, Select } from '@grafana/ui';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { DataSource } from './datasource';
import { CustomDataSourceOptions, CustomQuery } from './types';

type Props = QueryEditorProps;

const metricOptions: SelectableValue[] = [
  { label: 'CPU Usage', value: 'cpu_usage' },
  { label: 'Memory Usage', value: 'memory_usage' },
  { label: 'Disk I/O', value: 'disk_io' },
  { label: 'Network Traffic', value: 'network_traffic' },
];

export function QueryEditor({ query, onChange, onRunQuery }: Props) {
  const onMetricChange = (value: SelectableValue) => {
    onChange({ ...query, metric: value.value! });
    onRunQuery();
  };

  const onHostChange = (event: ChangeEvent) => {
    onChange({ ...query, host: event.target.value });
  };

  return (
    <>
      
        
      
    
  );
}

Creating a custom panel plugin

Initialize panel plugin project

Create a new panel plugin for specialized data visualization requirements.

cd ~/grafana-plugins
npx @grafana/create-plugin@latest custom-status-panel --pluginType=panel
cd custom-status-panel

Implement the panel component

Build a React component that renders your custom visualization using Grafana's data and theme APIs.

import React from 'react';
import { PanelProps } from '@grafana/data';
import { useStyles2, useTheme2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { StatusOptions } from 'types';

interface Props extends PanelProps {}

const getStyles = () => {
  return {
    wrapper: css`
      font-family: Open Sans;
      position: relative;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 100%;
    `,
    status: css`
      font-size: 48px;
      font-weight: bold;
      margin-bottom: 10px;
    `,
    value: css`
      font-size: 24px;
      opacity: 0.7;
    `,
    healthy: css`
      color: #73BF69;
    `,
    warning: css`
      color: #FF9830;
    `,
    critical: css`
      color: #F2495C;
    `,
  };
};

export const StatusPanel: React.FC = ({ options, data, width, height }) => {
  const theme = useTheme2();
  const styles = useStyles2(getStyles);

  // Get the latest data point
  const frame = data.series[0];
  if (!frame || !frame.fields.length) {
    return 
No data
; } const valueField = frame.fields.find(field => field.type === 'number'); if (!valueField || !valueField.values.length) { return
No numeric data
; } const currentValue = valueField.values.get(valueField.values.length - 1); // Determine status based on thresholds let status = 'HEALTHY'; let statusClass = styles.healthy; if (currentValue >= options.criticalThreshold) { status = 'CRITICAL'; statusClass = styles.critical; } else if (currentValue >= options.warningThreshold) { status = 'WARNING'; statusClass = styles.warning; } return (
${styles.status} ${statusClass}}> {status}
{currentValue.toFixed(2)} {options.unit}
); };

Add panel options editor

Create configuration options that users can adjust in the panel settings.

import { PanelPlugin } from '@grafana/data';
import { StatusOptions } from './types';
import { StatusPanel } from './components/StatusPanel';

export const plugin = new PanelPlugin(StatusPanel).setPanelOptions((builder) => {
  return builder
    .addNumberInput({
      path: 'warningThreshold',
      name: 'Warning threshold',
      description: 'Value at which the panel shows warning status',
      defaultValue: 75,
      settings: {
        placeholder: '75',
        min: 0,
        max: 100,
      },
    })
    .addNumberInput({
      path: 'criticalThreshold',
      name: 'Critical threshold',
      description: 'Value at which the panel shows critical status',
      defaultValue: 90,
      settings: {
        placeholder: '90',
        min: 0,
        max: 100,
      },
    })
    .addTextInput({
      path: 'unit',
      name: 'Unit',
      description: 'Unit to display with the value',
      defaultValue: '%',
      settings: {
        placeholder: '%',
      },
    });
});

Plugin security hardening and deployment

Build and package plugins

Compile both plugins for production deployment with proper optimization.

cd ~/grafana-plugins/custom-api-datasource
npm run build
npm run sign

cd ~/grafana-plugins/custom-status-panel
npm run build
npm run sign

Configure plugin security

Set up proper permissions and security policies for plugin installation. This involves understanding both Grafana RBAC configuration and how to properly secure custom plugin installations.

[plugins]
allow_loading_unsigned_plugins = custom-api-datasource,custom-status-panel
plugin_admin_enabled = true
plugin_admin_external_manage_enabled = false

[security]
cookie_secure = true
cookie_samesite = strict
strict_transport_security = true
strict_transport_security_max_age_seconds = 86400
strict_transport_security_preload = true

Deploy plugins to production

Install plugins in the proper directory with correct ownership and permissions.

sudo mkdir -p /var/lib/grafana/plugins
sudo cp -r ~/grafana-plugins/custom-api-datasource/dist /var/lib/grafana/plugins/custom-api-datasource
sudo cp -r ~/grafana-plugins/custom-status-panel/dist /var/lib/grafana/plugins/custom-status-panel
sudo chown -R grafana:grafana /var/lib/grafana/plugins
sudo chmod -R 755 /var/lib/grafana/plugins
Never use chmod 777. It gives every user on the system full access to your plugin files. The grafana user needs read/execute permissions, which 755 provides safely.

Configure plugin validation

Set up checksum validation and update mechanisms for production plugin management.

# Plugin integrity checks
[[servers]]
host = "127.0.0.1"
port = 389
use_ssl = false
start_tls = false
bind_dn = "cn=admin,dc=example,dc=com"
bind_password = "secure_password"
search_filter = "(cn=%s)"
search_base_dns = ["dc=example,dc=com"]

[servers.attributes]
name = "givenName"
surname = "sn"
username = "cn"
member_of = "memberOf"
email = "mail"

Plugin access control

[[servers.group_mappings]] group_dn = "cn=grafana-plugin-admins,ou=groups,dc=example,dc=com" org_role = "Admin" [[servers.group_mappings]] group_dn = "cn=grafana-users,ou=groups,dc=example,dc=com" org_role = "Viewer"

Restart and verify deployment

Restart Grafana and confirm plugins load correctly with proper security settings.

sudo systemctl restart grafana-server
sudo systemctl status grafana-server
sudo journalctl -u grafana-server -f --lines=50

Advanced plugin development patterns

Implement plugin testing

Add comprehensive testing for both frontend and backend plugin components.

import React from 'react';
import { render, screen } from '@testing-library/react';
import { StatusPanel } from '../components/StatusPanel';
import { PanelProps } from '@grafana/data';
import { StatusOptions } from '../types';

const mockProps: PanelProps = {
  data: {
    series: [
      {
        fields: [
          {
            name: 'value',
            type: 'number',
            values: { get: (index: number) => index === 0 ? 85 : 85 },
            length: 1,
          },
        ],
        length: 1,
      },
    ],
  } as any,
  options: {
    warningThreshold: 75,
    criticalThreshold: 90,
    unit: '%',
  },
  width: 200,
  height: 100,
} as any;

describe('StatusPanel', () => {
  it('renders warning status correctly', () => {
    render();
    expect(screen.getByText('WARNING')).toBeInTheDocument();
    expect(screen.getByText('85.00 %')).toBeInTheDocument();
  });
});

Add plugin logging and monitoring

Implement structured logging for plugin debugging and performance monitoring. This complements broader Grafana monitoring strategies by providing plugin-specific insights.

import (
	"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)

func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {
	logger := log.DefaultLogger
	logger.Info("Processing query", "refId", query.RefID, "timeRange", query.TimeRange)

	start := time.Now()
	defer func() {
		duration := time.Since(start)
		logger.Info("Query completed", "refId", query.RefID, "duration", duration)
	}()

	// Query implementation...
	return backend.DataResponse{Frames: frames}
}

Verify your setup

# Check plugin installation
curl -u admin:admin http://localhost:3000/api/plugins | jq '.[] | select(.id=="custom-api-datasource" or .id=="custom-status-panel")'

Verify plugin functionality

curl -X POST -u admin:admin -H "Content-Type: application/json" \ -d '{"datasource":{"type":"custom-api-datasource"}}' \ http://localhost:3000/api/ds/query

Check plugin logs

sudo journalctl -u grafana-server | grep -i plugin

Verify permissions

sudo find /var/lib/grafana/plugins -type f -exec ls -la {} \;

Common issues

SymptomCauseFix
Plugin not loadingIncorrect permissions or unsigned pluginFix ownership with chown grafana:grafana and verify allow_loading_unsigned_plugins
Frontend build errorsMissing dependencies or TypeScript issuesRun npm install and check tsconfig.json configuration
Backend compilation failsGo module issues or missing SDKRun go mod tidy and verify grafana-plugin-sdk-go version
Query returns no dataBackend authentication or API connectivityCheck API credentials and network connectivity from Grafana server
Panel renders blankData format mismatch or React errorsCheck browser console for errors and verify data frame structure

Next steps

Running this in production?

Want this handled for you? Building custom plugins is complex, but maintaining them across environments with proper security, testing, and updates is the harder operational challenge. See how we run infrastructure like this for European teams who need specialized monitoring without the operational overhead.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.