From 3d7090828575de8b68748307ddd227f387691068 Mon Sep 17 00:00:00 2001 From: Wesley van Tilburg Date: Thu, 29 Jan 2026 09:33:03 +0000 Subject: [PATCH] referit changes --- CLAUDE.md | 104 ++++++++++++++++++ package.json | 3 +- pkg/datasource/problems_alerting.go | 58 ++++++++-- pkg/zabbix/methods.go | 66 ++++++++++- pkg/zabbix/models.go | 1 + .../components/VariableQueryEditor.tsx | 2 +- 6 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5571488 --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/package.json b/package.json index d345490..46cf5b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "grafana-zabbix", - "version": "6.2.0-rit", + "version": "6.2-rit", + "description": "Zabbix plugin for Grafana", "homepage": "http://grafana-zabbix.org", diff --git a/pkg/datasource/problems_alerting.go b/pkg/datasource/problems_alerting.go index 5abf02c..f4fb923 100644 --- a/pkg/datasource/problems_alerting.go +++ b/pkg/datasource/problems_alerting.go @@ -11,13 +11,20 @@ import ( ) 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) if err != nil { + ds.logger.Error("GetTriggers failed", "error", err) return nil, err } + ds.logger.Info("GetTriggers returned", "count", len(triggers)) + 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)) @@ -30,6 +37,7 @@ func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, q return nil, err } + // Map trigger ID to problem (includes opdata captured at problem creation) problemMap := buildProblemMap(problems) currentTime := time.Now() @@ -37,10 +45,14 @@ func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, q for _, trigger := range triggers { for _, host := range trigger.Hosts { value := 0.0 - if problemMap[trigger.ID] { + problemName := "" + opdata := "" + if problem, hasProblem := problemMap[trigger.ID]; hasProblem { 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) } } @@ -48,21 +60,24 @@ func (ds *ZabbixDatasourceInstance) queryProblemsAlerting(ctx context.Context, q return frames, nil } -func buildProblemMap(problems []zabbix.Problem) map[string]bool { - problemMap := make(map[string]bool) +// buildProblemMap returns a map of trigger ID to Problem object +func buildProblemMap(problems []zabbix.Problem) map[string]zabbix.Problem { + problemMap := make(map[string]zabbix.Problem) for _, p := range problems { - problemMap[p.ObjectID] = true + problemMap[p.ObjectID] = p } 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{ - "host": host.Name, - "trigger": trigger.Description, - "trigger_id": trigger.ID, - "severity": trigger.Priority, - "tags": formatTags(trigger.Tags), + "host": host.Name, + "trigger": trigger.Description, + "trigger_id": trigger.ID, + "severity": mapSeverity(trigger.Priority), + "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}) @@ -75,6 +90,25 @@ func createAlertingFrame(trigger zabbix.Trigger, host zabbix.ItemHost, value flo 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 { if len(tags) == 0 { return "" diff --git a/pkg/zabbix/methods.go b/pkg/zabbix/methods.go index aca317c..7dd9526 100644 --- a/pkg/zabbix/methods.go +++ b/pkg/zabbix/methods.go @@ -5,6 +5,7 @@ import ( "sort" "strconv" "strings" + "time" "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) { + ds.logger.Info("GetTriggers called", "groupFilter", groupFilter, "hostFilter", hostFilter) + hosts, err := ds.GetHosts(ctx, groupFilter, hostFilter) if err != nil { + ds.logger.Error("GetHosts failed", "error", err) return nil, err } + ds.logger.Info("GetHosts returned", "count", len(hosts)) + if len(hosts) == 0 { return []Trigger{}, nil } @@ -580,23 +586,71 @@ func (ds *Zabbix) GetTriggers(ctx context.Context, groupFilter string, hostFilte hostids = append(hostids, host.ID) } - params := ZabbixAPIParams{ + // Get triggers currently in PROBLEM state (always include these) + problemParams := 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 + "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 { return nil, err } - var triggers []Trigger - err = convertTo(result, &triggers) - return triggers, err + var problemTriggers []Trigger + err = convertTo(problemResult, &problemTriggers) + 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) { @@ -605,7 +659,7 @@ func (ds *Zabbix) GetProblems(ctx context.Context, triggerIDs []string) ([]Probl } params := ZabbixAPIParams{ - "output": []string{"eventid", "objectid", "severity", "name"}, + "output": []string{"eventid", "objectid", "severity", "name", "opdata"}, "objectids": triggerIDs, "source": "0", "object": "0", diff --git a/pkg/zabbix/models.go b/pkg/zabbix/models.go index 923e63b..9e358a2 100644 --- a/pkg/zabbix/models.go +++ b/pkg/zabbix/models.go @@ -126,5 +126,6 @@ type Problem struct { ObjectID string `json:"objectid"` Severity string `json:"severity"` Name string `json:"name"` + Opdata string `json:"opdata"` Tags []Tag `json:"tags,omitempty"` } diff --git a/src/datasource/components/VariableQueryEditor.tsx b/src/datasource/components/VariableQueryEditor.tsx index ce2ead2..daa851e 100644 --- a/src/datasource/components/VariableQueryEditor.tsx +++ b/src/datasource/components/VariableQueryEditor.tsx @@ -51,7 +51,7 @@ export class ZabbixVariableQueryEditor extends PureComponent q.value === queryType); + return this.queryTypes.find((q) => q.value === queryType) ?? this.defaults.selectedQueryType; } handleQueryUpdate = (evt: React.ChangeEvent, prop: string) => {