├── .gitignore ├── zabbixprovisioner ├── provisioner │ ├── testdata │ │ ├── testsOK │ │ │ ├── test.txt │ │ │ ├── test2.exe │ │ │ ├── rulesOKcycle_test.yml │ │ │ └── rulesOK_test.yaml │ │ └── testsErr │ │ │ ├── read │ │ │ └── rulesErr_test.yml │ │ │ └── samename │ │ │ └── rulesErrName_test.yaml │ ├── provisioner_test.go │ ├── prometheus.go │ ├── zabbix.go │ └── provisioner.go ├── Dockerfile ├── zabbixclient │ ├── host_interface.go │ ├── LICENSE │ ├── host_group.go │ ├── application.go │ ├── trigger.go │ ├── item.go │ ├── base.go │ └── host.go ├── config.yaml └── zabbixutil │ ├── LICENSE │ └── reflector.go ├── .travis.yml ├── zabbixsender ├── zabbixsvc │ ├── hosts.yaml │ ├── zabbixsvc.go │ └── zabbixsvc_test.go └── zabbixsnd │ └── zabbix_sender.go ├── .github └── dependabot.yml ├── go.mod ├── .goreleaser.yml ├── Dockerfile ├── alerts.yaml ├── Makefile ├── kubernetes-manifest.yaml ├── README.md ├── cmd └── zal │ └── main.go ├── LICENSE └── grafana.json /.gitignore: -------------------------------------------------------------------------------- 1 | /cmd/zal/zal 2 | /zal 3 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/testdata/testsOK/test.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/testdata/testsOK/test2.exe: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.x" 5 | 6 | -------------------------------------------------------------------------------- /zabbixsender/zabbixsvc/hosts.yaml: -------------------------------------------------------------------------------- 1 | received1: default1 2 | received2: default2 3 | received3: default3 4 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/testdata/testsErr/read/rulesErr_test.yml: -------------------------------------------------------------------------------- 1 | - alert: InstanceDown 2 | expr: up == 0 3 | for: 1m 4 | labels: 5 | severity: critical -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: github.com/prometheus/common 10 | versions: 11 | - 0.16.0 12 | - 0.21.0 13 | - dependency-name: github.com/sirupsen/logrus 14 | versions: 15 | - 1.7.1 16 | - 1.8.0 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/devopyio/zabbix-alertmanager 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/go-kit/kit v0.10.0 // indirect 7 | github.com/pkg/errors v0.9.1 8 | github.com/povilasv/prommod v0.0.12 9 | github.com/prometheus/client_golang v1.11.0 10 | github.com/prometheus/common v0.29.0 11 | github.com/prometheus/tsdb v0.7.1 // indirect 12 | github.com/sirupsen/logrus v1.8.1 13 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 14 | gopkg.in/yaml.v2 v2.4.0 15 | ) 16 | -------------------------------------------------------------------------------- /zabbixprovisioner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN adduser provisioner -s /bin/false -D provisioner 4 | 5 | RUN mkdir -p /etc/provisioner 6 | COPY config.yaml /etc/provisioner 7 | 8 | COPY alertmanager-zabbix-provisioner /usr/bin 9 | RUN chmod +x /usr/bin/alertmanager-zabbix-provisioner 10 | 11 | USER provisioner 12 | 13 | ENTRYPOINT ["/usr/bin/alertmanager-zabbix-provisioner"] 14 | CMD ["-config", "/etc/provisioner/config.yaml", "-alerts", "/etc/prometheus/alerts.yml"] 15 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - go mod download 6 | 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | main: cmd/zal/main.go 11 | binary: zal 12 | 13 | checksum: 14 | name_template: 'checksums.txt' 15 | snapshot: 16 | name_template: "{{ .Tag }}-next" 17 | changelog: 18 | sort: asc 19 | filters: 20 | exclude: 21 | - '^docs:' 22 | - '^test:' 23 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/host_interface.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | type ( 4 | InterfaceType int 5 | ) 6 | 7 | const ( 8 | Agent InterfaceType = 1 9 | SNMP InterfaceType = 2 10 | IPMI InterfaceType = 3 11 | JMX InterfaceType = 4 12 | ) 13 | 14 | // https://www.zabbix.com/documentation/2.2/manual/appendix/api/hostinterface/definitions 15 | type HostInterface struct { 16 | DNS string `json:"dns"` 17 | IP string `json:"ip"` 18 | Main int `json:"main"` 19 | Port string `json:"port"` 20 | Type InterfaceType `json:"type"` 21 | UseIP int `json:"useip"` 22 | } 23 | 24 | type HostInterfaces []HostInterface 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1-alpine AS build 2 | LABEL Maintainer="info@devopy.io" Description="Fully automated Zabbix and Prometheus Alertmanager integration" 3 | RUN apk update && apk add make git gcc musl-dev 4 | 5 | ADD . /go/src/github.com/devopyio/zabbix-alertmanager 6 | 7 | WORKDIR /go/src/github.com/devopyio/zabbix-alertmanager 8 | 9 | ENV GO111MODULE on 10 | RUN make build 11 | RUN mv zal /zal 12 | 13 | FROM alpine:latest 14 | 15 | RUN apk add --no-cache ca-certificates && mkdir /app 16 | RUN adduser zal -u 1001 -g 1001 -s /bin/false -D zal 17 | 18 | COPY --from=build /zal /usr/bin 19 | RUN chown -R zal /usr/bin/zal 20 | 21 | USER zal 22 | ENTRYPOINT ["/usr/bin/zal"] 23 | -------------------------------------------------------------------------------- /zabbixprovisioner/config.yaml: -------------------------------------------------------------------------------- 1 | # Name of the host in zabbix 2 | - name: infra 3 | hostGroups: 4 | - prometheus 5 | # tag and deploymentStatus are inventory fields currently supported for an host 6 | tag: prometheus 7 | deploymentStatus: 0 8 | # itemDefault* below, defines item values when not specified in a rule 9 | itemDefaultApplication: prometheus 10 | # For history and trends in zabbix 2.x you have to put those in days like 7 or 90 11 | itemDefaultHistory: 5d 12 | itemDefaultTrends: 5d 13 | itemDefaultTrapperHosts: # Hosts permitted to send data (your webhook external CIDR, default is from everywhere) 14 | # Path to the alerts containing folder 15 | alertsDir: ./kubernetes-alerts/infra 16 | -------------------------------------------------------------------------------- /alerts.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: zal 3 | rules: 4 | - alert: DeadMansSwitch 5 | annotations: 6 | description: This is a DeadMansSwitch meant to ensure that the entire alerting 7 | pipeline is functional. 8 | summary: Alerting DeadMansSwitch 9 | zabbix_trigger_nodata: "600" 10 | expr: vector(1) 11 | labels: 12 | severity: critical 13 | - alert: ZabbixAlertmanagerErrors 14 | expr: sum(rate(alerts_errors_total{name="zal"}[5m])) > 0 15 | annotations: 16 | description: ZAL sender is having issues sending alerts to Zabbix. Please investigate. 17 | grafana_url: http://GRAFANA_URL/d/maYkrFemz/zabbix-alertmanager?orgId=1&from=now-30m&to=now 18 | labels: 19 | severity: critical 20 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/testdata/testsOK/rulesOKcycle_test.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: testing1 3 | rules: 4 | - alert: Instance5 5 | expr: up == 0 6 | for: 1m 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: "Instance {{ $labels.instance }} down" 11 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." 12 | - alert: Instance6 13 | expr: up == 0 14 | for: 1m 15 | labels: 16 | severity: critical 17 | annotations: 18 | summary: "Instance {{ $labels.instance }} down" 19 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." 20 | - alert: 21 | expr: up == 0 22 | for: 1m 23 | labels: 24 | severity: critical 25 | annotations: 26 | summary: "Instance {{ $labels.instance }} down" 27 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." 28 | - name: testing1 29 | rules: 30 | - alert: Instance7 31 | expr: up == 0 32 | for: 1m 33 | labels: 34 | severity: critical 35 | annotations: 36 | summary: "Instance {{ $labels.instance }} down" 37 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Alexey Palazhchenko. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixutil/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Alexey Palazhchenko. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/provisioner_test.go: -------------------------------------------------------------------------------- 1 | package provisioner_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/provisioner" 7 | ) 8 | 9 | const ( 10 | rulesOKpath = "./testdata/testsOK/" 11 | rulesErrReadFile = "./testdata/testsErr/read/" 12 | rulesErrOpenDir = "" 13 | rulesErrSameName = "./testdata/testsErr/samename/" 14 | ) 15 | 16 | func TestLoadPrometheusRulesFromPathOK(t *testing.T) { 17 | expected := 7 18 | rules, err := provisioner.LoadPrometheusRulesFromDir(rulesOKpath) 19 | if err != nil { 20 | t.Error("Expected to work, got :", err) 21 | } 22 | if len(rules) != expected { 23 | t.Errorf("Expeceted to get %d rules, but got %d", expected, len(rules)) 24 | } 25 | } 26 | 27 | func TestLoadPrometheusRulesFromPathErrorReadFile(t *testing.T) { 28 | _, err := provisioner.LoadPrometheusRulesFromDir(rulesErrReadFile) 29 | if err == nil { 30 | t.Error("Expected to get error, got :", err) 31 | } 32 | } 33 | 34 | func TestLoadPrometheusRulesFromPathErrorOpenDir(t *testing.T) { 35 | _, err := provisioner.LoadPrometheusRulesFromDir(rulesErrOpenDir) 36 | if err == nil { 37 | t.Error("Expected to get error, got :", err) 38 | } 39 | } 40 | 41 | func TestLoadPrometheusRulesFromPathErrorSameName(t *testing.T) { 42 | _, err := provisioner.LoadPrometheusRulesFromDir(rulesErrSameName) 43 | if err == nil { 44 | t.Error("Expected to get error, got :", err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/testdata/testsOK/rulesOK_test.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: testing1 3 | rules: 4 | - alert: Instance1 5 | expr: up == 0 6 | for: 1m 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: "Instance {{ $labels.instance }} down" 11 | description: "{{ $labels.instance }} of job has been down for more than 1 minute." 12 | - name: testing2 13 | rules: 14 | - alert: Instance2 15 | expr: up == 0 16 | for: 1m 17 | labels: 18 | severity: critical 19 | annotations: 20 | summary: "Instance {{ $labels.instance }} down" 21 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." 22 | - name: testing3 23 | rules: 24 | - alert: Instance3 25 | expr: up == 0 26 | for: 1m 27 | labels: 28 | severity: critical 29 | annotations: 30 | summary: "Instance {{ $labels.instance }} down" 31 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." 32 | - name: testing4 33 | rules: 34 | - alert: Instance4 35 | expr: up == 0 36 | for: 1m 37 | labels: 38 | severity: resolved 39 | annotations: 40 | summary: "Instance {{ $labels.instance }} down" 41 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/testdata/testsErr/samename/rulesErrName_test.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: testing1 3 | rules: 4 | - alert: Instance 5 | expr: up == 0 6 | for: 1m 7 | labels: 8 | severity: critical 9 | annotations: 10 | summary: "Instance {{ $labels.instance }} down" 11 | description: "{{ $labels.instance }} of job has been down for more than 1 minute." 12 | - name: testing2 13 | rules: 14 | - alert: Instance 15 | expr: up == 0 16 | for: 1m 17 | labels: 18 | severity: critical 19 | annotations: 20 | summary: "Instance {{ $labels.instance }} down" 21 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." 22 | - name: testing3 23 | rules: 24 | - alert: Instance3 25 | expr: up == 0 26 | for: 1m 27 | labels: 28 | severity: critical 29 | annotations: 30 | summary: "Instance {{ $labels.instance }} down" 31 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." 32 | - name: testing4 33 | rules: 34 | - alert: Instance4 35 | expr: up == 0 36 | for: 1m 37 | labels: 38 | severity: resolved 39 | annotations: 40 | summary: "Instance {{ $labels.instance }} down" 41 | description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute." -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DATE := $(shell date +%FT%T%z) 2 | USER := $(shell whoami) 3 | GIT_HASH := $(shell git --no-pager describe --tags --always) 4 | BRANCH := $(shell git branch | grep \* | cut -d ' ' -f2) 5 | DOCKER_IMAGE := zabbix-alertmanager 6 | 7 | lint_flags := run --deadline=120s 8 | linter := ./bin/golangci-lint 9 | testflags := -v -cover 10 | 11 | GO111MODULE := on 12 | all: $(linter) deps test build docker-push 13 | 14 | $(linter): 15 | curl -sfl https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s v1.15.0 16 | 17 | .phony: lint 18 | lint: $(linter) 19 | $(linter) $(lint_flags) ./... 20 | 21 | .PHONY: deps 22 | deps: 23 | go mod download 24 | 25 | .PHONY: test 26 | test: 27 | $(BUILDENV) go test $(TESTFLAGS) ./... 28 | 29 | .PHONY: build 30 | build: deps 31 | CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-s -X github.com/prometheus/common/version.Version=$(GIT_HASH) -X github.com/prometheus/common/version.BuildDate="$(DATE)" -X github.com/prometheus/common/version.Branch=$(BRANCH) -X github.com/prometheus/common/version.Revision=$(GIT_HASH) -X github.com/prometheus/common/version.BuildUser=$(USER) -extldflags "-static"' ./cmd/zal/ 32 | 33 | docker-build: 34 | docker build . -t $(DOCKER_IMAGE) 35 | 36 | docker-login: 37 | docker login -u $(DOCKER_USERNAME) -p $(DOCKER_PASSWORD) 38 | 39 | docker-push: docker-build docker-login 40 | docker tag $(DOCKER_IMAGE) $(DOCKER_USERNAME)/$(DOCKER_IMAGE):$(GIT_HASH) 41 | docker push $(DOCKER_USERNAME)/$(DOCKER_IMAGE):$(GIT_HASH) 42 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/prometheus.go: -------------------------------------------------------------------------------- 1 | package provisioner 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | yaml "gopkg.in/yaml.v2" 10 | ) 11 | 12 | type PrometheusAlertRules struct { 13 | Groups []struct { 14 | Rules []PrometheusRule `yaml:"rules"` 15 | } `yaml:"groups"` 16 | } 17 | 18 | type PrometheusRule struct { 19 | Name string `yaml:"alert"` 20 | Annotations map[string]string `yaml:"annotations"` 21 | Expression string `yaml:"expr"` 22 | Labels map[string]string `yaml:"labels"` 23 | } 24 | 25 | func LoadPrometheusRulesFromDir(dir string) ([]PrometheusRule, error) { 26 | filesInDir, err := ioutil.ReadDir(dir) 27 | if err != nil { 28 | return nil, errors.Wrapf(err, "can't open the alerts files directory") 29 | } 30 | 31 | var rules []PrometheusRule 32 | 33 | for _, file := range filesInDir { 34 | if strings.HasSuffix(file.Name(), ".yml") || strings.HasSuffix(file.Name(), ".yaml") { 35 | alertsFile, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) 36 | if err != nil { 37 | return nil, errors.Wrapf(err, "can't open the alerts file: %s", file.Name()) 38 | } 39 | 40 | ruleConfig := PrometheusAlertRules{} 41 | 42 | err = yaml.Unmarshal(alertsFile, &ruleConfig) 43 | if err != nil { 44 | return nil, errors.Wrapf(err, "can't read the alerts file: %s", file.Name()) 45 | } 46 | for _, rule := range ruleConfig.Groups { 47 | for _, alert := range rule.Rules { 48 | if alert.Name != "" { 49 | rules = append(rules, alert) 50 | } 51 | } 52 | } 53 | 54 | } 55 | } 56 | 57 | for i := 0; i < len(rules); i++ { 58 | for j := i + 1; j < len(rules); j++ { 59 | if rules[j].Name == rules[i].Name { 60 | return nil, errors.Errorf("can't load rules with the same alertname: %v, index: %v, %v", rules[j].Name, i+1, j+1) 61 | } 62 | } 63 | } 64 | 65 | return rules, nil 66 | } 67 | -------------------------------------------------------------------------------- /kubernetes-manifest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zal 5 | labels: 6 | name: zal 7 | annotations: 8 | prometheus.io/path: /metrics 9 | prometheus.io/scrape: "true" 10 | spec: 11 | ports: 12 | - port: 9095 13 | protocol: TCP 14 | targetPort: 9095 15 | selector: 16 | k8s-app: zal 17 | --- 18 | apiVersion: policy/v1beta1 19 | kind: PodDisruptionBudget 20 | metadata: 21 | name: zal 22 | spec: 23 | minAvailable: 1 24 | selector: 25 | matchLabels: 26 | k8s-app: zal 27 | --- 28 | apiVersion: apps/v1 29 | kind: Deployment 30 | metadata: 31 | labels: 32 | k8s-app: zal 33 | name: zal 34 | spec: 35 | replicas: 2 36 | selector: 37 | matchLabels: 38 | k8s-app: zal 39 | template: 40 | metadata: 41 | labels: 42 | name: zal 43 | k8s-app: zal 44 | spec: 45 | affinity: 46 | podAntiAffinity: 47 | preferredDuringSchedulingIgnoredDuringExecution: 48 | - podAffinityTerm: 49 | labelSelector: 50 | matchExpressions: 51 | - key: k8s-app 52 | operator: In 53 | values: 54 | - zal 55 | topologyKey: kubernetes.io/hostname 56 | weight: 100 57 | containers: 58 | - name: zal 59 | image: quay.io/devopyio/zabbix-alertmanager:v1.2.0 60 | args: 61 | - send 62 | - --log.level=info 63 | - --zabbix-addr=ZABBIX_ADDR:10051 64 | - --default-host=infra 65 | - --hosts-path=/etc/zal/sender-config.yml 66 | ports: 67 | - containerPort: 9095 68 | volumeMounts: 69 | - name: config-volume 70 | mountPath: /etc/zal 71 | volumes: 72 | - name: config-volume 73 | configMap: 74 | name: zal 75 | --- 76 | apiVersion: v1 77 | kind: ConfigMap 78 | metadata: 79 | name: zal 80 | namespace: sys-mon 81 | data: 82 | sender-config.yml: |- 83 | # Resolver name to zabbix host mapping 84 | # web is Alertmanager's resolver name 85 | # and the mapping is Zabbix host name. 86 | infra: infra 87 | web: webteam 88 | --- 89 | 90 | -------------------------------------------------------------------------------- /zabbixsender/zabbixsnd/zabbix_sender.go: -------------------------------------------------------------------------------- 1 | package zabbixsnd 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net" 8 | "time" 9 | ) 10 | 11 | var Header = []byte("ZBXD\x01") 12 | 13 | type Metric struct { 14 | Host string `json:"host"` 15 | Key string `json:"key"` 16 | Value string `json:"value"` 17 | Clock int64 `json:"clock"` 18 | } 19 | 20 | type Packet struct { 21 | Request string `json:"request"` 22 | Data []*Metric `json:"data"` 23 | Clock int64 `json:"clock"` 24 | } 25 | 26 | //NewPacket creates new packet 27 | func NewPacket(data []*Metric, clock ...int64) *Packet { 28 | p := &Packet{Request: `sender data`, Data: data} 29 | // use current time, if `clock` is not specified 30 | if p.Clock = time.Now().Unix(); len(clock) > 0 { 31 | p.Clock = int64(clock[0]) 32 | } 33 | return p 34 | } 35 | 36 | // DataLen Packet return 8 bytes with packet length in little endian order 37 | func (p *Packet) DataLen() ([]byte, error) { 38 | dataLen := make([]byte, 8) 39 | JSONData, err := json.Marshal(p) 40 | if err != nil { 41 | return nil, err 42 | } 43 | binary.LittleEndian.PutUint32(dataLen, uint32(len(JSONData))) 44 | return dataLen, nil 45 | } 46 | 47 | // Sender sends data to zabbix 48 | // Read more: https://www.zabbix.com/documentation/3.4/manual/config/items/itemtypes/trapper 49 | type Sender struct { 50 | addr *net.TCPAddr 51 | } 52 | 53 | // New creates new sender 54 | func New(addr string) (*Sender, error) { 55 | tcpAddr, err := net.ResolveTCPAddr("tcp", addr) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &Sender{ 61 | addr: tcpAddr, 62 | }, nil 63 | } 64 | 65 | // Send method Sender class, send packet to zabbix 66 | func (s *Sender) Send(packet *Packet) ([]byte, error) { 67 | conn, err := net.DialTCP("tcp", nil, s.addr) 68 | if err != nil { 69 | return nil, err 70 | } 71 | defer conn.Close() 72 | 73 | dataPacket, err := json.Marshal(packet) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | datalen, err := packet.DataLen() 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | buffer := append(Header, datalen...) 84 | buffer = append(buffer, dataPacket...) 85 | 86 | _, err = conn.Write(buffer) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | res, err := ioutil.ReadAll(conn) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return res, nil 97 | } 98 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/host_group.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | reflector "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/zabbixutil" 5 | ) 6 | 7 | type ( 8 | InternalType int 9 | ) 10 | 11 | const ( 12 | NotInternal InternalType = 0 13 | Internal InternalType = 1 14 | ) 15 | 16 | // https://www.zabbix.com/documentation/2.2/manual/appendix/api/hostgroup/definitions 17 | type HostGroup struct { 18 | GroupId string `json:"groupid,omitempty"` 19 | Name string `json:"name"` 20 | Internal InternalType `json:"internal,omitempty"` 21 | } 22 | 23 | type HostGroups []HostGroup 24 | 25 | type HostGroupId struct { 26 | GroupId string `json:"groupid"` 27 | } 28 | 29 | type HostGroupIds []HostGroupId 30 | 31 | // Wrapper for hostgroup.get: https://www.zabbix.com/documentation/2.2/manual/appendix/api/hostgroup/get 32 | func (api *API) HostGroupsGet(params Params) (HostGroups, error) { 33 | var res HostGroups 34 | if _, present := params["output"]; !present { 35 | params["output"] = "extend" 36 | } 37 | response, err := api.CallWithError("hostgroup.get", params) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | reflector.MapsToStructs2(response.Result.([]interface{}), &res, reflector.Strconv, "json") 43 | return res, nil 44 | } 45 | 46 | // Gets host group by Id only if there is exactly 1 matching host group. 47 | func (api *API) HostGroupGetById(id string) (*HostGroup, error) { 48 | var res *HostGroup 49 | groups, err := api.HostGroupsGet(Params{"groupids": id}) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | if len(groups) == 1 { 55 | res = &groups[0] 56 | } else { 57 | e := ExpectedOneResult(len(groups)) 58 | err = &e 59 | return nil, err 60 | } 61 | return res, nil 62 | } 63 | 64 | // Wrapper for hostgroup.create: https://www.zabbix.com/documentation/2.2/manual/appendix/api/hostgroup/create 65 | func (api *API) HostGroupsCreate(hostGroups HostGroups) error { 66 | response, err := api.CallWithError("hostgroup.create", hostGroups) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | result := response.Result.(map[string]interface{}) 72 | groupids := result["groupids"].([]interface{}) 73 | for i, id := range groupids { 74 | hostGroups[i].GroupId = id.(string) 75 | } 76 | return nil 77 | } 78 | 79 | // Wrapper for hostgroup.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/hostgroup/delete 80 | // Cleans GroupId in all hostGroups elements if call succeed. 81 | func (api *API) HostGroupsDelete(hostGroups HostGroups) error { 82 | ids := make([]string, len(hostGroups)) 83 | for i, group := range hostGroups { 84 | ids[i] = group.GroupId 85 | } 86 | 87 | err := api.HostGroupsDeleteByIds(ids) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | for i := range hostGroups { 93 | hostGroups[i].GroupId = "" 94 | } 95 | 96 | return nil 97 | } 98 | 99 | // Wrapper for hostgroup.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/hostgroup/delete 100 | func (api *API) HostGroupsDeleteByIds(ids []string) error { 101 | response, err := api.CallWithError("hostgroup.delete", ids) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | result := response.Result.(map[string]interface{}) 107 | groupids := result["groupids"].([]interface{}) 108 | if len(ids) != len(groupids) { 109 | err = &ExpectedMore{len(ids), len(groupids)} 110 | return err 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/application.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | reflector "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/zabbixutil" 5 | ) 6 | 7 | // https://www.zabbix.com/documentation/2.2/manual/appendix/api/application/definitions 8 | type Application struct { 9 | ApplicationId string `json:"applicationid,omitempty"` 10 | HostId string `json:"hostid"` 11 | Name string `json:"name"` 12 | TemplateId string `json:"templateid,omitempty"` 13 | } 14 | 15 | type Applications []Application 16 | 17 | // Wrapper for application.get: https://www.zabbix.com/documentation/2.2/manual/appendix/api/application/get 18 | func (api *API) ApplicationsGet(params Params) (Applications, error) { 19 | var res Applications 20 | if _, present := params["output"]; !present { 21 | params["output"] = "extend" 22 | } 23 | response, err := api.CallWithError("application.get", params) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | reflector.MapsToStructs2(response.Result.([]interface{}), &res, reflector.Strconv, "json") 29 | return res, nil 30 | } 31 | 32 | // Gets application by Id only if there is exactly 1 matching application. 33 | func (api *API) ApplicationGetById(id string) (*Application, error) { 34 | var res *Application 35 | apps, err := api.ApplicationsGet(Params{"applicationids": id}) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | if len(apps) == 1 { 41 | res = &apps[0] 42 | } else { 43 | e := ExpectedOneResult(len(apps)) 44 | err = &e 45 | return nil, err 46 | } 47 | return res, nil 48 | } 49 | 50 | // Gets application by host Id and name only if there is exactly 1 matching application. 51 | func (api *API) ApplicationGetByHostIdAndName(hostId, name string) (*Application, error) { 52 | var res *Application 53 | apps, err := api.ApplicationsGet(Params{"hostids": hostId, "filter": map[string]string{"name": name}}) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if len(apps) == 1 { 59 | res = &apps[0] 60 | } else { 61 | e := ExpectedOneResult(len(apps)) 62 | err = &e 63 | return nil, err 64 | } 65 | return res, nil 66 | } 67 | 68 | // Wrapper for application.create: https://www.zabbix.com/documentation/2.2/manual/appendix/api/application/create 69 | func (api *API) ApplicationsCreate(apps Applications) error { 70 | response, err := api.CallWithError("application.create", apps) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | result := response.Result.(map[string]interface{}) 76 | applicationids := result["applicationids"].([]interface{}) 77 | for i, id := range applicationids { 78 | apps[i].ApplicationId = id.(string) 79 | } 80 | return nil 81 | } 82 | 83 | // Wrapper for application.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/application/delete 84 | // Cleans ApplicationId in all apps elements if call succeed. 85 | func (api *API) ApplicationsDelete(apps Applications) error { 86 | ids := make([]string, len(apps)) 87 | for i, app := range apps { 88 | ids[i] = app.ApplicationId 89 | } 90 | 91 | err := api.ApplicationsDeleteByIds(ids) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | for i := range apps { 97 | apps[i].ApplicationId = "" 98 | } 99 | return nil 100 | 101 | } 102 | 103 | // Wrapper for application.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/application/delete 104 | func (api *API) ApplicationsDeleteByIds(ids []string) error { 105 | response, err := api.CallWithError("application.delete", ids) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | result := response.Result.(map[string]interface{}) 111 | applicationids := result["applicationids"].([]interface{}) 112 | if len(ids) != len(applicationids) { 113 | err = &ExpectedMore{len(ids), len(applicationids)} 114 | return err 115 | } 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/trigger.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | reflector "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/zabbixutil" 5 | ) 6 | 7 | type ( 8 | PriorityType int 9 | ) 10 | 11 | const ( 12 | NotClassified PriorityType = 0 13 | Information PriorityType = 1 14 | Warning PriorityType = 2 15 | Average PriorityType = 3 16 | High PriorityType = 4 17 | Critical PriorityType = 5 18 | 19 | Enabled StatusType = 0 20 | Disabled StatusType = 1 21 | 22 | OK ValueType = 0 23 | Problem ValueType = 1 24 | ) 25 | 26 | // https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/definitions 27 | type Trigger struct { 28 | TriggerId string `json:"triggerid,omitempty"` 29 | Description string `json:"description"` 30 | Expression string `json:"expression"` 31 | Comments string `json:"comments"` 32 | URL string `json:"url"` 33 | ManualClose int32 `json:"manual_close"` 34 | Value ValueType `json:",omitempty"` 35 | Priority PriorityType `json:"priority"` 36 | Status StatusType `json:"status"` 37 | Tags []Tag `json:"tags"` 38 | } 39 | 40 | type Tag struct { 41 | Tag string `json:"tag"` 42 | Value string `json:"value"` 43 | } 44 | type Triggers []Trigger 45 | 46 | // Wrapper for item.get https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/get 47 | func (api *API) TriggersGet(params Params) (Triggers, error) { 48 | var res Triggers 49 | if _, present := params["output"]; !present { 50 | params["output"] = "extend" 51 | } 52 | response, err := api.CallWithError("trigger.get", params) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | reflector.MapsToStructs2(response.Result.([]interface{}), &res, reflector.Strconv, "json") 58 | return res, nil 59 | } 60 | 61 | // Wrapper for item.create: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/create 62 | func (api *API) TriggersCreate(triggers Triggers) error { 63 | response, err := api.CallWithError("trigger.create", triggers) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | result := response.Result.(map[string]interface{}) 69 | triggerids := result["triggerids"].([]interface{}) 70 | for i, id := range triggerids { 71 | triggers[i].TriggerId = id.(string) 72 | } 73 | return nil 74 | } 75 | 76 | // Wrapper for item.update: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/update 77 | func (api *API) TriggersUpdate(triggers Triggers) error { 78 | _, err := api.CallWithError("trigger.update", triggers) 79 | if err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | 85 | // Wrapper for item.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/delete 86 | // Cleans ItemId in all items elements if call succeed. 87 | func (api *API) TriggersDelete(triggers Triggers) error { 88 | ids := make([]string, len(triggers)) 89 | for i, trigger := range triggers { 90 | ids[i] = trigger.TriggerId 91 | } 92 | 93 | err := api.TriggersDeleteByIds(ids) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | for i := range triggers { 99 | triggers[i].TriggerId = "" 100 | } 101 | 102 | return nil 103 | } 104 | 105 | // Wrapper for item.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/delete 106 | func (api *API) TriggersDeleteByIds(ids []string) error { 107 | response, err := api.CallWithError("trigger.delete", ids) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | result := response.Result.(map[string]interface{}) 113 | triggerids1, ok := result["triggerids"].([]interface{}) 114 | l := len(triggerids1) 115 | if !ok { 116 | // some versions actually return map there 117 | triggerids2 := result["triggerids"].(map[string]interface{}) 118 | l = len(triggerids2) 119 | } 120 | if len(ids) != l { 121 | err = &ExpectedMore{len(ids), l} 122 | return err 123 | } 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![zabbix-alertmanager](http://devopy.io/wp-content/uploads/2019/02/zal-200.png) 2 | 3 | # zabbix-alertmanager 4 | 5 | ![Build Status](https://travis-ci.com/devopyio/zabbix-alertmanager.svg?branch=master) 6 | ![Go Report Card](https://goreportcard.com/badge/github.com/devopyio/zabbix-alertmanager) 7 | [![Docker Repository on Quay](https://quay.io/repository/devopyio/zabbix-alertmanager/status "Docker Repository on Quay")](https://quay.io/repository/devopyio/zabbix-alertmanager) 8 | 9 | Fully automated [Zabbix](https://www.zabbix.com/) and [Prometheus Alertmanager](https://prometheus.io/docs/alerting/alertmanager/) integration. 10 | 11 | ## Tutorials 12 | 13 | [Introducing ZAL - Zabbix Alertmanager Integration](https://devopy.io/zabbix-alertmanager-integration/) 14 | 15 | [Setting Up Zabbix Alertmanager integration](http://devopy.io/setting-up-zabbix-alertmanager-integration/) 16 | 17 | [Running Zabbix Alertmanager integration](http://devopy.io/) 18 | 19 | 20 | ## Deployment 21 | 22 | Checkout [kubernetes-manifests.yaml](https://github.com/devopyio/zabbix-alertmanager/blob/master/kubernetes-manifest.yaml) for deployment in Kubernetes. 23 | 24 | [Releases](https://github.com/devopyio/zabbix-alertmanager/releases) page for binaries. 25 | 26 | [grafana.json](https://github.com/devopyio/zabbix-alertmanager/blob/master/grafana.json) for Grafana dashboard. 27 | 28 | [alerts.yaml](https://github.com/devopyio/zabbix-alertmanager/blob/master/alerts.yaml) for Prometheus alerts. 29 | 30 | ## General Info 31 | 32 | Project consists of 2 components: 33 | 34 | ## 1. zal send 35 | 36 | `zal send` command, which listens for Alert requests from Alertmanager and sends them to Zabbix. 37 | 38 | Run `zal send --help` to see possible options. Consult [Setting Up Zabbix Alertmanager integration](http://devopy.io/setting-up-zabbix-alertmanager-integration/) for step by step tutorial. 39 | 40 | ## 2. zal prov 41 | 42 | `zal prov` command, which reads Prometheus Alerting rules and converts them into Zabbix Triggers. 43 | 44 | Run the `zal prov --help` to get the instructions. 45 | 46 | ## Usage 47 | 48 | ``` 49 | usage: zal [] [ ...] 50 | 51 | Zabbix and Prometheus integration. 52 | 53 | Flags: 54 | -h, --help Show context-sensitive help (also try --help-long and --help-man). 55 | --version Show application version. 56 | --log.level=info Log level. 57 | --log.format=text Log format. 58 | 59 | Commands: 60 | help [...] 61 | Show help. 62 | 63 | send --zabbix-addr=ZABBIX-ADDR [] 64 | Listens for Alert requests from Alertmanager and sends them to Zabbix. 65 | 66 | prov --config-path=CONFIG-PATH --user=USER --password=PASSWORD [] 67 | Reads Prometheus Alerting rules and converts them into Zabbix Triggers. 68 | ``` 69 | 70 | ## Zal send 71 | 72 | ``` 73 | usage: zal send --zabbix-addr=ZABBIX-ADDR [] 74 | 75 | Listens for Alert requests from Alertmanager and sends them to Zabbix. 76 | 77 | Flags: 78 | -h, --help Show context-sensitive help (also try --help-long and --help-man). 79 | --version Show application version. 80 | --log.level=info Log level. 81 | --log.format=text Log format. 82 | --addr="0.0.0.0:9095" Server address which will receive alerts from alertmanager. 83 | --zabbix-addr=ZABBIX-ADDR Zabbix address. 84 | --hosts-path=HOSTS-PATH Path to resolver to host mapping file. 85 | --key-prefix="prometheus" Prefix to add to the trapper item key 86 | --default-host="prometheus" 87 | default host to send alerts to 88 | 89 | ``` 90 | 91 | ## Zal prov 92 | ``` 93 | usage: zal prov --config-path=CONFIG-PATH --user=USER --password=PASSWORD [] 94 | 95 | Reads Prometheus Alerting rules and converts them into Zabbix Triggers. 96 | 97 | Flags: 98 | -h, --help Show context-sensitive help (also try --help-long and --help-man). 99 | --version Show application version. 100 | --log.level=info Log level. 101 | --log.format=text Log format. 102 | --config-path=CONFIG-PATH Path to provisioner hosts config file. 103 | --user=USER Zabbix json rpc user. 104 | --password=PASSWORD Zabbix json rpc password. 105 | --url="http://127.0.0.1/zabbix/api_jsonrpc.php" 106 | Zabbix json rpc url. 107 | --key-prefix="prometheus" Prefix to add to the trapper item key. 108 | --prometheus-url="" Prometheus URL. 109 | ``` 110 | -------------------------------------------------------------------------------- /cmd/zal/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | 11 | "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/provisioner" 12 | "github.com/devopyio/zabbix-alertmanager/zabbixsender/zabbixsnd" 13 | "github.com/devopyio/zabbix-alertmanager/zabbixsender/zabbixsvc" 14 | "github.com/povilasv/prommod" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | ver "github.com/prometheus/common/version" 18 | log "github.com/sirupsen/logrus" 19 | kingpin "gopkg.in/alecthomas/kingpin.v2" 20 | ) 21 | 22 | func main() { 23 | app := kingpin.New("zal", "Zabbix and Prometheus integration.") 24 | 25 | app.Version(ver.Print("zal")) 26 | app.HelpFlag.Short('h') 27 | 28 | send := app.Command("send", "Listens for Alert requests from Alertmanager and sends them to Zabbix.") 29 | senderAddr := send.Flag("addr", "Server address which will receive alerts from alertmanager.").Default("0.0.0.0:9095").String() 30 | zabbixAddr := send.Flag("zabbix-addr", "Zabbix address.").Envar("ZABBIX_URL").Required().String() 31 | hostsFile := send.Flag("hosts-path", "Path to resolver to host mapping file.").String() 32 | keyPrefix := send.Flag("key-prefix", "Prefix to add to the trapper item key").Default("prometheus").String() 33 | defaultHost := send.Flag("default-host", "default host to send alerts to").Default("prometheus").String() 34 | 35 | prov := app.Command("prov", "Reads Prometheus Alerting rules and converts them into Zabbix Triggers.") 36 | provConfig := prov.Flag("config-path", "Path to provisioner hosts config file.").Required().String() 37 | provUser := prov.Flag("user", "Zabbix json rpc user.").Envar("ZABBIX_USER").Required().String() 38 | provPassword := prov.Flag("password", "Zabbix json rpc password.").Envar("ZABBIX_PASSWORD").Required().String() 39 | provURL := prov.Flag("url", "Zabbix json rpc url.").Envar("ZABBIX_URL").Default("http://127.0.0.1/zabbix/api_jsonrpc.php").String() 40 | provKeyPrefix := prov.Flag("key-prefix", "Prefix to add to the trapper item key.").Default("prometheus").String() 41 | prometheusURL := prov.Flag("prometheus-url", "Prometheus URL.").Default("").String() 42 | 43 | logLevel := app.Flag("log.level", "Log level."). 44 | Default("info").Enum("error", "warn", "info", "debug") 45 | logFormat := app.Flag("log.format", "Log format."). 46 | Default("text").Enum("text", "json") 47 | 48 | cmd := kingpin.MustParse(app.Parse(os.Args[1:])) 49 | 50 | switch strings.ToLower(*logLevel) { 51 | case "error": 52 | log.SetLevel(log.ErrorLevel) 53 | case "warn": 54 | log.SetLevel(log.WarnLevel) 55 | case "info": 56 | log.SetLevel(log.InfoLevel) 57 | case "debug": 58 | log.SetLevel(log.DebugLevel) 59 | } 60 | 61 | switch strings.ToLower(*logFormat) { 62 | case "json": 63 | log.SetFormatter(&log.JSONFormatter{}) 64 | case "text": 65 | log.SetFormatter(&log.TextFormatter{DisableColors: true}) 66 | } 67 | log.SetOutput(os.Stdout) 68 | 69 | prometheus.MustRegister(ver.NewCollector("zal")) 70 | prometheus.MustRegister(prommod.NewCollector("zal")) 71 | switch cmd { 72 | case send.FullCommand(): 73 | s, err := zabbixsnd.New(*zabbixAddr) 74 | if err != nil { 75 | log.Fatalf("error could not create zabbix sender: %v", err) 76 | } 77 | 78 | hosts := make(map[string]string) 79 | 80 | if hostsFile != nil && *hostsFile != "" { 81 | hosts, err = zabbixsvc.LoadHostsFromFile(*hostsFile) 82 | if err != nil { 83 | log.Errorf("cant load the default hosts file: %v", err) 84 | } 85 | } 86 | 87 | h := &zabbixsvc.JSONHandler{ 88 | Sender: s, 89 | KeyPrefix: *keyPrefix, 90 | DefaultHost: *defaultHost, 91 | Hosts: hosts, 92 | } 93 | 94 | http.Handle("/metrics", promhttp.Handler()) 95 | http.HandleFunc("/alerts", h.HandlePost) 96 | 97 | log.Info("Zabbix sender started, listening on ", *senderAddr) 98 | if err := http.ListenAndServe(*senderAddr, nil); err != nil { 99 | log.Fatal(err) 100 | } 101 | 102 | case prov.FullCommand(): 103 | cfg, err := provisioner.LoadHostConfigFromFile(*provConfig) 104 | if err != nil { 105 | log.Fatal(err) 106 | } 107 | log.Infof("loaded hosts configuration from '%s'", *provConfig) 108 | 109 | prov, err := provisioner.New(*prometheusURL, *provKeyPrefix, *provURL, *provUser, *provPassword, cfg) 110 | if err != nil { 111 | log.Fatalf("error failed to create provisioner: %s", err) 112 | } 113 | 114 | if err := prov.Run(); err != nil { 115 | log.Fatalf("error provisioning zabbix items: %s", err) 116 | } 117 | } 118 | } 119 | 120 | func interrupt(logger log.Logger, cancel <-chan struct{}) error { 121 | c := make(chan os.Signal, 1) 122 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) 123 | select { 124 | case s := <-c: 125 | log.Info("caught signal. Exiting.", "signal", s) 126 | return nil 127 | case <-cancel: 128 | return errors.New("canceled") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/item.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "fmt" 5 | 6 | reflector "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/zabbixutil" 7 | ) 8 | 9 | type ( 10 | ItemType int 11 | ValueType int 12 | DataType int 13 | DeltaType int 14 | ) 15 | 16 | const ( 17 | ZabbixAgent ItemType = 0 18 | SNMPv1Agent ItemType = 1 19 | ZabbixTrapper ItemType = 2 20 | SimpleCheck ItemType = 3 21 | SNMPv2Agent ItemType = 4 22 | ZabbixInternal ItemType = 5 23 | SNMPv3Agent ItemType = 6 24 | ZabbixAgentActive ItemType = 7 25 | ZabbixAggregate ItemType = 8 26 | WebItem ItemType = 9 27 | ExternalCheck ItemType = 10 28 | DatabaseMonitor ItemType = 11 29 | IPMIAgent ItemType = 12 30 | SSHAgent ItemType = 13 31 | TELNETAgent ItemType = 14 32 | Calculated ItemType = 15 33 | JMXAgent ItemType = 16 34 | 35 | Float ValueType = 0 36 | Character ValueType = 1 37 | Log ValueType = 2 38 | Unsigned ValueType = 3 39 | Text ValueType = 4 40 | 41 | Decimal DataType = 0 42 | Octal DataType = 1 43 | Hexadecimal DataType = 2 44 | Boolean DataType = 3 45 | 46 | AsIs DeltaType = 0 47 | Speed DeltaType = 1 48 | Delta DeltaType = 2 49 | ) 50 | 51 | // https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/definitions 52 | type Item struct { 53 | ItemId string `json:"itemid,omitempty"` 54 | Delay int `json:"delay"` 55 | HostId string `json:"hostid"` 56 | InterfaceId string `json:"interfaceid,omitempty"` 57 | Key string `json:"key_"` 58 | Name string `json:"name"` 59 | Type ItemType `json:"type"` 60 | ValueType ValueType `json:"value_type"` 61 | DataType DataType `json:"data_type"` 62 | Delta DeltaType `json:"delta"` 63 | Description string `json:"description"` 64 | Error string `json:"error"` 65 | History string `json:"history,omitempty"` 66 | Trends string `json:"trends,omitempty"` 67 | TrapperHosts string `json:"trapper_hosts,omitempty"` 68 | 69 | ApplicationIds []string `json:"applications,omitempty"` 70 | } 71 | 72 | type Items []Item 73 | 74 | // Converts slice to map by key. Panics if there are duplicate keys. 75 | func (items Items) ByKey() map[string]Item { 76 | res := make(map[string]Item, len(items)) 77 | for _, i := range items { 78 | _, present := res[i.Key] 79 | if present { 80 | panic(fmt.Errorf("Duplicate key %s", i.Key)) 81 | } 82 | res[i.Key] = i 83 | } 84 | return res 85 | } 86 | 87 | // Wrapper for item.get https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/get 88 | func (api *API) ItemsGet(params Params) (Items, error) { 89 | var res Items 90 | if _, present := params["output"]; !present { 91 | params["output"] = "extend" 92 | } 93 | response, err := api.CallWithError("item.get", params) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | reflector.MapsToStructs2(response.Result.([]interface{}), &res, reflector.Strconv, "json") 99 | return res, nil 100 | } 101 | 102 | // Gets items by application Id. 103 | func (api *API) ItemsGetByApplicationId(id string) (res Items, err error) { 104 | return api.ItemsGet(Params{"applicationids": id}) 105 | } 106 | 107 | // Wrapper for item.create: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/create 108 | func (api *API) ItemsCreate(items Items) error { 109 | response, err := api.CallWithError("item.create", items) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | result := response.Result.(map[string]interface{}) 115 | itemids := result["itemids"].([]interface{}) 116 | for i, id := range itemids { 117 | items[i].ItemId = id.(string) 118 | } 119 | return nil 120 | } 121 | 122 | // Wrapper for item.update: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/update 123 | func (api *API) ItemsUpdate(items Items) error { 124 | _, err := api.CallWithError("item.update", items) 125 | if err != nil { 126 | return err 127 | } 128 | return nil 129 | } 130 | 131 | // Wrapper for item.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/delete 132 | // Cleans ItemId in all items elements if call succeed. 133 | func (api *API) ItemsDelete(items Items) error { 134 | ids := make([]string, len(items)) 135 | for i, item := range items { 136 | ids[i] = item.ItemId 137 | } 138 | 139 | err := api.ItemsDeleteByIds(ids) 140 | if err == nil { 141 | return err 142 | } 143 | for i := range items { 144 | items[i].ItemId = "" 145 | } 146 | return nil 147 | } 148 | 149 | // Wrapper for item.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/item/delete 150 | func (api *API) ItemsDeleteByIds(ids []string) error { 151 | response, err := api.CallWithError("item.delete", ids) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | result := response.Result.(map[string]interface{}) 157 | itemids1, ok := result["itemids"].([]interface{}) 158 | l := len(itemids1) 159 | if !ok { 160 | itemids2 := result["itemids"].(map[string]interface{}) 161 | l = len(itemids2) 162 | } 163 | if len(ids) != l { 164 | err = &ExpectedMore{len(ids), l} 165 | return err 166 | } 167 | return nil 168 | } 169 | -------------------------------------------------------------------------------- /zabbixsender/zabbixsvc/zabbixsvc.go: -------------------------------------------------------------------------------- 1 | package zabbixsvc 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/devopyio/zabbix-alertmanager/zabbixsender/zabbixsnd" 12 | "github.com/pkg/errors" 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/promauto" 15 | log "github.com/sirupsen/logrus" 16 | yaml "gopkg.in/yaml.v2" 17 | ) 18 | 19 | // AlertmanageRequest this is request received from Alertmanager. 20 | type AlertmanagerRequest struct { 21 | Version string `json:"version"` 22 | GroupKey string `json:"groupKey"` 23 | Status string `json:"status"` 24 | Receiver string `json:"receiver"` 25 | GroupLabels map[string]string `json:"groupLabels"` 26 | CommonLabels map[string]string `json:"commonLabels"` 27 | CommonAnnotations map[string]string `json:"commonAnnotations"` 28 | ExternalURL string `json:"externalURL"` 29 | Alerts []Alert `json:"alerts"` 30 | } 31 | 32 | // Alert is alert received from alertmanager. 33 | type Alert struct { 34 | Labels map[string]string `json:"labels"` 35 | Annotations map[string]string `json:"annotations"` 36 | StartsAt string `json:"startsAt,omitempty"` 37 | EndsAt string `json:"EndsAt,omitempty"` 38 | } 39 | 40 | type ZabbixResponse struct { 41 | Response string `json:"response"` 42 | Info string `json:"info"` 43 | } 44 | 45 | // JSONHandler handles alerts 46 | type JSONHandler struct { 47 | Sender *zabbixsnd.Sender 48 | KeyPrefix string 49 | DefaultHost string 50 | Hosts map[string]string 51 | } 52 | 53 | var ( 54 | alertsSentStats = promauto.NewCounterVec( 55 | prometheus.CounterOpts{ 56 | Name: "alerts_sent_total", 57 | Help: "Current number of sent alerts by status", 58 | }, 59 | []string{"alert_status", "host"}, 60 | ) 61 | 62 | alertsErrorsTotal = promauto.NewCounterVec( 63 | prometheus.CounterOpts{ 64 | Name: "alerts_errors_total", 65 | Help: "Current number of different errors", 66 | }, 67 | []string{"alert_status", "host"}, 68 | ) 69 | ) 70 | 71 | func (h *JSONHandler) HandlePost(w http.ResponseWriter, r *http.Request) { 72 | dec := json.NewDecoder(r.Body) 73 | defer r.Body.Close() 74 | 75 | var req AlertmanagerRequest 76 | if err := dec.Decode(&req); err != nil { 77 | alertsErrorsTotal.WithLabelValues("", "").Inc() 78 | 79 | log.Errorf("error decoding message: %v", err) 80 | http.Error(w, "request body is not valid json", http.StatusBadRequest) 81 | return 82 | } 83 | 84 | if req.Status == "" || req.CommonLabels["alertname"] == "" { 85 | alertsErrorsTotal.WithLabelValues(req.Status, req.Receiver).Inc() 86 | http.Error(w, "missing fields in request body", http.StatusBadRequest) 87 | return 88 | } 89 | 90 | value := "0" 91 | if req.Status == "firing" { 92 | value = "1" 93 | } 94 | 95 | host, ok := h.Hosts[req.Receiver] 96 | if !ok { 97 | host = h.DefaultHost 98 | log.Warnf("using default host %s, receiver not found: %s", host, req.Receiver) 99 | } 100 | 101 | alertsSentStats.WithLabelValues(req.Status, host).Inc() 102 | 103 | var metrics []*zabbixsnd.Metric 104 | for _, alert := range req.Alerts { 105 | key := fmt.Sprintf("%s.%s", h.KeyPrefix, strings.ToLower(alert.Labels["alertname"])) 106 | m := &zabbixsnd.Metric{Host: host, Key: key, Value: value} 107 | 108 | m.Clock = time.Now().Unix() 109 | 110 | metrics = append(metrics, m) 111 | 112 | log.Debugf("sending zabbix metrics, host: '%s' key: '%s', value: '%s'", host, key, value) 113 | } 114 | 115 | res, err := h.zabbixSend(metrics) 116 | if err != nil { 117 | alertsErrorsTotal.WithLabelValues(req.Status, host).Add(float64(len(req.Alerts))) 118 | log.Errorf("failed to send to server, metrics: %v, error: %s, raw request: %v", metrics, err, req) 119 | http.Error(w, "failed to send to server", http.StatusInternalServerError) 120 | return 121 | } 122 | 123 | log.Debugf("request succesfully sent: %s", res) 124 | } 125 | 126 | func (h *JSONHandler) zabbixSend(metrics []*zabbixsnd.Metric) (*ZabbixResponse, error) { 127 | var zres ZabbixResponse 128 | 129 | packet := zabbixsnd.NewPacket(metrics) 130 | 131 | res, err := h.Sender.Send(packet) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | if err := json.Unmarshal(res[13:], &zres); err != nil { 137 | return nil, err 138 | } 139 | infoSplit := strings.Split(zres.Info, " ") 140 | 141 | failed := strings.Trim(infoSplit[3], ";") 142 | if failed != "0" || zres.Response != "success" { 143 | return nil, errors.Errorf("failed to fulfill the requests: %v, info: %v, Data: %v", failed, zres.Info, zres.Response) 144 | } 145 | 146 | return &zres, nil 147 | } 148 | 149 | func LoadHostsFromFile(filename string) (map[string]string, error) { 150 | hostsFile, err := ioutil.ReadFile(filename) 151 | if err != nil { 152 | return nil, errors.Wrapf(err, "can't open the alerts file- %v", filename) 153 | } 154 | 155 | var hosts map[string]string 156 | err = yaml.Unmarshal(hostsFile, &hosts) 157 | if err != nil { 158 | return nil, errors.Wrapf(err, "can't read the alerts file- %v", filename) 159 | } 160 | 161 | return hosts, nil 162 | } 163 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/base.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "sync/atomic" 11 | ) 12 | 13 | type ( 14 | Params map[string]interface{} 15 | ) 16 | 17 | type request struct { 18 | Jsonrpc string `json:"jsonrpc"` 19 | Method string `json:"method"` 20 | Params interface{} `json:"params"` 21 | Auth string `json:"auth,omitempty"` 22 | Id int32 `json:"id"` 23 | } 24 | 25 | type Response struct { 26 | Jsonrpc string `json:"jsonrpc"` 27 | Error *Error `json:"error"` 28 | Result interface{} `json:"result"` 29 | Id int32 `json:"id"` 30 | } 31 | 32 | type Error struct { 33 | Code int `json:"code"` 34 | Message string `json:"message"` 35 | Data string `json:"data"` 36 | } 37 | 38 | func (e *Error) Error() string { 39 | return fmt.Sprintf("%d (%s): %s", e.Code, e.Message, e.Data) 40 | } 41 | 42 | type ExpectedOneResult int 43 | 44 | func (e *ExpectedOneResult) Error() string { 45 | return fmt.Sprintf("Expected exactly one result, got %d.", *e) 46 | } 47 | 48 | type ExpectedMore struct { 49 | Expected int 50 | Got int 51 | } 52 | 53 | func (e *ExpectedMore) Error() string { 54 | return fmt.Sprintf("Expected %d, got %d.", e.Expected, e.Got) 55 | } 56 | 57 | type API struct { 58 | Auth string // auth token, filled by Login() 59 | Logger *log.Logger // request/response logger, nil by default 60 | url string 61 | c http.Client 62 | id int32 63 | } 64 | 65 | // Creates new API access object. 66 | // Typical URL is http://host/api_jsonrpc.php or http://host/zabbix/api_jsonrpc.php. 67 | // It also may contain HTTP basic auth username and password like 68 | // http://username:password@host/api_jsonrpc.php. 69 | func NewAPI(url string) (api *API) { 70 | return &API{url: url, c: http.Client{}} 71 | } 72 | 73 | // Allows one to use specific http.Client, for example with InsecureSkipVerify transport. 74 | func (api *API) SetClient(c *http.Client) { 75 | api.c = *c 76 | } 77 | 78 | func (api *API) printf(format string, v ...interface{}) { 79 | if api.Logger != nil { 80 | api.Logger.Printf(format, v...) 81 | } 82 | } 83 | 84 | func (api *API) callBytes(method string, params interface{}) ([]byte, error) { 85 | id := atomic.AddInt32(&api.id, 1) 86 | jsonobj := request{"2.0", method, params, api.Auth, id} 87 | b, err := json.Marshal(jsonobj) 88 | if err != nil { 89 | return nil, err 90 | } 91 | api.printf("Request (POST): %s", b) 92 | 93 | req, err := http.NewRequest("POST", api.url, bytes.NewReader(b)) 94 | if err != nil { 95 | return nil, err 96 | } 97 | req.ContentLength = int64(len(b)) 98 | req.Header.Add("Content-Type", "application/json-rpc") 99 | 100 | res, err := api.c.Do(req) 101 | if err != nil { 102 | api.printf("Error : %s", err) 103 | return nil, err 104 | } 105 | defer res.Body.Close() 106 | 107 | b, err = ioutil.ReadAll(res.Body) 108 | if err != nil { 109 | return nil, err 110 | } 111 | api.printf("Response (%d): %s", res.StatusCode, b) 112 | return b, nil 113 | } 114 | 115 | // Calls specified API method. Uses api.Auth if not empty. 116 | // err is something network or marshaling related. Caller should inspect response.Error to get API error. 117 | func (api *API) Call(method string, params interface{}) (Response, error) { 118 | var response Response 119 | b, err := api.callBytes(method, params) 120 | if err != nil { 121 | return response, err 122 | } 123 | err = json.Unmarshal(b, &response) 124 | if err != nil { 125 | return response, err 126 | } 127 | return response, nil 128 | } 129 | 130 | // Uses Call() and then sets err to response.Error if former is nil and latter is not. 131 | func (api *API) CallWithError(method string, params interface{}) (Response, error) { 132 | response, err := api.Call(method, params) 133 | if err == nil && response.Error != nil { 134 | err = response.Error 135 | } 136 | return response, err 137 | } 138 | 139 | // Calls "user.login" API method and fills api.Auth field. 140 | // This method modifies API structure and should not be called concurrently with other methods. 141 | func (api *API) Login(user, password string) (string, error) { 142 | params := map[string]string{"user": user, "password": password} 143 | response, err := api.CallWithError("user.login", params) 144 | if err != nil { 145 | return "", err 146 | } 147 | 148 | auth := response.Result.(string) 149 | api.Auth = auth 150 | return auth, nil 151 | } 152 | 153 | // Calls "APIInfo.version" API method. 154 | // This method temporary modifies API structure and should not be called concurrently with other methods. 155 | func (api *API) Version() (string, error) { 156 | // temporary remove auth for this method to succeed 157 | // https://www.zabbix.com/documentation/2.2/manual/appendix/api/apiinfo/version 158 | auth := api.Auth 159 | api.Auth = "" 160 | response, err := api.CallWithError("APIInfo.version", Params{}) 161 | if err != nil { 162 | return "", err 163 | } 164 | api.Auth = auth 165 | 166 | // despite what documentation says, Zabbix 2.2 requires auth, so we try again 167 | if e, ok := err.(*Error); ok && e.Code == -32602 { 168 | response, err = api.CallWithError("APIInfo.version", Params{}) 169 | } 170 | if err != nil { 171 | return "", err 172 | } 173 | 174 | v := response.Result.(string) 175 | return v, nil 176 | } 177 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixclient/host.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "reflect" 5 | 6 | reflector "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/zabbixutil" 7 | ) 8 | 9 | type ( 10 | AvailableType int 11 | StatusType int 12 | InventoryType int 13 | ) 14 | 15 | const ( 16 | Available AvailableType = 1 17 | Unavailable AvailableType = 2 18 | 19 | Monitored StatusType = 0 20 | Unmonitored StatusType = 1 21 | 22 | InventoryDisabled InventoryType = -1 23 | InventoryManual InventoryType = 0 24 | InventoryAutomatic InventoryType = 1 25 | ) 26 | 27 | // https://www.zabbix.com/documentation/2.2/manual/appendix/api/host/definitions 28 | type Host struct { 29 | HostId string `json:"hostid,omitempty"` 30 | Host string `json:"host"` 31 | Available AvailableType `json:"available"` 32 | Error string `json:"error"` 33 | Name string `json:"name"` 34 | Status StatusType `json:"status"` 35 | 36 | InventoryMode InventoryType `json:"inventory_mode"` 37 | Inventory map[string]string `json:"inventory"` 38 | 39 | // Fields below used only when creating hosts 40 | GroupIds HostGroupIds `json:"groups,omitempty"` 41 | Interfaces HostInterfaces `json:"interfaces,omitempty"` 42 | } 43 | 44 | type Hosts []Host 45 | 46 | // Wrapper for host.get: https://www.zabbix.com/documentation/2.2/manual/appendix/api/host/get 47 | func (api *API) HostsGet(params Params) (Hosts, error) { 48 | var res Hosts 49 | if _, present := params["output"]; !present { 50 | params["output"] = "extend" 51 | } 52 | response, err := api.CallWithError("host.get", params) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | reflector.MapsToStructs2(response.Result.([]interface{}), &res, reflector.Strconv, "json") 58 | 59 | if _, ok := params["selectInventory"]; ok { 60 | results := response.Result.([]interface{}) 61 | for i, _ := range results { 62 | host := results[i].(map[string]interface{}) 63 | if reflect.TypeOf(host["inventory"]).Kind() == reflect.Map { 64 | inventory := host["inventory"].(map[string]interface{}) 65 | res[i].Inventory = make(map[string]string, len(inventory)) 66 | for key, value := range inventory { 67 | res[i].Inventory[key] = value.(string) 68 | } 69 | } 70 | } 71 | } 72 | return res, nil 73 | } 74 | 75 | // Gets hosts by host group Ids. 76 | func (api *API) HostsGetByHostGroupIds(ids []string) (res Hosts, err error) { 77 | return api.HostsGet(Params{"groupids": ids}) 78 | } 79 | 80 | // Gets hosts by host groups. 81 | func (api *API) HostsGetByHostGroups(hostGroups HostGroups) (res Hosts, err error) { 82 | ids := make([]string, len(hostGroups)) 83 | for i, id := range hostGroups { 84 | ids[i] = id.GroupId 85 | } 86 | return api.HostsGetByHostGroupIds(ids) 87 | } 88 | 89 | // Gets host by Id only if there is exactly 1 matching host. 90 | func (api *API) HostGetById(id string) (*Host, error) { 91 | var res *Host 92 | hosts, err := api.HostsGet(Params{"hostids": id}) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | if len(hosts) == 1 { 98 | res = &hosts[0] 99 | } else { 100 | e := ExpectedOneResult(len(hosts)) 101 | err = &e 102 | return nil, err 103 | } 104 | return res, nil 105 | } 106 | 107 | // Gets host by Host only if there is exactly 1 matching host. 108 | func (api *API) HostGetByHost(host string) (*Host, error) { 109 | var res *Host 110 | hosts, err := api.HostsGet(Params{"filter": map[string]string{"host": host}}) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | if len(hosts) == 1 { 116 | res = &hosts[0] 117 | } else { 118 | e := ExpectedOneResult(len(hosts)) 119 | err = &e 120 | return nil, err 121 | } 122 | return res, nil 123 | } 124 | 125 | // Wrapper for host.create: https://www.zabbix.com/documentation/2.2/manual/appendix/api/host/create 126 | func (api *API) HostsCreate(hosts Hosts) error { 127 | response, err := api.CallWithError("host.create", hosts) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | result := response.Result.(map[string]interface{}) 133 | hostids := result["hostids"].([]interface{}) 134 | for i, id := range hostids { 135 | hosts[i].HostId = id.(string) 136 | } 137 | return nil 138 | } 139 | 140 | // Wrapper for host.update: https://www.zabbix.com/documentation/2.2/manual/appendix/api/host/update 141 | func (api *API) HostsUpdate(hosts Hosts) error { 142 | _, err := api.CallWithError("host.update", hosts) 143 | if err != nil { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | // Wrapper for host.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/host/delete 150 | // Cleans HostId in all hosts elements if call succeed. 151 | func (api *API) HostsDelete(hosts Hosts) error { 152 | ids := make([]string, len(hosts)) 153 | for i, host := range hosts { 154 | ids[i] = host.HostId 155 | } 156 | 157 | err := api.HostsDeleteByIds(ids) 158 | if err != nil { 159 | return err 160 | } 161 | for i := range hosts { 162 | hosts[i].HostId = "" 163 | } 164 | 165 | return nil 166 | } 167 | 168 | // Wrapper for host.delete: https://www.zabbix.com/documentation/2.2/manual/appendix/api/host/delete 169 | func (api *API) HostsDeleteByIds(ids []string) error { 170 | hostIds := make([]map[string]string, len(ids)) 171 | for i, id := range ids { 172 | hostIds[i] = map[string]string{"hostid": id} 173 | } 174 | 175 | response, err := api.CallWithError("host.delete", hostIds) 176 | if err != nil { 177 | // Zabbix 2.4 uses new syntax only 178 | if e, ok := err.(*Error); ok && e.Code == -32500 { 179 | response, err = api.CallWithError("host.delete", ids) 180 | if err != nil { 181 | return err 182 | } 183 | } 184 | } 185 | if err != nil { 186 | return err 187 | } 188 | 189 | result := response.Result.(map[string]interface{}) 190 | hostids := result["hostids"].([]interface{}) 191 | if len(ids) != len(hostids) { 192 | err = &ExpectedMore{len(ids), len(hostids)} 193 | return err 194 | } 195 | return nil 196 | } 197 | -------------------------------------------------------------------------------- /zabbixsender/zabbixsvc/zabbixsvc_test.go: -------------------------------------------------------------------------------- 1 | package zabbixsvc_test 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/devopyio/zabbix-alertmanager/zabbixsender/zabbixsnd" 11 | "github.com/devopyio/zabbix-alertmanager/zabbixsender/zabbixsvc" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | const ( 16 | alertOK = `{ 17 | "version":"4", 18 | "groupKey":"{}:{alertname=\"InstanceDown\"}", 19 | "status":"resolved", 20 | "receiver":"testing", 21 | "groupLabels":{ 22 | "alertname":"InstanceDown" 23 | }, 24 | "commonLabels":{ 25 | "alertname":"InstanceDown", 26 | "instance":"localhost:9100", 27 | "job":"node_exporter", 28 | "severity":"critical" 29 | }, 30 | "commonAnnotations":{ 31 | "description":"localhost:9100 of job node_exporter has been down for more than 1 minute.", 32 | "summary":"Instance localhost:9100 down" 33 | }, 34 | "externalURL":"http://edas-GE72-6QC:9093", 35 | "alerts":[ 36 | { 37 | "labels":{ 38 | "alertname":"InstanceDown", 39 | "instance":"localhost:9100", 40 | "job":"node_exporter", 41 | "severity":"critical" 42 | }, 43 | "annotations":{ 44 | "description":"localhost:9100 of job node_exporter has been down for more than 1 minute.", 45 | "summary":"Instance localhost:9100 down" 46 | }, 47 | "startsAt":"2018-08-30T16:59:09.653872838+03:00", 48 | "EndsAt":"2018-08-30T17:01:09.656110177+03:00" 49 | } 50 | ] 51 | }` 52 | 53 | alertBadReqErr = `{ 54 | "status": BadRequest 55 | }` 56 | 57 | alertMissingFields = `{ 58 | "version":"4", 59 | "groupKey":"{}:{alertname=\"InstanceDown\"}", 60 | "status":"", 61 | "receiver":"testing", 62 | "groupLabels":{ 63 | "alertname":"InstanceDown" 64 | }, 65 | "commonLabels":{ 66 | "alertname":"", 67 | "instance":"localhost:9100", 68 | "job":"node_exporter", 69 | "severity":"critical" 70 | }, 71 | "commonAnnotations":{ 72 | "description":"localhost:9100 of job node_exporter has been down for more than 1 minute.", 73 | "summary":"Instance localhost:9100 down" 74 | }, 75 | "externalURL":"http://edas-GE72-6QC:9093", 76 | "alerts":[ 77 | { 78 | "labels":{ 79 | "alertname":"InstanceDown", 80 | "instance":"localhost:9100", 81 | "job":"node_exporter", 82 | "severity":"critical" 83 | }, 84 | "annotations":{ 85 | "description":"localhost:9100 of job node_exporter has been down for more than 1 minute.", 86 | "summary":"Instance localhost:9100 down" 87 | }, 88 | "startsAt":"2018-08-30T16:59:09.653872838+03:00", 89 | "EndsAt":"2018-08-30T17:01:09.656110177+03:00" 90 | } 91 | ] 92 | }` 93 | 94 | alertInternal = `{ 95 | "version":"4", 96 | "groupKey":"{}:{alertname=\"InstanceDown\"}", 97 | "status":"firing", 98 | "receiver":"testing", 99 | "groupLabels":{ 100 | "alertname":"InstanceDown" 101 | }, 102 | "commonLabels":{ 103 | "alertname":"InstanceDown", 104 | "instance":"localhost:9100", 105 | "job":"node_exporter", 106 | "severity":"critical" 107 | }, 108 | "commonAnnotations":{ 109 | "description":"localhost:9100 of job node_exporter has been down for more than 1 minute.", 110 | "summary":"Instance localhost:9100 down" 111 | }, 112 | "externalURL":"http://edas-GE72-6QC:9093", 113 | "alerts":[ 114 | { 115 | "labels":{ 116 | "alertname":"InstanceDown", 117 | "instance":"localhost:9100", 118 | "job":"node_exporter", 119 | "severity":"critical" 120 | }, 121 | "annotations":{ 122 | "description":"localhost:9100 of job node_exporter has been down for more than 1 minute.", 123 | "summary":"Instance localhost:9100 down" 124 | }, 125 | "startsAt":"2018-08-30T16:59:09.653872838+03:00", 126 | "EndsAt":"2018-08-30T17:01:09.656110177+03:00" 127 | } 128 | ] 129 | }` 130 | ) 131 | 132 | func TestJSONHandlerOK(t *testing.T) { 133 | const expectedMsg = "ZBXD\x01}\x00\x00\x00\x00\x00\x00\x00{\"request\":\"sender data\",\"data\":[{\"host\":\"Testing\",\"key\":\".instancedown\",\"value\":\"0\",\"clock\"" 134 | l, err := net.Listen("tcp", "127.0.0.1:3000") 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | defer l.Close() 139 | 140 | go func() { 141 | for { 142 | conn, err := l.Accept() 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | defer conn.Close() 147 | buf := make([]byte, 141) 148 | _, err = conn.Read(buf) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | log.Info(string(buf)) 153 | 154 | if msg := string(buf[:105]); msg != expectedMsg { 155 | t.Fatalf("Unexpected message:\nGot:\t\t%s\nExpected:\t%s\n", msg, expectedMsg) 156 | } 157 | _, err = conn.Write([]byte("ZBXD\x01Z\x00\x00\x00\x00\x00\x00\x00{\"response\":\"success\",\"info\":\"processed: 1; failed: 0; total: 1; seconds spent: 0.000041\"}")) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | 162 | return 163 | } 164 | }() 165 | 166 | s, err := zabbixsnd.New("127.0.0.1:3000") 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | 171 | h := &zabbixsvc.JSONHandler{ 172 | Sender: s, 173 | DefaultHost: "Testing", 174 | } 175 | 176 | rr := httptest.NewRecorder() 177 | req, err := http.NewRequest("POST", "/", strings.NewReader(alertOK)) 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | 182 | h.HandlePost(rr, req) 183 | 184 | if rr.Code != http.StatusOK { 185 | t.Fatal("Expected working, got error:", rr.Code) 186 | } 187 | } 188 | 189 | func TestJSONHandlerStatusBadRequest(t *testing.T) { 190 | s, err := zabbixsnd.New("127.0.0.1:3000") 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | h := &zabbixsvc.JSONHandler{ 196 | Sender: s, 197 | DefaultHost: "host", 198 | } 199 | 200 | rr := httptest.NewRecorder() 201 | req, err := http.NewRequest("POST", "/", strings.NewReader(alertBadReqErr)) 202 | if err != nil { 203 | t.Fatal(err) 204 | } 205 | 206 | h.HandlePost(rr, req) 207 | 208 | if rr.Code != http.StatusBadRequest { 209 | t.Fatal("Expected error, got:", rr.Code) 210 | } 211 | 212 | } 213 | func TestJSONHandlerMissingFields(t *testing.T) { 214 | s, err := zabbixsnd.New("127.0.0.1:3000") 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | 219 | h := &zabbixsvc.JSONHandler{ 220 | Sender: s, 221 | DefaultHost: "host", 222 | } 223 | 224 | rr := httptest.NewRecorder() 225 | req, err := http.NewRequest("POST", "/", strings.NewReader(alertMissingFields)) 226 | if err != nil { 227 | t.Fatal(err) 228 | } 229 | 230 | h.HandlePost(rr, req) 231 | 232 | if rr.Code != http.StatusBadRequest { 233 | t.Fatal("Expected error, got:", rr.Code) 234 | } 235 | } 236 | 237 | func TestJSONHandlerInternal(t *testing.T) { 238 | l, err := net.Listen("tcp", "127.0.0.1:3000") 239 | if err != nil { 240 | t.Fatal(err) 241 | } 242 | defer l.Close() 243 | 244 | go func() { 245 | for { 246 | conn, err := l.Accept() 247 | if err != nil { 248 | t.Fatal(err) 249 | } 250 | defer conn.Close() 251 | buf := make([]byte, 112) 252 | _, err = conn.Read(buf) 253 | if err != nil { 254 | t.Fatal(err) 255 | } 256 | return 257 | } 258 | }() 259 | 260 | s, err := zabbixsnd.New("127.0.0.1:3000") 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | 265 | h := &zabbixsvc.JSONHandler{ 266 | Sender: s, 267 | DefaultHost: "host", 268 | } 269 | 270 | rr := httptest.NewRecorder() 271 | req, err := http.NewRequest("POST", "/", strings.NewReader(alertInternal)) 272 | if err != nil { 273 | t.Fatal(err) 274 | } 275 | 276 | h.HandlePost(rr, req) 277 | 278 | if rr.Code != http.StatusInternalServerError { 279 | t.Fatal("Expected error, got:", rr.Code) 280 | } 281 | 282 | } 283 | -------------------------------------------------------------------------------- /zabbixprovisioner/zabbixutil/reflector.go: -------------------------------------------------------------------------------- 1 | package zabbixutil 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Converts value to kind. Panics if it can't be done. 11 | type Converter func(value interface{}, kind reflect.Kind) interface{} 12 | 13 | // Converter: requires value to be exactly of specified kind. 14 | func NoConvert(value interface{}, kind reflect.Kind) interface{} { 15 | switch kind { 16 | case reflect.Bool: 17 | return value.(bool) 18 | 19 | case reflect.Int: 20 | return int64(value.(int)) 21 | case reflect.Int8: 22 | return int64(value.(int8)) 23 | case reflect.Int16: 24 | return int64(value.(int16)) 25 | case reflect.Int32: 26 | return int64(value.(int32)) 27 | case reflect.Int64: 28 | return value.(int64) 29 | 30 | case reflect.Uint: 31 | return uint64(value.(uint)) 32 | case reflect.Uint8: 33 | return uint64(value.(uint8)) 34 | case reflect.Uint16: 35 | return uint64(value.(uint16)) 36 | case reflect.Uint32: 37 | return uint64(value.(uint32)) 38 | case reflect.Uint64: 39 | return value.(uint64) 40 | case reflect.Uintptr: 41 | return uint64(value.(uintptr)) 42 | 43 | case reflect.Float32: 44 | return float64(value.(float32)) 45 | case reflect.Float64: 46 | return value.(float64) 47 | 48 | case reflect.String: 49 | return value.(string) 50 | } 51 | 52 | panic(fmt.Errorf("NoConvert: can't convert %#v to %s", value, kind)) 53 | } 54 | 55 | // Converter: uses strconv.Parse* functions. 56 | func Strconv(value interface{}, kind reflect.Kind) interface{} { 57 | err := fmt.Errorf("Strconv: can't convert %#v to %s", value, kind) 58 | s := fmt.Sprint(value) 59 | 60 | switch kind { 61 | case reflect.Bool: 62 | res, err := strconv.ParseBool(s) 63 | if err != nil { 64 | panic(err) 65 | } 66 | return res 67 | 68 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 69 | res, err := strconv.ParseInt(s, 10, 64) 70 | if err != nil { 71 | panic(err) 72 | } 73 | return res 74 | 75 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 76 | res, err := strconv.ParseUint(s, 10, 64) 77 | if err != nil { 78 | panic(err) 79 | } 80 | return res 81 | 82 | case reflect.Float32, reflect.Float64: 83 | res, err := strconv.ParseFloat(s, 64) 84 | if err != nil { 85 | panic(err) 86 | } 87 | return res 88 | 89 | case reflect.String: 90 | return s 91 | } 92 | 93 | panic(err) 94 | } 95 | 96 | // Converts a struct to map. 97 | // First argument is a pointer to struct. 98 | // Second argument is a not-nil map which will be modified. 99 | // Only exported struct fields are used. Pointers will be followed, nils will be present. 100 | // Tag may be used to change mapping between struct field and map key. 101 | // Currently supports bool, ints, uints, floats, strings and pointer to them. 102 | // Panics in case of error. 103 | func StructToMap(StructPointer interface{}, Map map[string]interface{}, tag string) { 104 | structPointerType := reflect.TypeOf(StructPointer) 105 | if structPointerType.Kind() != reflect.Ptr { 106 | panic(fmt.Errorf("StructToMap: expected pointer to struct as first argument, got %s", structPointerType.Kind())) 107 | } 108 | 109 | structType := structPointerType.Elem() 110 | if structType.Kind() != reflect.Struct { 111 | panic(fmt.Errorf("StructToMap: expected pointer to struct as first argument, got pointer to %s", structType.Kind())) 112 | } 113 | 114 | s := reflect.ValueOf(StructPointer).Elem() 115 | 116 | var name string 117 | for i := 0; i < structType.NumField(); i++ { 118 | stf := structType.Field(i) 119 | if stf.PkgPath != "" { 120 | continue 121 | } 122 | 123 | name = "" 124 | if tag != "" { 125 | name = strings.Split(stf.Tag.Get(tag), ",")[0] 126 | if name == "-" { 127 | continue 128 | } 129 | } 130 | if name == "" { 131 | name = stf.Name 132 | } 133 | 134 | f := s.Field(i) 135 | if f.Kind() == reflect.Ptr { 136 | if f.IsNil() { 137 | Map[name] = nil 138 | continue 139 | } 140 | f = f.Elem() 141 | } 142 | Map[name] = f.Interface() 143 | } 144 | } 145 | 146 | // Converts a struct to map. Uses StructToMap(). 147 | // First argument is a struct. 148 | // Second argument is a not-nil map which will be modified. 149 | // Only exported struct fields are used. Pointers will be followed, nils will be present. 150 | // Tag may be used to change mapping between struct field and map key. 151 | // Currently supports bool, ints, uints, floats, strings and pointer to them. 152 | // Panics in case of error. 153 | func StructValueToMap(Struct interface{}, Map map[string]interface{}, tag string) { 154 | structType := reflect.TypeOf(Struct) 155 | if structType.Kind() != reflect.Struct { 156 | panic(fmt.Errorf("StructValueToMap: expected struct as first argument, got %s", structType.Kind())) 157 | } 158 | 159 | v := reflect.New(reflect.TypeOf(Struct)) 160 | v.Elem().Set(reflect.ValueOf(Struct)) 161 | StructToMap(v.Interface(), Map, tag) 162 | } 163 | 164 | // Converts a slice of structs to a slice of maps. Uses StructValueToMap(). 165 | // First argument is a slice of structs. 166 | // Second argument is a pointer to (possibly nil) slice of maps which will be set. 167 | func StructsToMaps(Structs interface{}, Maps *[]map[string]interface{}, tag string) { 168 | sliceType := reflect.TypeOf(Structs) 169 | if sliceType.Kind() != reflect.Slice { 170 | panic(fmt.Errorf("Expected slice of structs as first argument, got %s", sliceType.Kind())) 171 | } 172 | 173 | structType := sliceType.Elem() 174 | if structType.Kind() != reflect.Struct { 175 | panic(fmt.Errorf("Expected slice of structs as first argument, got slice of %s", structType.Kind())) 176 | } 177 | 178 | structs := reflect.ValueOf(Structs) 179 | l := structs.Len() 180 | maps := reflect.MakeSlice(reflect.TypeOf([]map[string]interface{}{}), 0, l) 181 | 182 | for i := 0; i < l; i++ { 183 | m := make(map[string]interface{}) 184 | StructValueToMap(structs.Index(i).Interface(), m, tag) 185 | maps = reflect.Append(maps, reflect.ValueOf(m)) 186 | } 187 | 188 | reflect.ValueOf(Maps).Elem().Set(maps) 189 | } 190 | 191 | // Converts a map to struct using converter function. 192 | // First argument is a map. 193 | // Second argument is a not-nil pointer to struct which will be modified. 194 | // Only exported struct fields are set. Omitted or extra values in map are ignored. Pointers will be set. 195 | // Tag may be used to change mapping between struct field and map key. 196 | // Currently supports bool, ints, uints, floats, strings and pointer to them. 197 | // Panics in case of error. 198 | func MapToStruct(Map map[string]interface{}, StructPointer interface{}, converter Converter, tag string) { 199 | structPointerType := reflect.TypeOf(StructPointer) 200 | if structPointerType.Kind() != reflect.Ptr { 201 | panic(fmt.Errorf("MapToStruct: expected pointer to struct as second argument, got %s", structPointerType.Kind())) 202 | } 203 | 204 | structType := structPointerType.Elem() 205 | if structType.Kind() != reflect.Struct { 206 | panic(fmt.Errorf("MapToStruct: expected pointer to struct as second argument, got pointer to %s", structType.Kind())) 207 | } 208 | s := reflect.ValueOf(StructPointer).Elem() 209 | 210 | var name string 211 | defer func() { 212 | e := recover() 213 | if e == nil { 214 | return 215 | } 216 | 217 | panic(fmt.Errorf("MapToStruct, field %s: %s", name, e)) 218 | }() 219 | 220 | for i := 0; i < structType.NumField(); i++ { 221 | f := s.Field(i) 222 | if !f.CanSet() { 223 | continue 224 | } 225 | 226 | stf := structType.Field(i) 227 | name = "" 228 | if tag != "" { 229 | name = strings.Split(stf.Tag.Get(tag), ",")[0] 230 | if name == "-" { 231 | continue 232 | } 233 | } 234 | if name == "" { 235 | name = stf.Name 236 | } 237 | v, ok := Map[name] 238 | if !ok { 239 | continue 240 | } 241 | 242 | var fp reflect.Value 243 | kind := f.Kind() 244 | if kind == reflect.Ptr { 245 | t := f.Type().Elem() 246 | kind = t.Kind() 247 | fp = reflect.New(t) 248 | f = fp.Elem() 249 | } 250 | 251 | switch kind { 252 | case reflect.Bool: 253 | f.SetBool(converter(v, kind).(bool)) 254 | 255 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 256 | f.SetInt(converter(v, kind).(int64)) 257 | 258 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 259 | f.SetUint(converter(v, kind).(uint64)) 260 | 261 | case reflect.Float32, reflect.Float64: 262 | f.SetFloat(converter(v, kind).(float64)) 263 | 264 | case reflect.String: 265 | f.SetString(converter(v, kind).(string)) 266 | 267 | default: 268 | // not implemented 269 | } 270 | 271 | if fp.IsValid() { 272 | s.Field(i).Set(fp) 273 | } 274 | } 275 | 276 | return 277 | } 278 | 279 | // Converts a slice of maps to a slice of structs. Uses MapToStruct(). 280 | // First argument is a slice of maps. 281 | // Second argument is a pointer to (possibly nil) slice of structs which will be set. 282 | func MapsToStructs(Maps []map[string]interface{}, SlicePointer interface{}, converter Converter, tag string) { 283 | slicePointerType := reflect.TypeOf(SlicePointer) 284 | if slicePointerType.Kind() != reflect.Ptr { 285 | panic(fmt.Errorf("MapsToStructs: expected pointer to slice of structs as second argument, got %s", slicePointerType.Kind())) 286 | } 287 | 288 | sliceType := slicePointerType.Elem() 289 | if sliceType.Kind() != reflect.Slice { 290 | panic(fmt.Errorf("MapsToStructs: expected pointer to slice of structs as second argument, got pointer to %s", sliceType.Kind())) 291 | } 292 | 293 | structType := sliceType.Elem() 294 | if structType.Kind() != reflect.Struct { 295 | panic(fmt.Errorf("MapsToStructs: expected pointer to slice of structs as second argument, got pointer to slice of %s", structType.Kind())) 296 | } 297 | 298 | slice := reflect.MakeSlice(sliceType, 0, len(Maps)) 299 | for _, m := range Maps { 300 | s := reflect.New(structType) 301 | MapToStruct(m, s.Interface(), converter, tag) 302 | slice = reflect.Append(slice, s.Elem()) 303 | } 304 | reflect.ValueOf(SlicePointer).Elem().Set(slice) 305 | } 306 | 307 | // Variant of MapsToStructs() with relaxed signature. 308 | func MapsToStructs2(Maps []interface{}, SlicePointer interface{}, converter Converter, tag string) { 309 | m := make([]map[string]interface{}, len(Maps)) 310 | for index, i := range Maps { 311 | m[index] = i.(map[string]interface{}) 312 | } 313 | MapsToStructs(m, SlicePointer, converter, tag) 314 | } 315 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/zabbix.go: -------------------------------------------------------------------------------- 1 | package provisioner 2 | 3 | import ( 4 | "strings" 5 | 6 | zabbix "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/zabbixclient" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type State int 11 | 12 | const ( 13 | StateNew State = iota 14 | StateUpdated 15 | StateEqual 16 | StateOld 17 | ) 18 | 19 | var StateName = map[State]string{ 20 | StateNew: "New", 21 | StateUpdated: "Updated", 22 | StateEqual: "Equal", 23 | StateOld: "Old", 24 | } 25 | 26 | type CustomApplication struct { 27 | State State 28 | zabbix.Application 29 | } 30 | 31 | type CustomTrigger struct { 32 | State State 33 | zabbix.Trigger 34 | } 35 | 36 | type CustomHostGroup struct { 37 | State State 38 | zabbix.HostGroup 39 | } 40 | 41 | type CustomItem struct { 42 | State State 43 | zabbix.Item 44 | Applications map[string]struct{} 45 | } 46 | 47 | type CustomHost struct { 48 | State State 49 | zabbix.Host 50 | HostGroups map[string]struct{} 51 | Applications map[string]*CustomApplication 52 | Items map[string]*CustomItem 53 | Triggers map[string]*CustomTrigger 54 | } 55 | 56 | type CustomZabbix struct { 57 | Hosts map[string]*CustomHost 58 | HostGroups map[string]*CustomHostGroup 59 | } 60 | 61 | func (z *CustomZabbix) AddHost(host *CustomHost) (updatedHost *CustomHost) { 62 | updatedHost = host 63 | 64 | if existing, ok := z.Hosts[host.Name]; ok { 65 | if existing.Equal(host) { 66 | if host.State == StateOld { 67 | existing.HostId = host.HostId 68 | existing.State = StateEqual 69 | updatedHost = existing 70 | } 71 | } else { 72 | if host.State == StateOld { 73 | existing.HostId = host.HostId 74 | } 75 | existing.State = StateUpdated 76 | updatedHost = existing 77 | } 78 | } 79 | 80 | z.Hosts[host.Name] = updatedHost 81 | return updatedHost 82 | } 83 | 84 | func (host *CustomHost) AddItem(item *CustomItem) { 85 | 86 | updatedItem := item 87 | 88 | if existing, ok := host.Items[item.Key]; ok { 89 | if existing.Equal(item) { 90 | if item.State == StateOld { 91 | existing.ItemId = item.ItemId 92 | existing.State = StateEqual 93 | updatedItem = existing 94 | } 95 | } else { 96 | if item.State == StateOld { 97 | existing.ItemId = item.ItemId 98 | } 99 | existing.State = StateUpdated 100 | updatedItem = existing 101 | } 102 | } 103 | 104 | host.Items[item.Key] = updatedItem 105 | } 106 | 107 | func (host *CustomHost) AddTrigger(trigger *CustomTrigger) { 108 | 109 | updatedTrigger := trigger 110 | 111 | if existing, ok := host.Triggers[trigger.Expression]; ok { 112 | if existing.Equal(trigger) { 113 | if trigger.State == StateOld { 114 | existing.TriggerId = trigger.TriggerId 115 | existing.State = StateEqual 116 | updatedTrigger = existing 117 | } 118 | } else { 119 | if trigger.State == StateOld { 120 | existing.TriggerId = trigger.TriggerId 121 | } 122 | existing.State = StateUpdated 123 | updatedTrigger = existing 124 | } 125 | } 126 | 127 | host.Triggers[trigger.Expression] = updatedTrigger 128 | } 129 | 130 | func (host *CustomHost) AddApplication(application *CustomApplication) { 131 | if _, ok := host.Applications[application.Name]; ok { 132 | if application.State == StateOld { 133 | application.State = StateEqual 134 | } 135 | } 136 | host.Applications[application.Name] = application 137 | } 138 | 139 | func (z *CustomZabbix) AddHostGroup(hostGroup *CustomHostGroup) { 140 | if _, ok := z.HostGroups[hostGroup.Name]; ok { 141 | if hostGroup.State == StateOld { 142 | hostGroup.State = StateEqual 143 | } 144 | } 145 | z.HostGroups[hostGroup.Name] = hostGroup 146 | } 147 | 148 | func (i *CustomHost) Equal(j *CustomHost) bool { 149 | if i.Name != j.Name { 150 | return false 151 | } 152 | 153 | if len(i.HostGroups) != len(j.HostGroups) { 154 | return false 155 | } 156 | 157 | for hostGroupName, _ := range i.HostGroups { 158 | if _, ok := j.HostGroups[hostGroupName]; !ok { 159 | return false 160 | } 161 | } 162 | 163 | if len(i.Inventory) != len(j.Inventory) { 164 | return false 165 | } 166 | 167 | for key, valueI := range i.Inventory { 168 | if valueJ, ok := j.Inventory[key]; !ok { 169 | return false 170 | } else if valueJ != valueI { 171 | return false 172 | } 173 | } 174 | 175 | return true 176 | } 177 | 178 | func (i *CustomItem) Equal(j *CustomItem) bool { 179 | if i.Name != j.Name { 180 | return false 181 | } 182 | 183 | if i.Description != j.Description { 184 | return false 185 | } 186 | 187 | if i.Trends != j.Trends { 188 | return false 189 | } 190 | 191 | if i.History != j.History { 192 | return false 193 | } 194 | 195 | if i.TrapperHosts != j.TrapperHosts { 196 | return false 197 | } 198 | 199 | if len(i.Applications) != len(j.Applications) { 200 | return false 201 | } 202 | 203 | for appName, _ := range i.Applications { 204 | if _, ok := j.Applications[appName]; !ok { 205 | return false 206 | } 207 | } 208 | 209 | return true 210 | } 211 | 212 | func (i *CustomTrigger) Equal(j *CustomTrigger) bool { 213 | if i.Expression != j.Expression { 214 | return false 215 | } 216 | 217 | if i.Description != j.Description { 218 | return false 219 | } 220 | 221 | if i.Priority != j.Priority { 222 | return false 223 | } 224 | 225 | if i.Comments != j.Comments { 226 | return false 227 | } 228 | 229 | if i.URL != j.URL { 230 | return false 231 | } 232 | 233 | if i.ManualClose != j.ManualClose { 234 | return false 235 | } 236 | 237 | return true 238 | } 239 | 240 | func (z *CustomZabbix) GetHostsByState() (hostByState map[State]zabbix.Hosts) { 241 | 242 | hostByState = map[State]zabbix.Hosts{ 243 | StateNew: zabbix.Hosts{}, 244 | StateOld: zabbix.Hosts{}, 245 | StateUpdated: zabbix.Hosts{}, 246 | StateEqual: zabbix.Hosts{}, 247 | } 248 | 249 | newHostAmmount := 0 250 | for _, host := range z.Hosts { 251 | for hostGroupName, _ := range host.HostGroups { 252 | host.GroupIds = append(host.GroupIds, zabbix.HostGroupId{GroupId: z.HostGroups[hostGroupName].GroupId}) 253 | } 254 | hostByState[host.State] = append(hostByState[host.State], host.Host) 255 | if StateName[host.State] == "New" || StateName[host.State] == "Updated" { 256 | newHostAmmount++ 257 | log.Infof("GetHostByState = State: %s, Name: %s", StateName[host.State], host.Name) 258 | } else { 259 | log.Debugf("GetHostByState = State: %s, Name: %s", StateName[host.State], host.Name) 260 | } 261 | } 262 | 263 | log.Infof("HOSTS, total: %v, new or updated: %v", len(z.Hosts), newHostAmmount) 264 | return hostByState 265 | } 266 | 267 | func (z *CustomZabbix) GetHostGroupsByState() (hostGroupsByState map[State]zabbix.HostGroups) { 268 | 269 | hostGroupsByState = map[State]zabbix.HostGroups{ 270 | StateNew: zabbix.HostGroups{}, 271 | StateOld: zabbix.HostGroups{}, 272 | StateUpdated: zabbix.HostGroups{}, 273 | StateEqual: zabbix.HostGroups{}, 274 | } 275 | 276 | newHostGroupAmmount := 0 277 | for _, hostGroup := range z.HostGroups { 278 | hostGroupsByState[hostGroup.State] = append(hostGroupsByState[hostGroup.State], hostGroup.HostGroup) 279 | if StateName[hostGroup.State] == "New" || StateName[hostGroup.State] == "Updated" { 280 | newHostGroupAmmount++ 281 | log.Infof("GetHostGroupsByState = State: %s, Name: %s", StateName[hostGroup.State], hostGroup.Name) 282 | } else { 283 | log.Debugf("GetHostGroupsByState = State: %s, Name: %s", StateName[hostGroup.State], hostGroup.Name) 284 | } 285 | } 286 | 287 | log.Infof("HOSTGROUPS, total: %v, new or updated: %v", len(z.HostGroups), newHostGroupAmmount) 288 | 289 | return hostGroupsByState 290 | } 291 | 292 | func (zabbix *CustomZabbix) PropagateCreatedHosts(hosts zabbix.Hosts) { 293 | for _, newHost := range hosts { 294 | if host, ok := zabbix.Hosts[newHost.Name]; ok { 295 | host.HostId = newHost.HostId 296 | } 297 | } 298 | } 299 | 300 | func (zabbix *CustomZabbix) PropagateCreatedHostGroups(hostGroups zabbix.HostGroups) { 301 | for _, newHostGroup := range hostGroups { 302 | if hostGroup, ok := zabbix.HostGroups[newHostGroup.Name]; ok { 303 | hostGroup.GroupId = newHostGroup.GroupId 304 | } 305 | } 306 | } 307 | 308 | func (host *CustomHost) PropagateCreatedApplications(applications zabbix.Applications) { 309 | 310 | for _, application := range applications { 311 | host.Applications[application.Name].ApplicationId = application.ApplicationId 312 | } 313 | } 314 | 315 | func (host *CustomHost) GetItemsByState() (itemsByState map[State]zabbix.Items) { 316 | 317 | itemsByState = map[State]zabbix.Items{ 318 | StateNew: zabbix.Items{}, 319 | StateOld: zabbix.Items{}, 320 | StateUpdated: zabbix.Items{}, 321 | StateEqual: zabbix.Items{}, 322 | } 323 | 324 | newItemAmmount := 0 325 | for _, item := range host.Items { 326 | item.HostId = host.HostId 327 | item.Item.ApplicationIds = []string{} 328 | for appName, _ := range item.Applications { 329 | item.Item.ApplicationIds = append(item.Item.ApplicationIds, host.Applications[appName].ApplicationId) 330 | } 331 | itemsByState[item.State] = append(itemsByState[item.State], item.Item) 332 | if StateName[item.State] == "New" || StateName[item.State] == "Updated" { 333 | newItemAmmount++ 334 | log.Infof("GetItemsByState = State: %s, Key: %s, Applications: %+v", StateName[item.State], item.Key, item.Applications) 335 | } else { 336 | log.Debugf("GetItemsByState = State: %s, Key: %s, Applications: %+v", StateName[item.State], item.Key, item.Applications) 337 | } 338 | } 339 | 340 | log.Infof("ITEMS, total: %v, new or updated: %v", len(host.Items), newItemAmmount) 341 | return itemsByState 342 | } 343 | 344 | func (host *CustomHost) GetTriggersByState() (triggersByState map[State]zabbix.Triggers) { 345 | 346 | triggersByState = map[State]zabbix.Triggers{ 347 | StateNew: zabbix.Triggers{}, 348 | StateOld: zabbix.Triggers{}, 349 | StateUpdated: zabbix.Triggers{}, 350 | StateEqual: zabbix.Triggers{}, 351 | } 352 | 353 | newTriggerAmmount := 0 354 | for _, trigger := range host.Triggers { 355 | triggersByState[trigger.State] = append(triggersByState[trigger.State], trigger.Trigger) 356 | if StateName[trigger.State] == "New" || StateName[trigger.State] == "Updated" { 357 | newTriggerAmmount++ 358 | log.Infof("GetTriggersByState = State: %s, Expression: %s", StateName[trigger.State], trigger.Expression) 359 | } else { 360 | log.Debugf("GetTriggersByState = State: %s, Expression: %s", StateName[trigger.State], trigger.Expression) 361 | } 362 | } 363 | 364 | log.Infof("TRIGGERS, total: %v, new or updated: %v", len(host.Triggers), newTriggerAmmount) 365 | return triggersByState 366 | } 367 | 368 | func (host *CustomHost) GetApplicationsByState() (applicationsByState map[State]zabbix.Applications) { 369 | 370 | applicationsByState = map[State]zabbix.Applications{ 371 | StateNew: zabbix.Applications{}, 372 | StateOld: zabbix.Applications{}, 373 | StateUpdated: zabbix.Applications{}, 374 | StateEqual: zabbix.Applications{}, 375 | } 376 | newAppAmmount := 0 377 | for _, application := range host.Applications { 378 | application.Application.HostId = host.HostId 379 | applicationsByState[application.State] = append(applicationsByState[application.State], application.Application) 380 | if StateName[application.State] == "New" || StateName[application.State] == "Updated" { 381 | newAppAmmount++ 382 | log.Infof("GetApplicationsByState = State: %s, Name: %s", StateName[application.State], application.Name) 383 | } else { 384 | log.Debugf("GetApplicationsByState = State: %s, Name: %s", StateName[application.State], application.Name) 385 | } 386 | } 387 | 388 | log.Infof("APPLICATIONS, total: %v, new or updated: %v", len(host.Applications), newAppAmmount) 389 | return applicationsByState 390 | } 391 | 392 | func GetZabbixPriority(severity string) zabbix.PriorityType { 393 | 394 | switch strings.ToLower(severity) { 395 | case "information": 396 | return zabbix.Information 397 | case "warning": 398 | return zabbix.Warning 399 | case "average": 400 | return zabbix.Average 401 | case "high": 402 | return zabbix.High 403 | case "critical": 404 | return zabbix.Critical 405 | default: 406 | return zabbix.NotClassified 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /zabbixprovisioner/provisioner/provisioner.go: -------------------------------------------------------------------------------- 1 | package provisioner 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | zabbix "github.com/devopyio/zabbix-alertmanager/zabbixprovisioner/zabbixclient" 11 | "github.com/pkg/errors" 12 | log "github.com/sirupsen/logrus" 13 | yaml "gopkg.in/yaml.v2" 14 | ) 15 | 16 | type HostConfig struct { 17 | Name string `yaml:"name"` 18 | HostGroups []string `yaml:"hostGroups"` 19 | Tag string `yaml:"tag"` 20 | DeploymentStatus string `yaml:"deploymentStatus"` 21 | ItemDefaultApplication string `yaml:"itemDefaultApplication"` 22 | ItemDefaultHistory string `yaml:"itemDefaultHistory"` 23 | ItemDefaultTrends string `yaml:"itemDefaultTrends"` 24 | ItemDefaultTrapperHosts string `yaml:"itemDefaultTrapperHosts"` 25 | HostAlertsDir string `yaml:"alertsDir"` 26 | TriggerTags map[string]string `yaml:"triggerTags"` 27 | } 28 | 29 | type Provisioner struct { 30 | api *zabbix.API 31 | keyPrefix string 32 | hosts []HostConfig 33 | prometheusUrl string 34 | *CustomZabbix 35 | } 36 | 37 | func New(prometheusUrl, keyPrefix, url, user, password string, hosts []HostConfig) (*Provisioner, error) { 38 | transport := http.DefaultTransport 39 | 40 | api := zabbix.NewAPI(url) 41 | api.SetClient(&http.Client{ 42 | Transport: transport, 43 | }) 44 | 45 | _, err := api.Login(user, password) 46 | if err != nil { 47 | return nil, errors.Wrap(err, "error while login to zabbix api") 48 | } 49 | 50 | return &Provisioner{ 51 | api: api, 52 | keyPrefix: keyPrefix, 53 | hosts: hosts, 54 | prometheusUrl: prometheusUrl, 55 | }, nil 56 | } 57 | 58 | func LoadHostConfigFromFile(filename string) ([]HostConfig, error) { 59 | configFile, err := ioutil.ReadFile(filename) 60 | if err != nil { 61 | return nil, errors.Wrapf(err, "can't open the config file: %s", filename) 62 | } 63 | 64 | hosts := []HostConfig{} 65 | 66 | err = yaml.Unmarshal(configFile, &hosts) 67 | if err != nil { 68 | return nil, errors.Wrapf(err, "can't read the config file: %s", filename) 69 | } 70 | 71 | return hosts, nil 72 | } 73 | 74 | func (p *Provisioner) Run() error { 75 | p.CustomZabbix = &CustomZabbix{ 76 | Hosts: map[string]*CustomHost{}, 77 | HostGroups: map[string]*CustomHostGroup{}, 78 | } 79 | 80 | //All hosts will have the rules which were only written for them 81 | for _, host := range p.hosts { 82 | if err := p.LoadRulesFromPrometheus(host); err != nil { 83 | return errors.Wrapf(err, "error loading prometheus rules, file: %s", host.HostAlertsDir) 84 | } 85 | } 86 | 87 | if err := p.LoadDataFromZabbix(); err != nil { 88 | return errors.Wrap(err, "error loading zabbix rules") 89 | } 90 | 91 | if err := p.ApplyChanges(); err != nil { 92 | return errors.Wrap(err, "error applying changes") 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // Create hosts structures and populate them from Prometheus rules 99 | func (p *Provisioner) LoadRulesFromPrometheus(hostConfig HostConfig) error { 100 | rules, err := LoadPrometheusRulesFromDir(hostConfig.HostAlertsDir) 101 | if err != nil { 102 | return errors.Wrap(err, "error loading rules") 103 | } 104 | 105 | log.Infof("Prometheus Rules for host - %v loaded: %v", hostConfig.Name, len(rules)) 106 | 107 | newHost := &CustomHost{ 108 | State: StateNew, 109 | Host: zabbix.Host{ 110 | Host: hostConfig.Name, 111 | Available: 1, 112 | Name: hostConfig.Name, 113 | Status: 0, 114 | InventoryMode: zabbix.InventoryManual, 115 | Inventory: map[string]string{ 116 | "deployment_status": hostConfig.DeploymentStatus, 117 | "tag": hostConfig.Tag, 118 | }, 119 | Interfaces: zabbix.HostInterfaces{ 120 | zabbix.HostInterface{ 121 | DNS: "", 122 | IP: "127.0.0.1", 123 | Main: 1, 124 | Port: "10050", 125 | Type: 1, 126 | UseIP: 1, 127 | }, 128 | }, 129 | }, 130 | HostGroups: make(map[string]struct{}, len(hostConfig.HostGroups)), 131 | Items: map[string]*CustomItem{}, 132 | Applications: map[string]*CustomApplication{}, 133 | Triggers: map[string]*CustomTrigger{}, 134 | } 135 | 136 | for _, hostGroupName := range hostConfig.HostGroups { 137 | p.AddHostGroup(&CustomHostGroup{ 138 | State: StateNew, 139 | HostGroup: zabbix.HostGroup{ 140 | Name: hostGroupName, 141 | }, 142 | }) 143 | 144 | newHost.HostGroups[hostGroupName] = struct{}{} 145 | } 146 | 147 | // Parse Prometheus rules and create corresponding items/triggers and applications for this host 148 | for _, rule := range rules { 149 | key := fmt.Sprintf("%s.%s", strings.ToLower(p.keyPrefix), strings.ToLower(rule.Name)) 150 | 151 | var triggerTags []zabbix.Tag 152 | for k, v := range hostConfig.TriggerTags { 153 | triggerTags = append(triggerTags, zabbix.Tag{Tag: k, Value: v}) 154 | } 155 | 156 | newItem := &CustomItem{ 157 | State: StateNew, 158 | Item: zabbix.Item{ 159 | Name: rule.Name, 160 | Key: key, 161 | HostId: "", //To be filled when the host will be created 162 | Type: 2, //Trapper 163 | ValueType: 3, 164 | History: hostConfig.ItemDefaultHistory, 165 | Trends: hostConfig.ItemDefaultTrends, 166 | TrapperHosts: hostConfig.ItemDefaultTrapperHosts, 167 | }, 168 | Applications: map[string]struct{}{}, 169 | } 170 | 171 | newTrigger := &CustomTrigger{ 172 | State: StateNew, 173 | Trigger: zabbix.Trigger{ 174 | Description: rule.Name, 175 | Expression: fmt.Sprintf("{%s:%s.last()}<>0", newHost.Name, key), 176 | ManualClose: 1, 177 | Tags: triggerTags, 178 | }, 179 | } 180 | 181 | if p.prometheusUrl != "" { 182 | newTrigger.URL = p.prometheusUrl + "/alerts" 183 | 184 | url := p.prometheusUrl + "/graph?g0.expr=" + url.QueryEscape(rule.Expression) 185 | if len(url) < 255 { 186 | newTrigger.URL = url 187 | } 188 | } 189 | 190 | if v, ok := rule.Annotations["summary"]; ok { 191 | newTrigger.Comments = v 192 | } else if v, ok := rule.Annotations["message"]; ok { 193 | newTrigger.Comments = v 194 | } else if v, ok := rule.Annotations["description"]; ok { 195 | newTrigger.Comments = v 196 | } 197 | 198 | if v, ok := rule.Labels["severity"]; ok { 199 | newTrigger.Priority = GetZabbixPriority(v) 200 | } 201 | 202 | // Add the special "No Data" trigger if requested 203 | if delay, ok := rule.Annotations["zabbix_trigger_nodata"]; ok { 204 | newTrigger.Trigger.Description = fmt.Sprintf("%s - no data for the last %s seconds", newTrigger.Trigger.Description, delay) 205 | newTrigger.Trigger.Expression = fmt.Sprintf("{%s:%s.nodata(%s)}", newHost.Name, key, delay) 206 | } 207 | 208 | // If no applications are found in the rule, add the default application declared in the configuration 209 | if len(newItem.Applications) == 0 { 210 | newHost.AddApplication(&CustomApplication{ 211 | State: StateNew, 212 | Application: zabbix.Application{ 213 | Name: hostConfig.ItemDefaultApplication, 214 | }, 215 | }) 216 | newItem.Applications[hostConfig.ItemDefaultApplication] = struct{}{} 217 | } 218 | 219 | log.Debugf("Loading item from Prometheus: %+v", newItem) 220 | newHost.AddItem(newItem) 221 | 222 | log.Debugf("Loading trigger from Prometheus: %+v", newTrigger) 223 | newHost.AddTrigger(newTrigger) 224 | 225 | } 226 | log.Debugf("Host from Prometheus: %+v", newHost) 227 | p.AddHost(newHost) 228 | 229 | return nil 230 | } 231 | 232 | // Update created hosts with the current state in Zabbix 233 | func (p *Provisioner) LoadDataFromZabbix() error { 234 | hostNames := make([]string, len(p.hosts)) 235 | hostGroupNames := []string{} 236 | for i, _ := range p.hosts { 237 | hostNames[i] = p.hosts[i].Name 238 | hostGroupNames = append(hostGroupNames, p.hosts[i].HostGroups...) 239 | } 240 | 241 | if len(hostNames) == 0 { 242 | return errors.Errorf("error no hosts are defined") 243 | } 244 | 245 | zabbixHostGroups, err := p.api.HostGroupsGet(zabbix.Params{ 246 | "output": "extend", 247 | "filter": map[string][]string{ 248 | "name": hostGroupNames, 249 | }, 250 | }) 251 | if err != nil { 252 | return errors.Wrapf(err, "error getting hostgroups: %v", hostGroupNames) 253 | } 254 | 255 | for _, zabbixHostGroup := range zabbixHostGroups { 256 | p.AddHostGroup(&CustomHostGroup{ 257 | State: StateOld, 258 | HostGroup: zabbixHostGroup, 259 | }) 260 | } 261 | 262 | zabbixHosts, err := p.api.HostsGet(zabbix.Params{ 263 | "output": "extend", 264 | "selectInventory": []string{ 265 | "tag", 266 | "deployment_status", 267 | }, 268 | "filter": map[string][]string{ 269 | "host": hostNames, 270 | }, 271 | }) 272 | if err != nil { 273 | return errors.Wrapf(err, "error getting hosts: %v", hostNames) 274 | } 275 | 276 | for _, zabbixHost := range zabbixHosts { 277 | zabbixHostGroups, err := p.api.HostGroupsGet(zabbix.Params{ 278 | "output": "extend", 279 | "hostids": zabbixHost.HostId, 280 | }) 281 | if err != nil { 282 | return errors.Wrapf(err, "error getting hostgroup, hostid: %v", zabbixHost.HostId) 283 | } 284 | 285 | hostGroups := make(map[string]struct{}, len(zabbixHostGroups)) 286 | for _, zabbixHostGroup := range zabbixHostGroups { 287 | hostGroups[zabbixHostGroup.Name] = struct{}{} 288 | } 289 | 290 | // Remove hostid because the Zabbix api add it automatically and it breaks the comparison between new/old hosts 291 | delete(zabbixHost.Inventory, "hostid") 292 | 293 | oldHost := p.AddHost(&CustomHost{ 294 | State: StateOld, 295 | Host: zabbixHost, 296 | HostGroups: hostGroups, 297 | Items: map[string]*CustomItem{}, 298 | Applications: map[string]*CustomApplication{}, 299 | Triggers: map[string]*CustomTrigger{}, 300 | }) 301 | log.Debugf("Load host from Zabbix: %+v", oldHost) 302 | 303 | zabbixApplications, err := p.api.ApplicationsGet(zabbix.Params{ 304 | "output": "extend", 305 | "hostids": oldHost.HostId, 306 | }) 307 | if err != nil { 308 | return errors.Wrapf(err, "error getting application, hostid: %v", oldHost.HostId) 309 | } 310 | 311 | for _, zabbixApplication := range zabbixApplications { 312 | oldHost.AddApplication(&CustomApplication{ 313 | State: StateOld, 314 | Application: zabbixApplication, 315 | }) 316 | } 317 | 318 | zabbixItems, err := p.api.ItemsGet(zabbix.Params{ 319 | "output": "extend", 320 | "hostids": oldHost.Host.HostId, 321 | }) 322 | if err != nil { 323 | return errors.Wrapf(err, "error getting item, hostid: %v", oldHost.Host.HostId) 324 | } 325 | 326 | for _, zabbixItem := range zabbixItems { 327 | newItem := &CustomItem{ 328 | State: StateOld, 329 | Item: zabbixItem, 330 | } 331 | 332 | zabbixApplications, err := p.api.ApplicationsGet(zabbix.Params{ 333 | "output": "extend", 334 | "itemids": zabbixItem.ItemId, 335 | }) 336 | if err != nil { 337 | return errors.Wrapf(err, "error getting item, itemid: %v", oldHost.Host.HostId) 338 | } 339 | 340 | newItem.Applications = make(map[string]struct{}, len(zabbixApplications)) 341 | for _, zabbixApplication := range zabbixApplications { 342 | newItem.Applications[zabbixApplication.Name] = struct{}{} 343 | } 344 | 345 | log.Debugf("Loading item from Zabbix: %+v", newItem) 346 | oldHost.AddItem(newItem) 347 | } 348 | 349 | zabbixTriggers, err := p.api.TriggersGet(zabbix.Params{ 350 | "output": "extend", 351 | "hostids": oldHost.Host.HostId, 352 | "expandExpression": true, 353 | }) 354 | if err != nil { 355 | return errors.Wrapf(err, "error getting zabbix triggers, hostids: %v", oldHost.Host.HostId) 356 | } 357 | 358 | for _, zabbixTrigger := range zabbixTriggers { 359 | newTrigger := &CustomTrigger{ 360 | State: StateOld, 361 | Trigger: zabbixTrigger, 362 | } 363 | 364 | log.Debugf("Loading trigger from Zabbix: %+v", newTrigger) 365 | oldHost.AddTrigger(newTrigger) 366 | } 367 | } 368 | return nil 369 | } 370 | 371 | func (p *Provisioner) ApplyChanges() error { 372 | hostGroupsByState := p.GetHostGroupsByState() 373 | if len(hostGroupsByState[StateNew]) != 0 { 374 | log.Debugf("Creating HostGroups: %+v\n", hostGroupsByState[StateNew]) 375 | err := p.api.HostGroupsCreate(hostGroupsByState[StateNew]) 376 | if err != nil { 377 | return errors.Wrap(err, "Failed in creating hostgroups") 378 | } 379 | } 380 | 381 | // Make sure we update ids for the newly created host groups 382 | p.PropagateCreatedHostGroups(hostGroupsByState[StateNew]) 383 | 384 | hostsByState := p.GetHostsByState() 385 | if len(hostsByState[StateNew]) != 0 { 386 | log.Debugf("Creating Hosts: %+v\n", hostsByState[StateNew]) 387 | err := p.api.HostsCreate(hostsByState[StateNew]) 388 | if err != nil { 389 | return errors.Wrap(err, "Failed in creating host") 390 | } 391 | } 392 | 393 | // Make sure we update ids for the newly created hosts 394 | p.PropagateCreatedHosts(hostsByState[StateNew]) 395 | 396 | if len(hostsByState[StateUpdated]) != 0 { 397 | log.Debugf("Updating Hosts: %+v\n", hostsByState[StateUpdated]) 398 | err := p.api.HostsUpdate(hostsByState[StateUpdated]) 399 | if err != nil { 400 | return errors.Wrap(err, "Failed in updating host") 401 | } 402 | } 403 | 404 | for _, host := range p.Hosts { 405 | log.Debugf("Updating host, hostName: %s", host.Name) 406 | 407 | applicationsByState := host.GetApplicationsByState() 408 | if len(applicationsByState[StateOld]) != 0 { 409 | log.Debugf("Deleting applications: %+v\n", applicationsByState[StateOld]) 410 | err := p.api.ApplicationsDelete(applicationsByState[StateOld]) 411 | if err != nil { 412 | return errors.Wrap(err, "Failed in deleting applications") 413 | } 414 | } 415 | 416 | if len(applicationsByState[StateNew]) != 0 { 417 | log.Debugf("Creating applications: %+v\n", applicationsByState[StateNew]) 418 | err := p.api.ApplicationsCreate(applicationsByState[StateNew]) 419 | if err != nil { 420 | return errors.Wrap(err, "Failed in creating applications") 421 | } 422 | } 423 | host.PropagateCreatedApplications(applicationsByState[StateNew]) 424 | 425 | itemsByState := host.GetItemsByState() 426 | triggersByState := host.GetTriggersByState() 427 | 428 | if len(triggersByState[StateOld]) != 0 { 429 | log.Debugf("Deleting triggers: %+v\n", triggersByState[StateOld]) 430 | err := p.api.TriggersDelete(triggersByState[StateOld]) 431 | if err != nil { 432 | return errors.Wrap(err, "Failed in deleting triggers") 433 | } 434 | } 435 | 436 | if len(itemsByState[StateOld]) != 0 { 437 | log.Debugf("Deleting items: %+v\n", itemsByState[StateOld]) 438 | err := p.api.ItemsDelete(itemsByState[StateOld]) 439 | if err != nil { 440 | return errors.Wrap(err, "Failed in deleting items") 441 | } 442 | } 443 | 444 | if len(itemsByState[StateUpdated]) != 0 { 445 | log.Debugf("Updating items: %+v\n", itemsByState[StateUpdated]) 446 | err := p.api.ItemsUpdate(itemsByState[StateUpdated]) 447 | if err != nil { 448 | return errors.Wrap(err, "Failed in updating items") 449 | } 450 | } 451 | 452 | if len(triggersByState[StateUpdated]) != 0 { 453 | log.Debugf("Updating triggers: %+v\n", triggersByState[StateUpdated]) 454 | err := p.api.TriggersUpdate(triggersByState[StateUpdated]) 455 | if err != nil { 456 | return errors.Wrap(err, "Failed in updating triggers") 457 | } 458 | } 459 | 460 | if len(itemsByState[StateNew]) != 0 { 461 | log.Debugf("Creating items: %+v\n", itemsByState[StateNew]) 462 | err := p.api.ItemsCreate(itemsByState[StateNew]) 463 | if err != nil { 464 | return errors.Wrap(err, "Failed in creating items") 465 | } 466 | } 467 | 468 | if len(triggersByState[StateNew]) != 0 { 469 | log.Debugf("Creating triggers: %+v\n", triggersByState[StateNew]) 470 | err := p.api.TriggersCreate(triggersByState[StateNew]) 471 | if err != nil { 472 | return errors.Wrap(err, "Failed in creating triggers") 473 | } 474 | } 475 | } 476 | return nil 477 | } 478 | -------------------------------------------------------------------------------- /grafana.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "VAR_LABELSELECTOR", 5 | "type": "constant", 6 | "label": "labelselector", 7 | "value": "name", 8 | "description": "" 9 | }, 10 | { 11 | "name": "VAR_LABELVALUE", 12 | "type": "constant", 13 | "label": "labelvalue", 14 | "value": "zal", 15 | "description": "" 16 | } 17 | ], 18 | "__requires": [ 19 | { 20 | "type": "grafana", 21 | "id": "grafana", 22 | "name": "Grafana", 23 | "version": "6.0.1" 24 | }, 25 | { 26 | "type": "panel", 27 | "id": "graph", 28 | "name": "Graph", 29 | "version": "5.0.0" 30 | }, 31 | { 32 | "type": "panel", 33 | "id": "table", 34 | "name": "Table", 35 | "version": "5.0.0" 36 | } 37 | ], 38 | "annotations": { 39 | "list": [ 40 | { 41 | "builtIn": 1, 42 | "datasource": "-- Grafana --", 43 | "enable": true, 44 | "hide": true, 45 | "iconColor": "rgba(0, 211, 255, 1)", 46 | "name": "Annotations & Alerts", 47 | "type": "dashboard" 48 | } 49 | ] 50 | }, 51 | "editable": true, 52 | "gnetId": null, 53 | "graphTooltip": 0, 54 | "id": null, 55 | "iteration": 1554014406953, 56 | "links": [], 57 | "panels": [ 58 | { 59 | "aliasColors": {}, 60 | "bars": false, 61 | "dashLength": 10, 62 | "dashes": false, 63 | "description": "Alerts sent to Zabbix", 64 | "fill": 1, 65 | "gridPos": { 66 | "h": 9, 67 | "w": 12, 68 | "x": 0, 69 | "y": 0 70 | }, 71 | "id": 2, 72 | "interval": "2m", 73 | "legend": { 74 | "avg": false, 75 | "current": false, 76 | "hideEmpty": true, 77 | "max": false, 78 | "min": false, 79 | "show": false, 80 | "total": false, 81 | "values": false 82 | }, 83 | "lines": true, 84 | "linewidth": 2, 85 | "links": [], 86 | "nullPointMode": "null as zero", 87 | "paceLength": 10, 88 | "percentage": false, 89 | "pointradius": 2, 90 | "points": false, 91 | "renderer": "flot", 92 | "seriesOverrides": [ 93 | { 94 | "alias": "sent", 95 | "color": "#73BF69" 96 | } 97 | ], 98 | "stack": false, 99 | "steppedLine": false, 100 | "targets": [ 101 | { 102 | "expr": "sum(rate(alerts_sent_total{$labelselector=\"$labelvalue\"}[$__interval]))", 103 | "format": "time_series", 104 | "intervalFactor": 1, 105 | "legendFormat": "sent", 106 | "refId": "B" 107 | } 108 | ], 109 | "thresholds": [], 110 | "timeFrom": null, 111 | "timeRegions": [], 112 | "timeShift": null, 113 | "title": "Alerts sent to Zabbix", 114 | "tooltip": { 115 | "shared": true, 116 | "sort": 0, 117 | "value_type": "individual" 118 | }, 119 | "type": "graph", 120 | "xaxis": { 121 | "buckets": null, 122 | "mode": "time", 123 | "name": null, 124 | "show": true, 125 | "values": [] 126 | }, 127 | "yaxes": [ 128 | { 129 | "format": "short", 130 | "label": null, 131 | "logBase": 1, 132 | "max": null, 133 | "min": "0", 134 | "show": true 135 | }, 136 | { 137 | "format": "short", 138 | "label": null, 139 | "logBase": 1, 140 | "max": null, 141 | "min": null, 142 | "show": true 143 | } 144 | ], 145 | "yaxis": { 146 | "align": false, 147 | "alignLevel": null 148 | } 149 | }, 150 | { 151 | "aliasColors": {}, 152 | "bars": false, 153 | "dashLength": 10, 154 | "dashes": false, 155 | "description": "Alerts sent to Zabbix", 156 | "fill": 1, 157 | "gridPos": { 158 | "h": 9, 159 | "w": 12, 160 | "x": 12, 161 | "y": 0 162 | }, 163 | "id": 10, 164 | "interval": "2m", 165 | "legend": { 166 | "avg": false, 167 | "current": false, 168 | "hideEmpty": true, 169 | "max": false, 170 | "min": false, 171 | "show": false, 172 | "total": false, 173 | "values": false 174 | }, 175 | "lines": true, 176 | "linewidth": 2, 177 | "links": [], 178 | "nullPointMode": "null", 179 | "paceLength": 10, 180 | "percentage": false, 181 | "pointradius": 2, 182 | "points": false, 183 | "renderer": "flot", 184 | "seriesOverrides": [ 185 | { 186 | "alias": "errors", 187 | "color": "#F2495C" 188 | } 189 | ], 190 | "stack": false, 191 | "steppedLine": false, 192 | "targets": [ 193 | { 194 | "expr": "sum(rate(alerts_errors_total{$labelselector=\"$labelvalue\"}[$__interval]))", 195 | "format": "time_series", 196 | "intervalFactor": 1, 197 | "legendFormat": "errors", 198 | "refId": "A" 199 | } 200 | ], 201 | "thresholds": [], 202 | "timeFrom": null, 203 | "timeRegions": [], 204 | "timeShift": null, 205 | "title": "Failed Alerts", 206 | "tooltip": { 207 | "shared": true, 208 | "sort": 0, 209 | "value_type": "individual" 210 | }, 211 | "type": "graph", 212 | "xaxis": { 213 | "buckets": null, 214 | "mode": "time", 215 | "name": null, 216 | "show": true, 217 | "values": [] 218 | }, 219 | "yaxes": [ 220 | { 221 | "format": "short", 222 | "label": null, 223 | "logBase": 1, 224 | "max": null, 225 | "min": "0", 226 | "show": true 227 | }, 228 | { 229 | "format": "short", 230 | "label": null, 231 | "logBase": 1, 232 | "max": null, 233 | "min": null, 234 | "show": true 235 | } 236 | ], 237 | "yaxis": { 238 | "align": false, 239 | "alignLevel": null 240 | } 241 | }, 242 | { 243 | "collapsed": false, 244 | "gridPos": { 245 | "h": 1, 246 | "w": 24, 247 | "x": 0, 248 | "y": 9 249 | }, 250 | "id": 6, 251 | "panels": [], 252 | "title": "Ops", 253 | "type": "row" 254 | }, 255 | { 256 | "aliasColors": {}, 257 | "bars": false, 258 | "dashLength": 10, 259 | "dashes": false, 260 | "fill": 1, 261 | "gridPos": { 262 | "h": 8, 263 | "w": 12, 264 | "x": 0, 265 | "y": 10 266 | }, 267 | "id": 4, 268 | "legend": { 269 | "alignAsTable": true, 270 | "avg": false, 271 | "current": false, 272 | "max": false, 273 | "min": false, 274 | "rightSide": true, 275 | "show": true, 276 | "total": false, 277 | "values": false 278 | }, 279 | "lines": true, 280 | "linewidth": 1, 281 | "links": [], 282 | "nullPointMode": "null", 283 | "paceLength": 10, 284 | "percentage": false, 285 | "pointradius": 2, 286 | "points": false, 287 | "renderer": "flot", 288 | "seriesOverrides": [], 289 | "stack": false, 290 | "steppedLine": false, 291 | "targets": [ 292 | { 293 | "expr": "process_resident_memory_bytes{$labelselector=\"$labelvalue\"}", 294 | "format": "time_series", 295 | "intervalFactor": 1, 296 | "legendFormat": "{{kubernetes_pod_name}} resident memory ", 297 | "refId": "A" 298 | }, 299 | { 300 | "expr": "process_virtual_memory_bytes{$labelselector=\"$labelvalue\"}", 301 | "format": "time_series", 302 | "intervalFactor": 1, 303 | "legendFormat": "{{kubernetes_pod_name}} virtual memory ", 304 | "refId": "B" 305 | } 306 | ], 307 | "thresholds": [], 308 | "timeFrom": null, 309 | "timeRegions": [], 310 | "timeShift": null, 311 | "title": "Memory", 312 | "tooltip": { 313 | "shared": true, 314 | "sort": 0, 315 | "value_type": "individual" 316 | }, 317 | "type": "graph", 318 | "xaxis": { 319 | "buckets": null, 320 | "mode": "time", 321 | "name": null, 322 | "show": true, 323 | "values": [] 324 | }, 325 | "yaxes": [ 326 | { 327 | "format": "bytes", 328 | "label": null, 329 | "logBase": 1, 330 | "max": null, 331 | "min": null, 332 | "show": true 333 | }, 334 | { 335 | "format": "short", 336 | "label": null, 337 | "logBase": 1, 338 | "max": null, 339 | "min": null, 340 | "show": true 341 | } 342 | ], 343 | "yaxis": { 344 | "align": false, 345 | "alignLevel": null 346 | } 347 | }, 348 | { 349 | "aliasColors": {}, 350 | "bars": false, 351 | "dashLength": 10, 352 | "dashes": false, 353 | "fill": 1, 354 | "gridPos": { 355 | "h": 8, 356 | "w": 12, 357 | "x": 12, 358 | "y": 10 359 | }, 360 | "id": 8, 361 | "legend": { 362 | "alignAsTable": true, 363 | "avg": false, 364 | "current": false, 365 | "max": false, 366 | "min": false, 367 | "rightSide": true, 368 | "show": true, 369 | "total": false, 370 | "values": false 371 | }, 372 | "lines": true, 373 | "linewidth": 1, 374 | "links": [], 375 | "nullPointMode": "null", 376 | "paceLength": 10, 377 | "percentage": false, 378 | "pointradius": 2, 379 | "points": false, 380 | "renderer": "flot", 381 | "seriesOverrides": [], 382 | "stack": false, 383 | "steppedLine": false, 384 | "targets": [ 385 | { 386 | "expr": "go_gc_duration_seconds{$labelselector=\"$labelvalue\",quantile=\"0.75\"}", 387 | "format": "time_series", 388 | "interval": "", 389 | "intervalFactor": 1, 390 | "legendFormat": "{{quantile}} {{kubernetes_pod_name}}", 391 | "refId": "A" 392 | }, 393 | { 394 | "expr": "go_gc_duration_seconds{$labelselector=\"$labelvalue\",quantile=\"1\"}", 395 | "format": "time_series", 396 | "intervalFactor": 1, 397 | "legendFormat": "{{quantile}} {{kubernetes_pod_name}}", 398 | "refId": "B" 399 | } 400 | ], 401 | "thresholds": [], 402 | "timeFrom": null, 403 | "timeRegions": [], 404 | "timeShift": null, 405 | "title": "Gargage Collection Duration", 406 | "tooltip": { 407 | "shared": true, 408 | "sort": 0, 409 | "value_type": "individual" 410 | }, 411 | "type": "graph", 412 | "xaxis": { 413 | "buckets": null, 414 | "mode": "time", 415 | "name": null, 416 | "show": true, 417 | "values": [] 418 | }, 419 | "yaxes": [ 420 | { 421 | "format": "s", 422 | "label": null, 423 | "logBase": 1, 424 | "max": null, 425 | "min": null, 426 | "show": true 427 | }, 428 | { 429 | "format": "short", 430 | "label": null, 431 | "logBase": 1, 432 | "max": null, 433 | "min": null, 434 | "show": true 435 | } 436 | ], 437 | "yaxis": { 438 | "align": false, 439 | "alignLevel": null 440 | } 441 | }, 442 | { 443 | "aliasColors": {}, 444 | "bars": false, 445 | "dashLength": 10, 446 | "dashes": false, 447 | "fill": 10, 448 | "gridPos": { 449 | "h": 8, 450 | "w": 12, 451 | "x": 0, 452 | "y": 18 453 | }, 454 | "id": 9, 455 | "interval": "2m", 456 | "legend": { 457 | "alignAsTable": true, 458 | "avg": false, 459 | "current": false, 460 | "max": false, 461 | "min": false, 462 | "rightSide": true, 463 | "show": true, 464 | "total": false, 465 | "values": false 466 | }, 467 | "lines": true, 468 | "linewidth": 1, 469 | "links": [], 470 | "nullPointMode": "null as zero", 471 | "paceLength": 10, 472 | "percentage": false, 473 | "pointradius": 2, 474 | "points": false, 475 | "renderer": "flot", 476 | "seriesOverrides": [], 477 | "stack": true, 478 | "steppedLine": false, 479 | "targets": [ 480 | { 481 | "expr": "rate(process_cpu_seconds_total{$labelselector=\"$labelvalue\"}[$__interval])", 482 | "format": "time_series", 483 | "intervalFactor": 1, 484 | "legendFormat": "{{kubernetes_pod_name}} open", 485 | "refId": "A" 486 | } 487 | ], 488 | "thresholds": [], 489 | "timeFrom": null, 490 | "timeRegions": [], 491 | "timeShift": null, 492 | "title": "CPU usage", 493 | "tooltip": { 494 | "shared": true, 495 | "sort": 0, 496 | "value_type": "individual" 497 | }, 498 | "type": "graph", 499 | "xaxis": { 500 | "buckets": null, 501 | "mode": "time", 502 | "name": null, 503 | "show": true, 504 | "values": [] 505 | }, 506 | "yaxes": [ 507 | { 508 | "format": "short", 509 | "label": null, 510 | "logBase": 1, 511 | "max": null, 512 | "min": null, 513 | "show": true 514 | }, 515 | { 516 | "format": "short", 517 | "label": null, 518 | "logBase": 1, 519 | "max": null, 520 | "min": null, 521 | "show": true 522 | } 523 | ], 524 | "yaxis": { 525 | "align": false, 526 | "alignLevel": null 527 | } 528 | }, 529 | { 530 | "aliasColors": {}, 531 | "bars": false, 532 | "dashLength": 10, 533 | "dashes": false, 534 | "fill": 10, 535 | "gridPos": { 536 | "h": 8, 537 | "w": 12, 538 | "x": 12, 539 | "y": 18 540 | }, 541 | "id": 7, 542 | "legend": { 543 | "alignAsTable": true, 544 | "avg": false, 545 | "current": false, 546 | "max": false, 547 | "min": false, 548 | "rightSide": true, 549 | "show": true, 550 | "total": false, 551 | "values": false 552 | }, 553 | "lines": true, 554 | "linewidth": 1, 555 | "links": [], 556 | "nullPointMode": "null as zero", 557 | "paceLength": 10, 558 | "percentage": false, 559 | "pointradius": 2, 560 | "points": false, 561 | "renderer": "flot", 562 | "seriesOverrides": [], 563 | "stack": true, 564 | "steppedLine": false, 565 | "targets": [ 566 | { 567 | "expr": "process_open_fds{$labelselector=\"$labelvalue\"}", 568 | "format": "time_series", 569 | "intervalFactor": 1, 570 | "legendFormat": "{{kubernetes_pod_name}} open", 571 | "refId": "A" 572 | } 573 | ], 574 | "thresholds": [], 575 | "timeFrom": null, 576 | "timeRegions": [], 577 | "timeShift": null, 578 | "title": "File Descriptor", 579 | "tooltip": { 580 | "shared": true, 581 | "sort": 0, 582 | "value_type": "individual" 583 | }, 584 | "type": "graph", 585 | "xaxis": { 586 | "buckets": null, 587 | "mode": "time", 588 | "name": null, 589 | "show": true, 590 | "values": [] 591 | }, 592 | "yaxes": [ 593 | { 594 | "format": "short", 595 | "label": null, 596 | "logBase": 1, 597 | "max": null, 598 | "min": null, 599 | "show": true 600 | }, 601 | { 602 | "format": "short", 603 | "label": null, 604 | "logBase": 1, 605 | "max": null, 606 | "min": null, 607 | "show": true 608 | } 609 | ], 610 | "yaxis": { 611 | "align": false, 612 | "alignLevel": null 613 | } 614 | }, 615 | { 616 | "columns": [], 617 | "fontSize": "100%", 618 | "gridPos": { 619 | "h": 8, 620 | "w": 12, 621 | "x": 0, 622 | "y": 26 623 | }, 624 | "id": 12, 625 | "links": [], 626 | "pageSize": null, 627 | "scroll": true, 628 | "showHeader": true, 629 | "sort": { 630 | "col": 0, 631 | "desc": true 632 | }, 633 | "styles": [ 634 | { 635 | "alias": "Time", 636 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 637 | "pattern": "Time", 638 | "type": "date" 639 | }, 640 | { 641 | "alias": "", 642 | "colorMode": null, 643 | "colors": [ 644 | "rgba(245, 54, 54, 0.9)", 645 | "rgba(237, 129, 40, 0.89)", 646 | "rgba(50, 172, 45, 0.97)" 647 | ], 648 | "decimals": 2, 649 | "pattern": "Value", 650 | "thresholds": [], 651 | "type": "number", 652 | "unit": "short" 653 | } 654 | ], 655 | "targets": [ 656 | { 657 | "expr": "sum(zal_build_info{$labelselector=\"$labelvalue\"}) by (goversion, kubernetes_pod_name, version)", 658 | "format": "table", 659 | "instant": true, 660 | "intervalFactor": 1, 661 | "refId": "A" 662 | } 663 | ], 664 | "timeFrom": null, 665 | "timeShift": null, 666 | "title": "Version", 667 | "transform": "table", 668 | "type": "table" 669 | }, 670 | { 671 | "columns": [], 672 | "fontSize": "100%", 673 | "gridPos": { 674 | "h": 8, 675 | "w": 12, 676 | "x": 12, 677 | "y": 26 678 | }, 679 | "id": 13, 680 | "links": [], 681 | "pageSize": null, 682 | "scroll": true, 683 | "showHeader": true, 684 | "sort": { 685 | "col": 0, 686 | "desc": true 687 | }, 688 | "styles": [ 689 | { 690 | "alias": "Time", 691 | "dateFormat": "YYYY-MM-DD HH:mm:ss", 692 | "pattern": "Time", 693 | "type": "date" 694 | }, 695 | { 696 | "alias": "", 697 | "colorMode": null, 698 | "colors": [ 699 | "rgba(245, 54, 54, 0.9)", 700 | "rgba(237, 129, 40, 0.89)", 701 | "rgba(50, 172, 45, 0.97)" 702 | ], 703 | "decimals": 2, 704 | "pattern": "Value", 705 | "thresholds": [], 706 | "type": "number", 707 | "unit": "short" 708 | } 709 | ], 710 | "targets": [ 711 | { 712 | "expr": "sum(go_mod_info{$labelselector=\"$labelvalue\"}) by (exported_name, version)", 713 | "format": "table", 714 | "instant": true, 715 | "intervalFactor": 1, 716 | "legendFormat": "", 717 | "refId": "A" 718 | } 719 | ], 720 | "timeFrom": null, 721 | "timeShift": null, 722 | "title": "Modules", 723 | "transform": "table", 724 | "type": "table" 725 | } 726 | ], 727 | "schemaVersion": 18, 728 | "style": "dark", 729 | "tags": [], 730 | "templating": { 731 | "list": [ 732 | { 733 | "current": { 734 | "value": "${VAR_LABELSELECTOR}", 735 | "text": "${VAR_LABELSELECTOR}" 736 | }, 737 | "hide": 2, 738 | "label": "labelselector", 739 | "name": "labelselector", 740 | "options": [ 741 | { 742 | "value": "${VAR_LABELSELECTOR}", 743 | "text": "${VAR_LABELSELECTOR}" 744 | } 745 | ], 746 | "query": "${VAR_LABELSELECTOR}", 747 | "skipUrlSync": false, 748 | "type": "constant" 749 | }, 750 | { 751 | "current": { 752 | "value": "${VAR_LABELVALUE}", 753 | "text": "${VAR_LABELVALUE}" 754 | }, 755 | "hide": 2, 756 | "label": "labelvalue", 757 | "name": "labelvalue", 758 | "options": [ 759 | { 760 | "value": "${VAR_LABELVALUE}", 761 | "text": "${VAR_LABELVALUE}" 762 | } 763 | ], 764 | "query": "${VAR_LABELVALUE}", 765 | "skipUrlSync": false, 766 | "type": "constant" 767 | } 768 | ] 769 | }, 770 | "time": { 771 | "from": "now-6h", 772 | "to": "now" 773 | }, 774 | "timepicker": { 775 | "refresh_intervals": [ 776 | "5s", 777 | "10s", 778 | "30s", 779 | "1m", 780 | "5m", 781 | "15m", 782 | "30m", 783 | "1h", 784 | "2h", 785 | "1d" 786 | ], 787 | "time_options": [ 788 | "5m", 789 | "15m", 790 | "1h", 791 | "6h", 792 | "12h", 793 | "24h", 794 | "2d", 795 | "7d", 796 | "30d" 797 | ] 798 | }, 799 | "timezone": "", 800 | "title": "Zabbix Alertmanager", 801 | "uid": "maYkrFemz", 802 | "version": 17 803 | } --------------------------------------------------------------------------------