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
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
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
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
| Symptom | Cause | Fix |
|---|---|---|
| Plugin not loading | Incorrect permissions or unsigned plugin | Fix ownership with chown grafana:grafana and verify allow_loading_unsigned_plugins |
| Frontend build errors | Missing dependencies or TypeScript issues | Run npm install and check tsconfig.json configuration |
| Backend compilation fails | Go module issues or missing SDK | Run go mod tidy and verify grafana-plugin-sdk-go version |
| Query returns no data | Backend authentication or API connectivity | Check API credentials and network connectivity from Grafana server |
| Panel renders blank | Data format mismatch or React errors | Check browser console for errors and verify data frame structure |
Next steps
- Set up Grafana high availability for production plugin deployment
- Configure advanced Grafana dashboards using your custom plugins
- Integrate with Prometheus monitoring stack for comprehensive observability
- Set up private plugin marketplace for organization-wide plugin distribution
- Implement automated plugin testing with CI/CD pipeline integration
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
GRAFANA_USER="grafana"
GRAFANA_GROUP="grafana"
GRAFANA_HOME="/usr/share/grafana"
PLUGIN_DIR=""
TEMP_DIR=""
# Usage
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Install and configure Grafana with custom plugin development environment
Options:
-h, --help Show this help message
-p, --plugin-name Custom plugin name (default: status-panel)
Examples:
$0
$0 -p my-custom-panel
EOF
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "Installation failed. Cleaning up..."
[ -n "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
if systemctl is-enabled grafana-server >/dev/null 2>&1; then
systemctl stop grafana-server || true
systemctl disable grafana-server || true
fi
fi
exit $exit_code
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
if [ "$EUID" -ne 0 ]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
log_error "curl is required but not installed"
exit 1
fi
}
# Detect distribution
detect_distro() {
if [ ! -f /etc/os-release ]; then
log_error "/etc/os-release not found. Cannot detect distribution."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
# Use yum for older versions
if ! command -v dnf >/dev/null 2>&1; then
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
fi
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="iptables"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected distribution: $ID"
}
# Install Node.js
install_nodejs() {
log_info "[1/6] Installing Node.js development environment..."
case "$PKG_MGR" in
apt)
$PKG_INSTALL apt-transport-https ca-certificates gnupg
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
$PKG_INSTALL nodejs git build-essential
;;
dnf|yum)
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
$PKG_INSTALL nodejs npm git gcc-c++ make
;;
esac
# Install yarn and Grafana toolkit
npm install -g yarn @grafana/toolkit
log_info "Node.js $(node --version) installed successfully"
}
# Install Grafana
install_grafana() {
log_info "[2/6] Installing Grafana..."
case "$PKG_MGR" in
apt)
$PKG_INSTALL apt-transport-https software-properties-common wget gpg
mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list
apt update
$PKG_INSTALL grafana
;;
dnf|yum)
cat > /etc/yum.repos.d/grafana.repo << 'EOF'
[grafana]
name=grafana
baseurl=https://rpm.grafana.com
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://rpm.grafana.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
EOF
$PKG_INSTALL grafana
;;
esac
# Set proper permissions
chown -R $GRAFANA_USER:$GRAFANA_GROUP /etc/grafana
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/lib/grafana
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana
}
# Configure Grafana for development
configure_grafana() {
log_info "[3/6] Configuring Grafana for plugin development..."
# Enable development mode in Grafana config
if grep -q "app_mode.*development" /etc/grafana/grafana.ini; then
sed -i 's/^;app_mode = production/app_mode = development/' /etc/grafana/grafana.ini
else
sed -i '/\[DEFAULT\]/a app_mode = development' /etc/grafana/grafana.ini
fi
# Allow unsigned plugins
if grep -q "allow_loading_unsigned_plugins" /etc/grafana/grafana.ini; then
sed -i 's/^;allow_loading_unsigned_plugins =/allow_loading_unsigned_plugins = true/' /etc/grafana/grafana.ini
else
sed -i '/\[plugins\]/a allow_loading_unsigned_plugins = true' /etc/grafana/grafana.ini
fi
# Set plugin directory
PLUGIN_DIR="/var/lib/grafana/plugins"
mkdir -p "$PLUGIN_DIR"
chown $GRAFANA_USER:$GRAFANA_GROUP "$PLUGIN_DIR"
chmod 755 "$PLUGIN_DIR"
}
# Create sample plugin
create_sample_plugin() {
log_info "[4/6] Creating sample custom plugin..."
TEMP_DIR="/tmp/grafana-plugin-dev"
mkdir -p "$TEMP_DIR"
cd "$TEMP_DIR"
# Initialize plugin using Grafana toolkit
npx @grafana/toolkit plugin:create "${PLUGIN_NAME}"
cd "${PLUGIN_NAME}"
# Build the plugin
npm install
npm run build
# Install plugin to Grafana
cp -r dist "${PLUGIN_DIR}/${PLUGIN_NAME}"
chown -R $GRAFANA_USER:$GRAFANA_GROUP "${PLUGIN_DIR}/${PLUGIN_NAME}"
chmod -R 755 "${PLUGIN_DIR}/${PLUGIN_NAME}"
log_info "Sample plugin '${PLUGIN_NAME}' created and installed"
}
# Configure firewall
configure_firewall() {
log_info "[5/6] Configuring firewall..."
case "$FIREWALL_CMD" in
ufw)
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; then
ufw allow 3000/tcp
log_info "UFW rule added for Grafana (port 3000)"
fi
;;
firewall-cmd)
if systemctl is-active firewalld >/dev/null 2>&1; then
firewall-cmd --permanent --add-port=3000/tcp
firewall-cmd --reload
log_info "Firewalld rule added for Grafana (port 3000)"
fi
;;
iptables)
if iptables -L >/dev/null 2>&1; then
iptables -I INPUT -p tcp --dport 3000 -j ACCEPT
service iptables save || true
log_info "Iptables rule added for Grafana (port 3000)"
fi
;;
esac
}
# Start services
start_services() {
log_info "[6/6] Starting Grafana service..."
systemctl daemon-reload
systemctl enable grafana-server
systemctl start grafana-server
# Wait for Grafana to start
local count=0
while ! curl -s http://localhost:3000 >/dev/null 2>&1; do
sleep 2
count=$((count + 1))
if [ $count -gt 30 ]; then
log_error "Grafana failed to start within 60 seconds"
exit 1
fi
done
}
# Verify installation
verify_installation() {
log_info "Verifying installation..."
# Check Node.js
if ! command -v node >/dev/null 2>&1; then
log_error "Node.js verification failed"
return 1
fi
# Check Grafana service
if ! systemctl is-active grafana-server >/dev/null 2>&1; then
log_error "Grafana service verification failed"
return 1
fi
# Check Grafana HTTP response
if ! curl -s http://localhost:3000 >/dev/null 2>&1; then
log_error "Grafana HTTP verification failed"
return 1
fi
# Check plugin directory
if [ ! -d "$PLUGIN_DIR" ]; then
log_error "Plugin directory verification failed"
return 1
fi
log_info "All verifications passed!"
return 0
}
# Parse command line arguments
PLUGIN_NAME="status-panel"
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-p|--plugin-name)
PLUGIN_NAME="$2"
shift 2
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
# Main execution
main() {
log_info "Starting Grafana custom plugin development environment setup..."
check_prerequisites
detect_distro
install_nodejs
install_grafana
configure_grafana
create_sample_plugin
configure_firewall
start_services
if verify_installation; then
log_info "Installation completed successfully!"
echo
echo "Grafana is now running at: http://localhost:3000"
echo "Default credentials: admin/admin"
echo "Custom plugin '${PLUGIN_NAME}' has been installed"
echo "Plugin development directory: ${PLUGIN_DIR}"
echo
echo "To develop plugins:"
echo "1. Create plugins in: ${TEMP_DIR}"
echo "2. Build with: npm run build"
echo "3. Copy dist/ to: ${PLUGIN_DIR}/your-plugin-name/"
echo "4. Restart Grafana: systemctl restart grafana-server"
else
log_error "Installation verification failed"
exit 1
fi
}
main "$@"
Review the script before running. Execute with: bash install.sh