This guide provides comprehensive instructions for creating custom process monitoring plugins using the OpenTelemetry-based framework provided in this repository.
- Development Environment Setup
- Quick Start
- Maintaining Shared Files and Checksums
- Framework Overview
- Creating a New Plugin
- Plugin Configuration
- Testing Your Plugin
- Installation Script Development
- Advanced Customization
- Best Practices
- Complete Example
- Troubleshooting
- Python 3.8 or later (Python 3.12 recommended)
- Conda or Miniconda for virtual environment management
- Git for version control
- Access to an Instana backend or demo environment
- Basic understanding of process monitoring concepts
Create a dedicated conda environment for this project:
# Create the environment
conda create -n instana-plugins python=3.12 -y
# Activate the environment
conda activate instana-plugins
# Install development dependencies
pip install -r tests/requirements.txt
# Optional: Install additional development tools
pip install ipython black flake8For a reproducible environment with all development tools:
# Create environment from the provided configuration
conda env create -f environment.yml
# Activate the environment
conda activate instana-pluginsTest that everything works correctly:
# Verify Python version
python --version # Should show Python 3.12.x
# Test the framework
python tests/run_tests.py
# Test a specific plugin
python m8mulprc/sensor.py --run-once --log-level=DEBUGThe environment includes:
- pytest: Advanced testing framework with fixtures and plugins
- pytest-cov: Test coverage reporting
- coverage: Code coverage analysis and HTML reports
- mock: Mocking library for unit tests
- ipython: Enhanced interactive Python shell
- black: Code formatter (optional)
- flake8: Code linting (optional)
# Activate environment when starting work
conda activate instana-plugins
# Deactivate when done
conda deactivate
# List environments
conda env list
# Remove environment if needed
conda env remove -n instana-pluginsIf using VS Code, ensure your Python interpreter points to the conda environment:
- Open Command Palette (
Cmd+Shift+P/Ctrl+Shift+P) - Select "Python: Select Interpreter"
- Choose the interpreter from your
instana-pluginsenvironment
To create a new plugin for monitoring a process called "MyApp":
- Create plugin directory:
mkdir myapp - Create configuration: Choose between:
- Modern TOML approach:
myapp/plugin.toml(recommended) - Legacy Python approach:
myapp/__init__.py(still supported)
- Modern TOML approach:
- Create sensor:
myapp/sensor.py - Create tests:
tests/test_myapp_sensor.py - Create installer:
myapp/install-instana-myapp-plugin.sh - Test your plugin:
python myapp/sensor.py --run-once
Note: This guide covers both the new TOML-based configuration system (v0.0.20+) and the legacy Python configuration approach for backward compatibility.
When you modify shared files in the common/ directory, you must regenerate the manifest to update SHA256 checksums:
After making changes to any files in the common/ directory:
# Navigate to the common directory
cd common/
# Run the manifest generator
python3 generate_manifest.py
# Verify the updated checksums
cat manifest.tomlYou must regenerate the manifest.toml when:
- ✅ Adding new files to
common/ - ✅ Modifying existing files in
common/ - ✅ Updating version numbers
- ✅ Changing framework metadata
- ✅ Before creating releases or tags
The installation system automatically verifies file integrity using SHA256 checksums:
# Test checksum verification
cd m8mulprc/
sudo ./install-instana-m8mulprc-plugin.sh
# Look for checksum verification messages:
# "✅ File checksum verified: metadata_store.py"
# "❌ Checksum mismatch detected for: [filename]"- Make Changes: Modify files in
common/ - Test Changes: Run your plugin to ensure functionality
- Regenerate Manifest:
cd common && python3 generate_manifest.py - Test Installation: Verify installation with new checksums
- Commit All Changes: Include both code changes and updated manifest.toml
Problem: "Checksum mismatch detected" Solution: Regenerate manifest.toml after any file changes
Problem: "Manifest file not found"
Solution: Run python3 generate_manifest.py in the common/ directory
Problem: "Invalid manifest format" Solution: Check manifest.toml syntax, regenerate if corrupted
The framework consists of two main layers:
┌─────────────────────────────────────┐
│ Your Plugin │
│ ┌─────────────┐ ┌─────────────────┐│
│ │ __init__.py │ │ sensor.py ││
│ │ (config) │ │ (entry point) ││
│ └─────────────┘ └─────────────────┘│
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Common Framework │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ base_sensor │ │process_monitor│ │
│ │ otel_connector│ │logging_config │ │
│ │ metadata_store│ │check_deps │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ OpenTelemetry / Instana │
└─────────────────────────────────────┘
- base_sensor.py: Main framework logic, argument parsing, OpenTelemetry setup
- process_monitor.py: Process detection and metrics collection
- otel_connector.py: OpenTelemetry protocol implementation
- logging_config.py: Centralized logging configuration
- metadata_store.py: Thread-safe metadata persistence with versioned schema
The framework includes a sophisticated metadata schema versioning system that automatically handles database migrations:
- Current Version: Schema version 1.0 (defined in
common/__init__.py) - Version Tracking:
schema_versiontable tracks all database migrations with timestamps - Automatic Migration: Detects legacy databases and migrates them automatically
- New Installations: Create fresh database with current schema (v1.0)
- Legacy Databases: Detected automatically, previous metadata deleted with user warning, fresh v1.0 schema created
- Current Schema: No migration needed, existing data fully preserved
By default, metadata is stored in ~/.instana_plugins/metadata.db. This location can be customized when initializing the MetadataStore.
- The framework handles all metadata operations automatically
- No plugin-specific code needed for metadata management
- Schema migrations are handled transparently
- All plugins share the same metadata database for consistency
- Metadata includes service IDs, metric IDs, and display name formatting rules
- Your sensor calls
run_sensor()with process name and configuration - Framework detects processes matching your criteria
- Metrics are collected from
/procfilesystem - Data is sent to Instana via OpenTelemetry OTLP protocol
- Process repeats at configured intervals
Create a new directory for your plugin. Use lowercase names following the existing convention:
mkdir myapp
cd myappYou can choose between two configuration approaches:
Create myapp/plugin.toml:
[plugin]
name = "myapp"
description = "MyApp process monitoring for Instana"
version = "1.0.0"
[service]
namespace = "MyCompany"
process_name = "MyApp"
[monitoring]
interval = 60
enabled = true
[dependencies]
python_version = ">=3.6"
packages = ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-exporter-otlp"]
[systemd]
service_name = "instana-myapp-monitor"
description = "MyApp monitoring service for Instana"Create myapp/__init__.py:
"""
Configuration for the myapp sensor plugin.
"""
# OpenTelemetry service namespace for grouping services
SERVICE_NAMESPACE = "MyCompany"
# Process name to monitor (case-insensitive matching will be used)
PROCESS_NAME = "MyApp"
# Plugin identifier for Instana (should match directory name)
PLUGIN_NAME = "myapp"TOML Configuration (plugin.toml):
- plugin.name: Unique identifier for your plugin (should match directory name)
- plugin.description: Human-readable description of what the plugin monitors
- plugin.version: Plugin version for tracking and compatibility
- service.namespace: Groups your services in Instana (company/product name)
- service.process_name: The process name to search for (case-insensitive regex matching)
- monitoring.interval: Default collection interval in seconds
- monitoring.enabled: Whether monitoring is enabled by default
- dependencies.python_version: Minimum Python version requirement
- dependencies.packages: Required Python packages for the plugin
- systemd.service_name: Name for the systemd service
- systemd.description: Description for the systemd service
Python Configuration (init.py):
- SERVICE_NAMESPACE: Groups your services in Instana. Use your company/product name.
- PROCESS_NAME: The process name to search for. The framework uses case-insensitive regex matching.
- PLUGIN_NAME: Unique identifier for your plugin. Should match your directory name.
- Rich Metadata: Store comprehensive plugin information including dependencies and descriptions
- Version Management: Built-in version tracking and compatibility checking
- Installation Integration: Automatic integration with shared installation functions
- Checksum Verification: Automatic integrity checking during installation
- Standardization: Industry-standard configuration format
- Future-Proof: Extensible for additional configuration options
- Metadata Sanitization: Automatic service name sanitization for vendor-agnostic compatibility
The framework includes intelligent metadata sanitization that ensures your plugins work with any monitoring system:
- Service Name Input: Your plugin provides service names with any characters (Unicode, emojis, special symbols)
- Automatic Sanitization: The framework converts service names to safe technical identifiers using only
[a-z0-9_] - Dual-Format Storage: Both sanitized (for performance) and original (for display) names are maintained
- Dynamic Metrics Prefixes: Metrics prefixes are automatically derived from sanitized service names
# Input service names (from your TOML or code)
"Strategy₿.M8MulPrc" → "strategy_m8mulprc" (stored) + "Strategy M8mulprc" (display)
"MyCompany@WebServer.v2" → "mycompany_webserver_v2" (stored) + "Mycompany Webserver V2" (display)
"Service-Name With Spaces!" → "service_name_with_spaces" (stored) + "Service Name With Spaces" (display)
"123numeric-start" → "metric_123numeric_start" (stored) + "Metric 123numeric Start" (display)- Unicode Freedom: Use any characters in service names, including company symbols and emojis
- Vendor Agnostic: Your plugins work with any monitoring system that requires safe identifiers
- Performance Optimized: Database queries use sanitized identifiers for optimal speed
- Professional Display: Human-readable names are preserved for monitoring dashboards
- No Configuration Needed: Sanitization is completely automatic
With metadata sanitization, you no longer need to specify hardcoded metrics prefixes in your plugin.toml:
# OLD APPROACH (no longer needed):
[otel_config]
service_name_template = "{service_namespace}.{process_name}"
metrics_prefix = "mycompany.myapp" # ❌ Hardcoded, removed in v0.0.20+
# NEW APPROACH (automatic):
[otel_config]
service_name_template = "{service_namespace}.{process_name}"
# ✅ Metrics prefix automatically derived from service name using sanitizationThe framework automatically:
- Takes your service name template:
"MyCompany.WebServer" - Sanitizes it for technical use:
"mycompany_webserver" - Uses sanitized version for metrics prefix and database storage
- Maintains original for display purposes:
"Mycompany Webserver"
This is the main executable file for your plugin:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MIT License
Copyright (c) 2025 Your Name/Organization
Process monitoring plugin for MyApp processes.
"""
import sys
import os
# Add the parent directory to the path to import the common modules
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from common.base_sensor import run_sensor
from common import VERSION
# Import configuration from package __init__.py
from . import SERVICE_NAMESPACE, PROCESS_NAME, PLUGIN_NAME
if __name__ == "__main__":
run_sensor(PROCESS_NAME, PLUGIN_NAME, VERSION, service_namespace=SERVICE_NAMESPACE)- Shebang: Makes the file executable directly
- Path manipulation: Allows importing common modules
- Configuration import: Uses your
__init__.pyconstants - run_sensor call: The framework handles everything else
chmod +x sensor.pyThe framework supports flexible process matching. You can customize the process detection in several ways:
PROCESS_NAME = "MyApp" # Matches: myapp, MyApp, MYAPP, etc.PROCESS_NAME = "MyApp.*" # Matches: MyApp, MyAppServer, MyAppWorker, etc.PROCESS_NAME = "^MyApp$" # Matches only: MyApp (exact case-insensitive)Choose meaningful service namespaces that group related services:
# Good examples:
SERVICE_NAMESPACE = "MyCompany" # Company-wide services
SERVICE_NAMESPACE = "ECommerce" # Product-specific services
SERVICE_NAMESPACE = "Infrastructure" # Infrastructure services
# Avoid:
SERVICE_NAMESPACE = "App" # Too generic
SERVICE_NAMESPACE = "Monitoring" # RedundantIf you need to monitor multiple related processes, create separate plugins:
myapp-web/ # Web server processes
myapp-worker/ # Background worker processes
myapp-db/ # Database processes
Each with their own configuration and process names.
Test your plugin immediately after creation:
# Test process detection (safe, read-only)
python sensor.py --run-once --log-level=DEBUG
# Test with specific agent connection
python sensor.py --run-once --agent-host=localhost --agent-port=4317Create a test file tests/test_myapp_sensor.py:
#!/usr/bin/env python3
"""
Test cases for the MyApp sensor.
"""
import unittest
from unittest.mock import patch
import sys
import os
# Add the parent directory to the path so we can import the sensor module
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
class TestMyAppSensor(unittest.TestCase):
"""Test cases for the MyApp sensor."""
def test_constants(self):
"""Test the sensor constants."""
from myapp import SERVICE_NAMESPACE, PROCESS_NAME, PLUGIN_NAME
self.assertEqual(SERVICE_NAMESPACE, "MyCompany")
self.assertEqual(PROCESS_NAME, "MyApp")
self.assertEqual(PLUGIN_NAME, "myapp")
def test_sensor_import(self):
"""Test that the sensor module can be imported."""
try:
import myapp.sensor
self.assertTrue(hasattr(myapp.sensor, 'run_sensor'))
except ImportError as e:
self.fail(f"Failed to import sensor module: {e}")
@patch('common.base_sensor.run_sensor')
def test_main_function_call(self, mock_run_sensor):
"""Test the main function parameters."""
from myapp import SERVICE_NAMESPACE, PROCESS_NAME, PLUGIN_NAME
from common import VERSION
# Import the sensor module
import myapp.sensor
# Manually call the main logic (since we can't easily test __main__)
myapp.sensor.run_sensor(PROCESS_NAME, PLUGIN_NAME, VERSION, service_namespace=SERVICE_NAMESPACE)
# Verify the function was called with correct parameters
mock_run_sensor.assert_called_once_with(
PROCESS_NAME,
PLUGIN_NAME,
VERSION,
service_namespace=SERVICE_NAMESPACE
)
if __name__ == '__main__':
unittest.main()# Run your specific test
python -m unittest tests/test_myapp_sensor.py
# Run all tests
cd tests
python run_tests.py
# Run with verbose output
python -m unittest -v tests/test_myapp_sensor.pyYour tests should cover:
- Configuration validation: Ensure constants are correct
- Import testing: Verify modules can be imported
- Function calls: Mock and verify framework calls
- Error handling: Test failure scenarios
Create install-instana-myapp-plugin.sh:
#!/bin/bash
#
# MIT License
#
# Copyright (c) 2025 Your Name/Organization
#
# MyApp Instana Plugin Installer
#
# Use the Strategy₿ installer as a template
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PARENT_DIR="$( dirname "$SCRIPT_DIR" )"
# Source the shared version extraction script
source "${PARENT_DIR}/common/extract_version.sh"
# Default installation directories
DEFAULT_BASE_DIR="/opt/instana_plugins"
# Define plugin-specific variables
PROCESS_NAME="MyApp"
PLUGIN_DIR_NAME="myapp" # Must match your directory name
# ... rest of the installation script follows the template patternWhen adapting an existing installation script, change these variables:
# Change these for your plugin:
PROCESS_NAME="MyApp" # Display name in messages
PLUGIN_DIR_NAME="myapp" # Directory name (lowercase)
SERVICE_NAME="instana-myapp-monitor" # Systemd service name
# Update help text:
function show_usage {
echo -e "Usage: $0 [OPTIONS]"
echo -e "Install the MyApp monitoring plugin for Instana"
# ... rest of help text
}# Test script syntax
bash -n install-instana-myapp-plugin.sh
# Test help output
./install-instana-myapp-plugin.sh --help
# Test installation (as root or with sudo)
sudo ./install-instana-myapp-plugin.sh
# Test non-root installation
./install-instana-myapp-plugin.sh -d ~/instana-pluginsIf you need custom metrics beyond the standard process metrics, you can extend the framework:
Create a custom process monitor by extending the base class:
# myapp/custom_monitor.py
from common.process_monitor import ProcessMonitor
class MyAppProcessMonitor(ProcessMonitor):
def collect_custom_metrics(self, pid):
"""Collect additional metrics specific to MyApp."""
custom_metrics = {}
# Example: Read custom metrics from a file
try:
with open(f'/proc/{pid}/status') as f:
# Parse custom data
pass
except Exception as e:
self.logger.warning(f"Failed to collect custom metrics: {e}")
return custom_metricsUse environment variables for runtime customization:
# In your sensor.py
import os
# Custom collection interval
COLLECTION_INTERVAL = int(os.environ.get('MYAPP_INTERVAL', '60'))
# Custom process filter
PROCESS_FILTER = os.environ.get('MYAPP_PROCESS_FILTER', PROCESS_NAME)Add custom resource attributes:
# In your __init__.py
CUSTOM_ATTRIBUTES = {
"service.version": "1.0.0",
"deployment.environment": "production",
"myapp.cluster": "east-coast"
}Then modify your sensor to pass these attributes to the framework.
If you need to monitor multiple related processes:
# myapp/__init__.py
PROCESS_NAMES = ["MyAppWeb", "MyAppWorker", "MyAppDB"]
SERVICE_NAMESPACE = "MyApp"
PLUGIN_NAME = "myapp"
# myapp/sensor.py
from . import PROCESS_NAMES, SERVICE_NAMESPACE, PLUGIN_NAME
if __name__ == "__main__":
for process_name in PROCESS_NAMES:
run_sensor(process_name, f"{PLUGIN_NAME}-{process_name.lower()}", VERSION,
service_namespace=SERVICE_NAMESPACE)- Plugin directories: lowercase, hyphens for multi-word (
my-app) - Process names: PascalCase matching actual process names (
MyApp) - Plugin names: lowercase, matching directory (
myapp) - Service namespace: PascalCase, meaningful grouping (
MyCompany)
- Minimal permissions: Use Linux capabilities when possible
- No hardcoded secrets: Use environment variables
- Input validation: Validate all user inputs
- TLS encryption: Enable for production deployments
- Reasonable intervals: Default to 60 seconds for production
- Resource monitoring: Monitor your plugin's own resource usage
- Efficient process detection: Use specific process name patterns
- Error handling: Fail gracefully without crashing
Include in your plugin directory:
- README.md: Plugin-specific documentation
- CHANGELOG.md: Version history and changes
- examples/: Configuration examples
- docs/: Additional documentation if needed
Follow semantic versioning and update these files:
- common/init.py: Framework version
- Plugin README: Version compatibility
- Installation script: Version checks
- Tests: Version validation
Here's a complete working example for monitoring a fictional "WebServer" process:
webserver/
├── __init__.py
├── sensor.py
├── install-instana-webserver-plugin.sh
└── README.md
"""
Configuration for the webserver sensor plugin.
"""
SERVICE_NAMESPACE = "WebInfrastructure"
PROCESS_NAME = "WebServer"
PLUGIN_NAME = "webserver"#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MIT License
Copyright (c) 2025 Your Organization
WebServer process monitoring plugin for Instana.
"""
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from common.base_sensor import run_sensor
from common import VERSION
from . import SERVICE_NAMESPACE, PROCESS_NAME, PLUGIN_NAME
if __name__ == "__main__":
run_sensor(PROCESS_NAME, PLUGIN_NAME, VERSION, service_namespace=SERVICE_NAMESPACE)#!/usr/bin/env python3
"""
Test cases for the WebServer sensor.
"""
import unittest
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
class TestWebServerSensor(unittest.TestCase):
def test_constants(self):
from webserver import SERVICE_NAMESPACE, PROCESS_NAME, PLUGIN_NAME
self.assertEqual(SERVICE_NAMESPACE, "WebInfrastructure")
self.assertEqual(PROCESS_NAME, "WebServer")
self.assertEqual(PLUGIN_NAME, "webserver")
if __name__ == '__main__':
unittest.main()# Test the plugin
python webserver/sensor.py --run-once --log-level=DEBUG
# Install the plugin
sudo ./webserver/install-instana-webserver-plugin.sh
# Run tests
python -m unittest tests/test_webserver_sensor.py-
Import Errors
ModuleNotFoundError: No module named 'common'Solution: Ensure
sys.path.insert()is correct and common directory exists -
Process Not Found
No processes found matching 'MyApp'Solution: Check process name spelling and case, verify process is running
-
Permission Denied
PermissionError: [Errno 13] Permission denied: '/proc/12345/stat'Solution: Run with appropriate permissions or use Linux capabilities
-
OpenTelemetry Connection Failed
Failed to export metrics: connection refusedSolution: Verify Instana agent is running and accepting OTLP connections
- Use debug logging:
--log-level=DEBUG - Test incrementally: Start with
--run-once - Check process list:
ps aux | grep -i myapp - Verify agent:
systemctl status instana-agent - Check ports:
netstat -ln | grep 4317
- Framework issues: Check existing plugins for patterns
- OpenTelemetry issues: Review OTLP documentation
- Instana integration: Consult Instana agent documentation
- Testing issues: Review test patterns in existing test files
When contributing new plugins or framework improvements:
- Follow existing patterns: Study current implementations
- Add comprehensive tests: Include unit and integration tests
- Document thoroughly: Update this guide and add plugin-specific docs
- Test on multiple environments: Verify compatibility
- Update version tracking: Increment versions appropriately
This framework is designed to be extensible and maintainable. By following these patterns and best practices, you can create robust monitoring plugins that integrate seamlessly with Instana.