referit changes
This commit is contained in:
104
CLAUDE.md
Normal file
104
CLAUDE.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Grafana-Zabbix is a hybrid Grafana plugin connecting Grafana to Zabbix monitoring infrastructure. It consists of three components:
|
||||||
|
- **Zabbix App** (`/src`) - Container app plugin
|
||||||
|
- **Zabbix Data Source** (`/src/datasource`) - TypeScript frontend + Go backend for querying Zabbix
|
||||||
|
- **Problems Panel** (`/src/panel-triggers`) - React panel for displaying Zabbix problems/triggers
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install all dependencies
|
||||||
|
make install # or: yarn install && go install -v ./pkg/
|
||||||
|
|
||||||
|
# Development
|
||||||
|
yarn dev # Watch mode for frontend
|
||||||
|
make run-backend # Watch mode for backend (uses bra)
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
make build # or: yarn build && mage -v build:backend
|
||||||
|
|
||||||
|
# Distribution (all platforms)
|
||||||
|
make dist
|
||||||
|
|
||||||
|
# After building, restart Grafana to reload the plugin
|
||||||
|
podman container stop grafana && podman container start grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frontend tests (Jest)
|
||||||
|
yarn test # Watch mode
|
||||||
|
yarn test:ci # CI mode, all tests
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
yarn test datasource.test.ts
|
||||||
|
yarn test Problems.test.tsx
|
||||||
|
|
||||||
|
# Backend tests (Go)
|
||||||
|
go test ./pkg/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Test files are located alongside source code with `.test.ts` or `.test.tsx` extensions:
|
||||||
|
- `src/datasource/specs/*.test.ts`
|
||||||
|
- `src/datasource/components/**/*.test.tsx`
|
||||||
|
- `src/datasource/zabbix/**/*.test.ts`
|
||||||
|
- `src/panel-triggers/components/**/*.test.tsx`
|
||||||
|
|
||||||
|
## Linting and Type Checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn lint # Check for linting errors
|
||||||
|
yarn lint:fix # Auto-fix lint and formatting issues
|
||||||
|
yarn typecheck # TypeScript type checking
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn server # Start Grafana + Zabbix via Docker Compose
|
||||||
|
yarn server:down # Stop and remove containers
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker setup in `/devenv/` includes multiple Zabbix versions (5.0, 6.0, 7.0, 7.2, 7.4) for compatibility testing.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**Frontend-Backend Communication**: Frontend queries go through Grafana's backend proxy to the plugin backend executable (`gpx_zabbix-datasource`), which handles Zabbix API calls. Uses gRPC via Grafana Plugin SDK.
|
||||||
|
|
||||||
|
**Key Frontend Packages**:
|
||||||
|
- `/src/datasource/datasource.ts` - Main ZabbixDatasource class
|
||||||
|
- `/src/datasource/zabbix/` - Zabbix API client logic
|
||||||
|
- `/src/datasource/components/` - Query editor, config editor components
|
||||||
|
- `/src/panel-triggers/components/` - Problems panel components
|
||||||
|
|
||||||
|
**Key Backend Packages** (`/pkg/`):
|
||||||
|
- `datasource/` - Main backend logic
|
||||||
|
- `zabbix/` - Zabbix API interactions
|
||||||
|
- `zabbixapi/` - Low-level API connector
|
||||||
|
- `cache/` - Caching layer
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js: v24 (see `.nvmrc`)
|
||||||
|
- Yarn: v4.12.0
|
||||||
|
- Go: 1.25
|
||||||
|
- Grafana: >=11.6.0
|
||||||
|
|
||||||
|
## PR Workflow
|
||||||
|
|
||||||
|
Run `yarn changeset` to create a changeset file for your PR. This is required for version bumping and changelog generation.
|
||||||
|
|
||||||
|
## Debugging Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build-debug # Build with debug flags
|
||||||
|
./debug-backend.sh # Attach delve debugger (after starting Grafana)
|
||||||
|
```
|
||||||
|
|
||||||
|
VS Code debug config connects to delve on port 3222.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "grafana-zabbix",
|
"name": "grafana-zabbix",
|
||||||
"version": "6.2.0-rit",
|
"version": "6.2-rit",
|
||||||
|
|
||||||
|
|
||||||
"description": "Zabbix plugin for Grafana",
|
"description": "Zabbix plugin for Grafana",
|
||||||
"homepage": "http://grafana-zabbix.org",
|
"homepage": "http://grafana-zabbix.org",
|
||||||
|
|||||||
@@ -11,13 +11,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, query *QueryModel) ([]*data.Frame, error) {
|
func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, query *QueryModel) ([]*data.Frame, error) {
|
||||||
|
ds.logger.Info("queryProblemsAlerting called", "groupFilter", query.Group.Filter, "hostFilter", query.Host.Filter)
|
||||||
|
|
||||||
triggers, err := ds.zabbix.GetTriggers(ctx, query.Group.Filter, query.Host.Filter)
|
triggers, err := ds.zabbix.GetTriggers(ctx, query.Group.Filter, query.Host.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ds.logger.Error("GetTriggers failed", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ds.logger.Info("GetTriggers returned", "count", len(triggers))
|
||||||
|
|
||||||
if len(triggers) == 0 {
|
if len(triggers) == 0 {
|
||||||
return []*data.Frame{}, nil
|
frame := data.NewFrame("No triggers found")
|
||||||
|
frame.RefID = query.RefID
|
||||||
|
return []*data.Frame{frame}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerIDs := make([]string, len(triggers))
|
triggerIDs := make([]string, len(triggers))
|
||||||
@@ -30,6 +37,7 @@ func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, q
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map trigger ID to problem (includes opdata captured at problem creation)
|
||||||
problemMap := buildProblemMap(problems)
|
problemMap := buildProblemMap(problems)
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
|
|
||||||
@@ -37,10 +45,14 @@ func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, q
|
|||||||
for _, trigger := range triggers {
|
for _, trigger := range triggers {
|
||||||
for _, host := range trigger.Hosts {
|
for _, host := range trigger.Hosts {
|
||||||
value := 0.0
|
value := 0.0
|
||||||
if problemMap[trigger.ID] {
|
problemName := ""
|
||||||
|
opdata := ""
|
||||||
|
if problem, hasProblem := problemMap[trigger.ID]; hasProblem {
|
||||||
value = 1.0
|
value = 1.0
|
||||||
|
problemName = problem.Name // Expanded trigger description at problem creation
|
||||||
|
opdata = problem.Opdata // Operational data if configured
|
||||||
}
|
}
|
||||||
frame := createAlertingFrame(trigger, host, value, currentTime, query.RefID)
|
frame := createAlertingFrame(trigger, host, value, problemName, opdata, currentTime, query.RefID)
|
||||||
frames = append(frames, frame)
|
frames = append(frames, frame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,21 +60,24 @@ func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, q
|
|||||||
return frames, nil
|
return frames, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildProblemMap(problems []zabbix.Problem) map[string]bool {
|
// buildProblemMap returns a map of trigger ID to Problem object
|
||||||
problemMap := make(map[string]bool)
|
func buildProblemMap(problems []zabbix.Problem) map[string]zabbix.Problem {
|
||||||
|
problemMap := make(map[string]zabbix.Problem)
|
||||||
for _, p := range problems {
|
for _, p := range problems {
|
||||||
problemMap[p.ObjectID] = true
|
problemMap[p.ObjectID] = p
|
||||||
}
|
}
|
||||||
return problemMap
|
return problemMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAlertingFrame(trigger zabbix.Trigger, host zabbix.ItemHost, value float64, ts time.Time, refID string) *data.Frame {
|
func createAlertingFrame(trigger zabbix.Trigger, host zabbix.ItemHost, value float64, problemName string, opdata string, ts time.Time, refID string) *data.Frame {
|
||||||
labels := data.Labels{
|
labels := data.Labels{
|
||||||
"host": host.Name,
|
"host": host.Name,
|
||||||
"trigger": trigger.Description,
|
"trigger": trigger.Description,
|
||||||
"trigger_id": trigger.ID,
|
"trigger_id": trigger.ID,
|
||||||
"severity": trigger.Priority,
|
"severity": mapSeverity(trigger.Priority),
|
||||||
"tags": formatTags(trigger.Tags),
|
"tags": formatTags(trigger.Tags),
|
||||||
|
"problem_name": problemName, // Expanded trigger description at problem creation
|
||||||
|
"opdata": opdata, // Operational data if configured on trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
timeField := data.NewField("time", nil, []time.Time{ts})
|
timeField := data.NewField("time", nil, []time.Time{ts})
|
||||||
@@ -75,6 +90,25 @@ func createAlertingFrame(trigger zabbix.Trigger, host zabbix.ItemHost, value flo
|
|||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapSeverity(priority string) string {
|
||||||
|
switch priority {
|
||||||
|
case "0":
|
||||||
|
return "Not classified"
|
||||||
|
case "1":
|
||||||
|
return "Information"
|
||||||
|
case "2":
|
||||||
|
return "Warning"
|
||||||
|
case "3":
|
||||||
|
return "Average"
|
||||||
|
case "4":
|
||||||
|
return "High"
|
||||||
|
case "5":
|
||||||
|
return "Disaster"
|
||||||
|
default:
|
||||||
|
return priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func formatTags(tags []zabbix.Tag) string {
|
func formatTags(tags []zabbix.Tag) string {
|
||||||
if len(tags) == 0 {
|
if len(tags) == 0 {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
)
|
)
|
||||||
@@ -567,10 +568,15 @@ func (ds *Zabbix) GetVersion(ctx context.Context) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ds *Zabbix) GetTriggers(ctx context.Context, groupFilter string, hostFilter string) ([]Trigger, error) {
|
func (ds *Zabbix) GetTriggers(ctx context.Context, groupFilter string, hostFilter string) ([]Trigger, error) {
|
||||||
|
ds.logger.Info("GetTriggers called", "groupFilter", groupFilter, "hostFilter", hostFilter)
|
||||||
|
|
||||||
hosts, err := ds.GetHosts(ctx, groupFilter, hostFilter)
|
hosts, err := ds.GetHosts(ctx, groupFilter, hostFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ds.logger.Error("GetHosts failed", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ds.logger.Info("GetHosts returned", "count", len(hosts))
|
||||||
|
|
||||||
if len(hosts) == 0 {
|
if len(hosts) == 0 {
|
||||||
return []Trigger{}, nil
|
return []Trigger{}, nil
|
||||||
}
|
}
|
||||||
@@ -580,23 +586,71 @@ func (ds *Zabbix) GetTriggers(ctx context.Context, groupFilter string, hostFilte
|
|||||||
hostids = append(hostids, host.ID)
|
hostids = append(hostids, host.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := ZabbixAPIParams{
|
// Get triggers currently in PROBLEM state (always include these)
|
||||||
|
problemParams := ZabbixAPIParams{
|
||||||
"output": []string{"triggerid", "description", "priority", "value"},
|
"output": []string{"triggerid", "description", "priority", "value"},
|
||||||
"hostids": hostids,
|
"hostids": hostids,
|
||||||
"selectHosts": []string{"hostid", "name"},
|
"selectHosts": []string{"hostid", "name"},
|
||||||
"selectTags": "extend",
|
"selectTags": "extend",
|
||||||
"monitored": true,
|
"monitored": true,
|
||||||
"expandDescription": true,
|
"expandDescription": true,
|
||||||
|
"skipDependent": true, // Only show root cause triggers, not dependent ones
|
||||||
|
"filter": map[string]interface{}{
|
||||||
|
"value": "1",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := ds.Request(ctx, &ZabbixAPIRequest{Method: "trigger.get", Params: params})
|
problemResult, err := ds.Request(ctx, &ZabbixAPIRequest{Method: "trigger.get", Params: problemParams})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var triggers []Trigger
|
var problemTriggers []Trigger
|
||||||
err = convertTo(result, &triggers)
|
err = convertTo(problemResult, &problemTriggers)
|
||||||
return triggers, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get triggers that changed state in the last 24 hours (for alert resolution)
|
||||||
|
oneDayAgo := time.Now().Add(-24 * time.Hour).Unix()
|
||||||
|
recentParams := ZabbixAPIParams{
|
||||||
|
"output": []string{"triggerid", "description", "priority", "value"},
|
||||||
|
"hostids": hostids,
|
||||||
|
"selectHosts": []string{"hostid", "name"},
|
||||||
|
"selectTags": "extend",
|
||||||
|
"monitored": true,
|
||||||
|
"expandDescription": true,
|
||||||
|
"skipDependent": true, // Only show root cause triggers, not dependent ones
|
||||||
|
"lastChangeSince": oneDayAgo,
|
||||||
|
}
|
||||||
|
|
||||||
|
recentResult, err := ds.Request(ctx, &ZabbixAPIRequest{Method: "trigger.get", Params: recentParams})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var recentTriggers []Trigger
|
||||||
|
err = convertTo(recentResult, &recentTriggers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge and deduplicate
|
||||||
|
triggerMap := make(map[string]Trigger)
|
||||||
|
for _, t := range problemTriggers {
|
||||||
|
triggerMap[t.ID] = t
|
||||||
|
}
|
||||||
|
for _, t := range recentTriggers {
|
||||||
|
triggerMap[t.ID] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
triggers := make([]Trigger, 0, len(triggerMap))
|
||||||
|
for _, t := range triggerMap {
|
||||||
|
triggers = append(triggers, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.logger.Info("GetTriggers returning", "problemCount", len(problemTriggers), "recentCount", len(recentTriggers), "totalCount", len(triggers))
|
||||||
|
return triggers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *Zabbix) GetProblems(ctx context.Context, triggerIDs []string) ([]Problem, error) {
|
func (ds *Zabbix) GetProblems(ctx context.Context, triggerIDs []string) ([]Problem, error) {
|
||||||
@@ -605,7 +659,7 @@ func (ds *Zabbix) GetProblems(ctx context.Context, triggerIDs []string) ([]Probl
|
|||||||
}
|
}
|
||||||
|
|
||||||
params := ZabbixAPIParams{
|
params := ZabbixAPIParams{
|
||||||
"output": []string{"eventid", "objectid", "severity", "name"},
|
"output": []string{"eventid", "objectid", "severity", "name", "opdata"},
|
||||||
"objectids": triggerIDs,
|
"objectids": triggerIDs,
|
||||||
"source": "0",
|
"source": "0",
|
||||||
"object": "0",
|
"object": "0",
|
||||||
|
|||||||
@@ -126,5 +126,6 @@ type Problem struct {
|
|||||||
ObjectID string `json:"objectid"`
|
ObjectID string `json:"objectid"`
|
||||||
Severity string `json:"severity"`
|
Severity string `json:"severity"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Opdata string `json:"opdata"`
|
||||||
Tags []Tag `json:"tags,omitempty"`
|
Tags []Tag `json:"tags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class ZabbixVariableQueryEditor extends PureComponent<VariableQueryProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSelectedQueryType(queryType: VariableQueryTypes) {
|
getSelectedQueryType(queryType: VariableQueryTypes) {
|
||||||
return this.queryTypes.find((q) => q.value === queryType);
|
return this.queryTypes.find((q) => q.value === queryType) ?? this.defaults.selectedQueryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleQueryUpdate = (evt: React.ChangeEvent<HTMLInputElement>, prop: string) => {
|
handleQueryUpdate = (evt: React.ChangeEvent<HTMLInputElement>, prop: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user