Azimutt Inspector is built around a plugin architecture, here are the four types of plugins:
- Collectors: Gather metrics and data from database instances, to store them in Azimutt storage
- Analyzers: Process metrics to generate alerts and suggestions, can use collectors data, direct instance connection and LLM calls
- Notifiers: Send alerts and suggestions to external systems, with filtering, routing, grouping, throttling, deduplication...
- 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.
The plugins directory is an independant project with one example for each plugin type:
ping.collector.ts- Simple ping collectorslow_ping.analyzer.ts- Ping duration analyzerconsole.notifier.ts- Console log notifierlist_connections.tool.ts- Database connections LLM tool
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.
You can either copy the example plugins folder as a template, or create a project from scratch.
# 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# 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 CommonJSEdit 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 MyCollectorBuild and configure:
npm install
npm run buildThen 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.
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 MyCollectorSee full definition in collectorModule.ts, the context provides access to:
config: Collector configuration (default + custom)execution: Current execution metadatadatabaseName: Database to connect toschemaFilter: Optional schema filterconnect({pg: c => {}, mysql: c => {}}): Connect to database and run queriesgetPreviousResult(): Get the last successful execution resultlogger: Collector loggerabortSignal: Check for cancellation
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 MyAnalyzerSee full definition in analyzerModule.ts, the context provides access to:
config: Analyzer configurationexecution: Current execution metadataanalyzer: Full analyzer configurationgetPreviousAnalyzerResult(): Last successful execution of this analyzergetCollectorsLastResultInfos(): Get last execution info for each (instance, collector) pair without heavy result fieldgetCollectorAllResults(filter): Get all successful execution info for a collector with filtering (collector, instance, since, limit)getExecutions(filter): Query execution historygetExecutionResults(filter): Get only successful executionsgetLastExecutionResult(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 alertsresolveAlerts(alerts): Resolve alertscreateInstantAlerts(alerts): Create and immediately close alertsgetSuggestions(filter): Get suggestions (all or filtered by module, instance, resolved status)createSuggestions(suggestions): Create optimization suggestionsseenSuggestions(ids): Mark suggestions as seenresolveSuggestions(ids): Resolve suggestionsgetInstances(ids): Fetch all instances or only specific onesconnect(instanceId, exec, opts): Connect to a specific database instanceuseLlm(exec): Use configured LLM for AI-powered analysislogger: LoggingabortSignal: Cancellation signal
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 MyNotifierSee full definition in notifierModule.ts, the context provides access to:
serverUrl: Azimutt Inspector base URL for building linksconfig: Notifier configurationexecution: Current execution metadatagetPreviousResult(): Last successful executiongetAlertsStartedAfter(date, filter): New alerts since dategetAlertsResolvedAfter(date, filter): Resolved alerts since dategetSuggestionsCreatedAfter(date): New suggestions since datelogger: LoggingabortSignal: Cancellation signal
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 MyToolSee full definition in llmToolModule.ts, the context provides access to:
config: Full Azimutt Inspector configurationmodel: LLM model being usedsource: Source of the tool call (for tracking)connect(instance, exec, opts): Connect to database with optional transaction modegetActiveSuggestions(filter): Query suggestionscreateSuggestion(suggestion): Create new suggestionsgetActiveAlerts(filter): Query alertslogger: LoggingabortSignal: Cancellation signal
Set up the PLUGIN_FOLDERS environment variable with a comma-separated list of folders containing compiled plugins (.js files).
Azimutt Inspector will:
- Scan these folders for
.jsfiles - Load modules that export plugin objects
- Validate plugin structure
- Register plugins for use in configuration
- 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)
- Use timeouts: Check for
abortSignalto cancel the execution - Limit results: Use
LIMITclauses in queries - Batch operations: Create alerts/suggestions in batches
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)
})
})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}
]
\`\`\`
`- Check plugin folder path in config
- Verify TypeScript compilation succeeded (
npm run build) - Check backend logs for loading errors
- Ensure default export is present
- Use
ctx.logger, you will see plugin logs in the UI - Check execution history in UI
- Verify database permissions