Skip to content

Latest commit

 

History

History
412 lines (323 loc) · 14.3 KB

File metadata and controls

412 lines (323 loc) · 14.3 KB

Plugin Development Guide

Azimutt Inspector is built around a plugin architecture, here are the four types of plugins:

  1. Collectors: Gather metrics and data from database instances, to store them in Azimutt storage
  2. Analyzers: Process metrics to generate alerts and suggestions, can use collectors data, direct instance connection and LLM calls
  3. Notifiers: Send alerts and suggestions to external systems, with filtering, routing, grouping, throttling, deduplication...
  4. LLM Tools: Extend AI agent and MCP capabilities with custom tools, such as database info, azimutt status or knowledge base

This guide explains how to develop, build, and load custom plugins.

Quick Start

The plugins directory is an independant project with one example for each plugin type:

To use them, you have to build them (npm run build) and put the output folder in PLUGIN_FOLDERS environment variable (you can put several folders, comma separated). Of course, you can create your own project for that, or even several, you just have to provide the path to them.

Building External Plugins

You can either copy the example plugins folder as a template, or create a project from scratch.

Option 1: Copy the example plugins

# Copy the plugins folder as a template
cp -r plugins my-custom-plugins
cd my-custom-plugins

# Install dependencies
npm install

# Build your plugins
npm run build

Option 2: Create from scratch

# 1. Initialize project
mkdir my-custom-plugins && cd my-custom-plugins
npm init -y
npm install --save-dev typescript

# 2. Create structure
mkdir src
npx tsc --init --rootDir ./src --outDir ./dist --target ES2020 --module CommonJS

Edit package.json to add dependencies and scripts:

{
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch"
  },
  "peerDependencies": {
    "@azimutt/inspector-core": "*",
    "zod": "4.1.8"
  },
  "devDependencies": {
    "@azimutt/inspector-core": "file:../inspector/packages/core",
    "typescript": "^5.7.3",
    "zod": "4.1.8"
  }
}

Create your first plugin in src/my_collector.ts:

import * as z from 'zod'
import { CollectorCtx, CollectorPlugin, CollectorPluginId } from '@azimutt/inspector-core'

const MyCollector: CollectorPlugin = {
  module: CollectorPluginId.parse('company.my_collector'),
  description: 'My custom collector',
  doc: `Collects custom metrics.`,
  config: z.strictObject({}),
  result: z.strictObject({ value: z.number() }),
  engines: ['postgresql'],
  collect: (ctx: CollectorCtx) => {
    return ctx.connect({
      pg: conn => conn.all('SELECT 1 AS value')
    })
  },
}

export default MyCollector

Build and configure:

npm install
npm run build

Then add PLUGIN_FOLDERS=../my-custom-plugins/dist to your environment variables. They will be loaded into Azimutt Inspector and you can use them in your configuration.

Plugin Types

1. Collector Plugins

Collectors gather data from database instances with a delay between two collections:

import * as z from 'zod'
import { CollectorCtx, CollectorPlugin, CollectorPluginId } from '@azimutt/inspector-core'

// specific conf for this collector (if needed
const MyConfig = z.strictObject({ threshold: z.number().optional() })
type MyConfig = z.infer<typeof MyConfig>

// the shape of collector rows
const MyResult = z.strictObject({ metric: z.number(), timestamp: z.date() })
type MyResult = z.infer<typeof MyResult>

const MyCollector: CollectorPlugin<MyResult, MyConfig> = {
  module: CollectorPluginId.parse('company.my_collector'), // Unique module ID
  description: 'Collects custom metrics',                  // One-line description
  doc: `Detailed documentation...`,                        // Markdown documentation
  config: MyConfig,                                        // Configuration schema
  result: MyResult,                                        // Result schema
  engines: ['postgresql', 'mysql'],                        // Supported databases
  collect: async (ctx: CollectorCtx<MyResult, MyConfig>): Promise<MyResult[]> => {
    return ctx.connect({
      pg: async conn => {
        const rows = await conn.all('SELECT ...')
        return rows.map(row => ({ metric: row.value, timestamp: new Date() }))
      },
      mysql: async conn => {
        const rows = await conn.all('SELECT ...')
        return rows.map(row => ({ metric: row.value, timestamp: new Date() }))
      },
    })
  },
}

export default MyCollector

See full definition in collectorModule.ts, the context provides access to:

  • config: Collector configuration (default + custom)
  • execution: Current execution metadata
  • databaseName: Database to connect to
  • schemaFilter: Optional schema filter
  • connect({pg: c => {}, mysql: c => {}}): Connect to database and run queries
  • getPreviousResult(): Get the last successful execution result
  • logger: Collector logger
  • abortSignal: Check for cancellation

2. Analyzer Plugins

Analyzers process collected data to create alerts and suggestions.

import * as z from 'zod'
import { AnalyzerCtx, AnalyzerPlugin, AnalyzerPluginId, AnalyzerResult } from '@azimutt/inspector-core'

const MyConfig = z.strictObject({ threshold: z.number() })
type MyConfig = z.infer<typeof MyConfig>

const MyResult = z.strictObject({ alertsCreated: AlertStarted.array(), alertsResolved: AlertId.array() })
type MyResult = z.infer<typeof MyResult>

const MyAnalyzer: AnalyzerPlugin<AnalyzerResult, MyConfig> = {
  module: AnalyzerPluginId.parse('company.my_analyzer'),
  description: 'Detects custom issues',
  doc: `Analyzes collected data and creates alerts when threshold is exceeded.`,
  config: MyConfig,
  analyze: async (ctx: AnalyzerCtx<MyConfig>): Promise<MyResult> => {
    const execInfosByInstance = await ctx.getCollectorsLastResultInfos()
    const activeAlerts = await ctx.getAlerts({ issue: 'my_issue', resolved: false })
    const [alertsCreated] = await ctx.createAlerts([{ issue: 'my_issue', title: 'Custom threshold exceeded', reason: 'reason', explanation: 'explanation', severity: 'warning' }])
    const [resolved] = await ctx.resolveAlerts(resolvedAlerts)
    return { alertsCreated, alertsResolved: resolved.map(a => a.id) }
  },
}

export default MyAnalyzer

See full definition in analyzerModule.ts, the context provides access to:

  • config: Analyzer configuration
  • execution: Current execution metadata
  • analyzer: Full analyzer configuration
  • getPreviousAnalyzerResult(): Last successful execution of this analyzer
  • getCollectorsLastResultInfos(): Get last execution info for each (instance, collector) pair without heavy result field
  • getCollectorAllResults(filter): Get all successful execution info for a collector with filtering (collector, instance, since, limit)
  • getExecutions(filter): Query execution history
  • getExecutionResults(filter): Get only successful executions
  • getLastExecutionResult(filter): Get last successful execution (deprecated, use getCollectorAllResults)
  • getLastExecutionResults(filter): Get last successful execution per job (deprecated, use getCollectorsLastResultInfos)
  • getAlerts(filter): Get alerts (all or filtered by module, instance, issue, resolved status)
  • createAlerts(alerts): Create new alerts
  • resolveAlerts(alerts): Resolve alerts
  • createInstantAlerts(alerts): Create and immediately close alerts
  • getSuggestions(filter): Get suggestions (all or filtered by module, instance, resolved status)
  • createSuggestions(suggestions): Create optimization suggestions
  • seenSuggestions(ids): Mark suggestions as seen
  • resolveSuggestions(ids): Resolve suggestions
  • getInstances(ids): Fetch all instances or only specific ones
  • connect(instanceId, exec, opts): Connect to a specific database instance
  • useLlm(exec): Use configured LLM for AI-powered analysis
  • logger: Logging
  • abortSignal: Cancellation signal

3. Notifier Plugins

Notifiers send alerts and suggestions to external systems (Slack, email, webhooks, etc.).

import * as z from 'zod'
import { NotifierCtx, NotifierPlugin, NotifierPluginId, NotifierResult, router } from '@azimutt/inspector-core'

const MyConfig = z.strictObject({
  webhookUrl: z.string().url(),
  channel: z.string().optional(),
})
type MyConfig = z.infer<typeof MyConfig>

const MyNotifier: NotifierPlugin<MyConfig> = {
  module: NotifierPluginId.parse('company.my_notifier'),
  description: 'Sends alerts via webhook',
  doc: `Posts alerts and suggestions to a webhook endpoint.`,
  config: MyConfig,
  notify: async (ctx: NotifierCtx<MyConfig>): Promise<NotifierResult> => {
    const prev = await ctx.getPreviousResult()
    const since = prev?.startedAt || new Date(Date.now() - 60000)
    const newAlerts = await ctx.getAlertsStartedAfter(since)
    const resolvedAlerts = await ctx.getAlertsResolvedAfter(since)
    const routerAbs = router.setBase(ctx.serverUrl)
    for (const alert of newAlerts) {
      await fetch(ctx.config.webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ type: 'alert', title: alert.title, severity: alert.severity, url: routerAbs.alert(alert.id) }),
      })
    }
    return { newAlerts: newAlerts.map(a => a.id), resolvedAlerts: resolvedAlerts.map(a => a.id) }
  }
}

export default MyNotifier

See full definition in notifierModule.ts, the context provides access to:

  • serverUrl: Azimutt Inspector base URL for building links
  • config: Notifier configuration
  • execution: Current execution metadata
  • getPreviousResult(): Last successful execution
  • getAlertsStartedAfter(date, filter): New alerts since date
  • getAlertsResolvedAfter(date, filter): Resolved alerts since date
  • getSuggestionsCreatedAfter(date): New suggestions since date
  • logger: Logging
  • abortSignal: Cancellation signal

4. LLM Tool Plugins

LLM Tools extend the AI agent capabilities with custom database queries or operations.

import { LlmToolArgs, LlmToolCtx, LlmToolPlugin, LlmToolPluginId } from '@azimutt/inspector-core'

export const MyTool: LlmToolPlugin = {
  module: LlmToolPluginId.parse('company.my_tool'),
  category: 'Database',
  description: 'Execute custom operation on database',
  info: 'Performs maintenance operations', // user explanations
  params: {
    instance: { type: 'string' },
    operation: { type: 'string', enum: ['analyze', 'vacuum'] },
  },
  call: (ctx: LlmToolCtx) => async (args: LlmToolArgs): Promise<string> => {
    if (typeof args.instance !== 'string') return Promise.reject(new Error('Missing instance parameter'))
    const result = await ctx.connect(args.instance, {
      pg: async conn => {
        if (args.operation === 'analyze') return await conn.all('ANALYZE VERBOSE;')
        if (args.operation === 'vacuum') return await conn.all('VACUUM ANALYZE;')
        throw new Error('Invalid operation')
      },
    })
    // Return LLM-friendly response
    return `Operation ${args.operation} completed: ${JSON.stringify(result, null, 2)}`
  },
}

export default MyTool

See full definition in llmToolModule.ts, the context provides access to:

  • config: Full Azimutt Inspector configuration
  • model: LLM model being used
  • source: Source of the tool call (for tracking)
  • connect(instance, exec, opts): Connect to database with optional transaction mode
  • getActiveSuggestions(filter): Query suggestions
  • createSuggestion(suggestion): Create new suggestions
  • getActiveAlerts(filter): Query alerts
  • logger: Logging
  • abortSignal: Cancellation signal

Loading Plugins

Set up the PLUGIN_FOLDERS environment variable with a comma-separated list of folders containing compiled plugins (.js files).

Azimutt Inspector will:

  1. Scan these folders for .js files
  2. Load modules that export plugin objects
  3. Validate plugin structure
  4. Register plugins for use in configuration

Best Practices

Naming Conventions

  • Module IDs: Use reverse domain notation: company.plugin_name
  • Avoid conflicts: Never use azimutt.* prefix (reserved for built-in plugins)
  • File names: Use snake_case.type.ts (e.g., my_collector.collector.ts)

Performance

  • Use timeouts: Check for abortSignal to cancel the execution
  • Limit results: Use LIMIT clauses in queries
  • Batch operations: Create alerts/suggestions in batches

Testing

Test your plugins before deployment:

// In your test file
import { describe, it, expect } from '@jest/globals'
import MyCollector from './my_collector'

describe('MyCollector', () => {
  it('should export valid module', () => {
    expect(MyCollector.module).toBe('company.my_collector')
    expect(MyCollector.engines).toContain('postgresql')
  })

  it('should validate config', () => {
    const result = MyCollector.config.safeParse({ threshold: 100 })
    expect(result.success).toBe(true)
  })
})

Documentation

Write clear documentation in the doc field:

doc: `# My Custom Collector

Collects custom metrics from the database.

## Configuration

- \`threshold\` (optional): Minimum threshold for metrics (default: 100)

## Requirements

- PostgreSQL 12+
- \`pg_stat_statements\` extension enabled

## Result Schema

\`\`\`json
[
  {
    "metric": 150,
    "timestamp": "2024-01-15T10:30:00Z"
  }
]
\`\`\`

## Example Configuration

\`\`\`hocon
collectors: [
  {id: my_collector, module: company.my_collector, threshold: 200}
]
\`\`\`
`

Troubleshooting

Plugin not loading

  1. Check plugin folder path in config
  2. Verify TypeScript compilation succeeded (npm run build)
  3. Check backend logs for loading errors
  4. Ensure default export is present

Runtime errors

  • Use ctx.logger, you will see plugin logs in the UI
  • Check execution history in UI
  • Verify database permissions