No Ășltimo Zabbix Summit 2019, juntamente com o lançamento do Zabbix 4.4, o novo Zabbix Agent 2 foi anunciado, cuja principal caracterĂstica Ă© a capacidade de escrever plug-ins para ele no idioma Go. E muitos imediatamente começaram a perguntar: mas como, de fato, esses plugins escrevem, como eles sĂŁo organizados? Onde obter documentação e exemplos?
Neste artigo, quero dar respostas a essas e algumas outras perguntas. Tudo em ordem, mas se vocĂȘ Ă© um daqueles que imediatamente entra em batalha, sinta-se Ă  vontade para pular a parte introdutĂłria e continuar a praticar âââ ââ 
Assim...

Que tipo de novo agente e por que ele apareceu?
Se vocĂȘ tentou escrever plug-ins para o primeiro Zabbix Agent ou pelo menos pretendia fazĂȘ-lo, provavelmente notou que suas opçÔes sĂŁo muito limitadas.
O plug-in do primeiro agente pode ser executado em vĂĄrios processos diferentes, nĂŁo dando ao criador controle suficiente sobre ele para implementar, por exemplo, usando conexĂ”es persistentes, mantendo o estado entre verificaçÔes, recebendo traps - era difĂcil ou impossĂvel fazer essas coisas.
. Go ( Zabbix Agent), . Go- â â . , Go- Zabbix Agent , .
, Go-:
- onfig ;
 - "out-of-the-box";
 - ;
 - Windows ;
 - .
Zabbix 4.4 , Zabbix 5.0 "production-ready". 
, , " ". , Zabbix 4.4. , Go- , , Zabbix 5.0 - .
â ServerConnector, ServerListener Scheduler.
ServerConnector ( / ), items . .
ServerListener Scheduler. , .
Scheduler . Scheduler () , item'.
: ( Bulk Passive, ). , , .
. â PlantUML ÂŻ\(ă)/ÂŻ

: ServerConnector ResultCache, .

Scheduler , ServerConnector' ServerListener item'. , ResultWriter, .
items Zabbix , ServerConnector updateRequest , . , . , , .
:
:
- configuratorTask
 - starterTask
 - collectorTask
 - watcherTask
 - exporterTask (directExporterTask)
 - stopperTask
(taskBase) , , . 
exporterTask
ExporterTask ( bulk ). item, . Scheduler Export Exporter ResultWriter.
directExporterTask
directExporterTask ExporterTask , , ( ), , 1 . . â directExporterTask .
watcherTask
WatcherTask () . Watch Watcher, .
collectorTask
Scheduler Collect Collector Period() .
starterTask
Start Runner, .
stopperTask
Stop Runner, .
configuratorTask
Configure Configurator, , .
5 : Exporter, Watcher, Collector, Runner Configurator.
Exporter Watcher : Exporter pull , Watcher â push.
plugin.Exporter
type Exporter interface {
    Export(key string, params []string, context ContextProvider) (result interface{}, err error)
}
Exporter â , , , . , . . , , . , , - .
, . . Go: , , , sync.Map . race-, .
Export() â 100 . , plugin.Base.SetCapacity.
func (b *Base) SetCapacity(capacity int)
, capacity . :
Plugins.<PluginName>.Capacity=1
plugin.Watcher
type Watcher interface {
    Watch(requests []*Request, context ContextProvider)
}
Watcher , . , trapping, . use case â , , , . , , , , .
plugin.Collector
type Collector interface {
    Collect() error
    Period() int
}
Collector , . , Exporterâ.
use case Collector â , , Zabbix .
Collectorâ 2 :
- Collect ;
 - Period .
Collector, , . 
plugin.Runner
type Runner interface {
    Start()
    Stop()
}
Runner , ( Start), , ( Stop).
, , , - , , ..
() . , (Zabbix Server Proxy), . , , . , . , , , 24 .
plugin.Configurator
type Configurator interface {
    Configure(globalOptions *GlobalOptions, privateOptions interface{})
    Validate(privateOptions interface{}) error
}
Configurator , .
2 :
- Configure .
 - Validate config . , , .
 
, , . .
Go- Zabbix .
. , â , , . internal/agent "plugin_" . , , UserParameters.
, , ( , , , ..) â . . go/plugins, .
Hello, world!
â Go, , . :
package packageName
import "zabbix.com/pkg/plugin"
type Plugin struct {
    plugin.Base
}
var impl Plugin
func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (res interface{}, err error) {
    
    return
}
func init() {
    plugin.RegisterMetrics(&impl, "PluginName", "key", "Description.")
}
, bash python, ? â^Ï^â  , - .
, , .
:
Zabbix.
$ git clone https://git.zabbix.com/scm/zbx/zabbix.git --depth 1 zabbix-agent2
$ cd zabbix-agent2
$ git checkout -b feature/myplugin release/4.4
src/go/plugins/weather weather.go , .
, "zabbix.com/pkg/plugin".
package weather
import  "zabbix.com/pkg/plugin"
, Base plugin. .
type Plugin struct {
    plugin.Base
}
var impl Plugin
. , , :
- GET API (, wttr.in)
 - .
 
Exporter.
func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) {
    if len(params) != 1 {
        return nil, errors.New("Wrong parameters.")
    }
    
    res, err := p.httpClient.Get(fmt.Sprintf("https://wttr.in/~%s?format=%%t", params[0]))
    if err != nil {
        return nil, err
    }
    temp, err := ioutil.ReadAll(res.Body)
    _ = res.Body.Close()
    if err != nil {
        return nil, err
    }
    return string(temp)[0 : len(temp)-4], nil
}
â , .
plugin.RegisterMetrics.
func RegisterMetrics(impl Accessor, name string, params ...string)
init ( ).
func init() {
  plugin.RegisterMetrics(&impl, "Weather", "weather.temp", "Returns Celsius temperature.")
}
, .
package plugins 
import (
  _ "zabbix.com/plugins/kernel"
  _ "zabbix.com/plugins/log"
  _ "zabbix.com/plugins/weather"
)
, 3 : linux, darwin windows. , , , .
: , src/go/plugins/plugins_<platform>.go.
Go, .
Go 1.13.
, --enable-agent2 make.
$ cd <zabbix-source>
$ ./bootstrap.sh; ./configure --enable-agent2 --enable-static; make
, . âmoscowâ .
$ <zabbix-source>/src/go/bin/zabbix_agent2 -t weather.temp[moscow]
+1
. , go run, .
$ go run <zabbix-source>/src/go/cmd/zabbix_agent2/zabbix_agent2.go
, zabbix.com/pkg/log: Tracef, Debugf, Warningf, Infof, Errf, Critf. Plugin, log. , [<PluginName>] .
Go- . Plugins. , -, , , . : Plugins.<PluginName>.<Parameter>=<Value>. :
- , ;
 - ;
 - ;
 - ;
 - .
 
, Configurator. Timeout, HTTP .
, , Timeout 1 30 , ( ) .
, .
type PluginOptions struct {
    
    Timeout int `conf:"optional,range=1:30"`
}
, conf. .
: [name=<name>,][optional,][range=<range>,][default=<default value>], :
- <name> â ( );
 - optional â , ;
 - <range> â <min>:<max>, <min> <max> ;
 - <default value> â . .
 
Plugin , â http.Client, .
type Plugin struct {
    plugin.Base
    options PluginOptions
    httpClient http.Client
}
Configurator. , 2 : Configure Validate.
func (p *Plugin) Configure(global *plugin.GlobalOptions, privateOptions interface{}) {
    if err := conf.Unmarshal(privateOptions, &p.options); err != nil {
        p.Errf("cannot unmarshal configuration options: %s", err)
    }
    
    if p.options.Timeout == 0 {
        p.options.Timeout = global.Timeout
    }
    p.httpClient = http.Client{Timeout: time.Duration(p.options.Timeout) * time.Second}
}
func (p *Plugin) Validate(privateOptions interface{}) error {
    
    return nil
}
conf.Unmarshal .
http.Get p.httpClient.Get.
res, err := p.httpClient.Get(fmt.Sprintf("https://wttr.in/~%s?format=%%t", params[0]))
if err != nil {
    if err.(*url.Error).Timeout() {
        return nil, errors.New("Request timeout.")
    }
    return nil, err
}
, :
Plugins.Weather.Timeout=1
, .
, - ? â . Timeout default, .. .
( , Validate Configure).
, . , , , , . Validate. , .
func (p *Plugin) Validate(privateOptions interface{}) error {
    var opts PluginOptions
    return conf.Unmarshal(privateOptions, &opts)
}
, , : "cannot create scheduling manager: invalid plugin Weather configuration: Cannot assign configuration: invalid parameter Plugins.Weather.Timeout at line 411: value out of range".
" ". runtime Validate Configure, . , - Configure â , . , Start Stop ( Runner).
: https://github.com/VadimIpatov/zabbix-weather-plugin.
Exporter . . , Collector Runner.
! - . , HTTP .
. "net/http/httptrace" ( Go 1.7).
type timeSample struct {
    DnsLookup         float64 `json:"dnsLookup"`
    Connect           float64 `json:"connect"`
    TlsHandshake      float64 `json:"tlsHandshake"`
    FirstResponseByte float64 `json:"firstResponseByte"`
    Rtt               float64 `json:"rtt"`
}
func (p *Plugin) measureTime(url string) (timeSample, error) {
    var (
        sample                            timeSample
        start, connect, dns, tlsHandshake time.Time
    )
    req, _ := http.NewRequest("GET", url, nil)
    trace := &httptrace.ClientTrace{
        DNSStart: func(_ httptrace.DNSStartInfo) {
            dns = time.Now()
        },
        DNSDone: func(_ httptrace.DNSDoneInfo) {
            sample.DnsLookup = float64(time.Since(dns) / time.Millisecond)
        },
        ConnectStart: func(_, _ string) {
            connect = time.Now()
        },
        ConnectDone: func(net, addr string, err error) {
            if err != nil {
                p.Errf("unable to connect to host %s: %s", addr, err.Error())
            }
            sample.Connect = float64(time.Since(connect) / time.Millisecond)
        },
        GotFirstResponseByte: func() {
            sample.FirstResponseByte = float64(time.Since(start) / time.Millisecond)
        },
        TLSHandshakeStart: func() {
            tlsHandshake = time.Now()
        },
        TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
            sample.TlsHandshake = float64(time.Since(tlsHandshake) / time.Millisecond)
        },
    }
    ctx, cancel := context.WithTimeout(req.Context(), time.Duration(p.options.Timeout)*time.Second)
    defer cancel()
    req = req.WithContext(httptrace.WithClientTrace(ctx, trace))
    start = time.Now()
    if _, err := http.DefaultTransport.RoundTrip(req); err != nil {
        return timeSample{}, err
    }
    sample.Rtt = float64(time.Since(start) / time.Millisecond)
    return sample, nil
}
- . (Ring Buffer). , â github.com/VadimIpatov/gcircularqueue. , . Open Source Go â github.com/montanaflynn/stats. .
type Plugin struct {
    plugin.Base
    urls map[string]*urlUnit
    sync.Mutex
    options Options
}
type urlUnit struct {
    url      string
    history  *gcircularqueue.CircularQueue
    accessed time.Time 
    modified time.Time 
}
Start Stop Runner.
func (p *Plugin) Start() {
    p.urls = make(map[string]*urlUnit)
}
func (p *Plugin) Stop() {
    p.urls = nil
}
Collector.
func (p *Plugin) Collect() (err error) {
    now := time.Now()
    p.Lock()
    for key, url := range p.urls {
        if now.Sub(url.accessed) > maxInactivityPeriod {
            p.Debugf("removed expired url %s", url.url)
            delete(p.urls, key)
            continue
        }
        res, err := p.measureTime(url.url)
        if err != nil {
            p.Errf(err.Error())
            continue
        }
        url.history.Push(res)
        if url.history.IsFull() {
            _ = url.history.Shift()
        }
        url.modified = now
    }
    p.Unlock()
    return
}
func (p *Plugin) Period() int {
    return p.options.Interval
}
URL ( ), p.measureTime(url.url) . , url.modified.
URL , .
, Collector . Exporter.
func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) {
    if len(params) != 1 {
        return nil, errors.New("Wrong parameters.")
    }
    url, err := parseURL(params[0])
    if err != nil {
        return nil, err
    }
    switch key {
    case keyHttpTraceStats:
        if _, ok := p.urls[url]; !ok {
            p.urls[url] = &urlUnit{
                url:     url,
                history: gcircularqueue.NewCircularQueue(maxHistory),
            }
        }
        p.Lock()
        defer p.Unlock()
        p.urls[url].accessed = time.Now()
        if p.urls[url].history.Len() < minStatRange {
            
            return
        }
        data := prepareData(p.urls[url].history.Elements())
        jsonRes, err := json.Marshal(stat{
            
            
            
            P99: timeSample{
                DnsLookup:         percentile(data[metricDnsLookup], p99),
                Connect:           percentile(data[metricConnect], p99),
                TlsHandshake:      percentile(data[metricTlsHandshake], p99),
                FirstResponseByte: percentile(data[metricFirstResponseByte], p99),
                Rtt:               percentile(data[metricRtt], p99),
            },
        })
        if err != nil {
            p.Errf(err.Error())
            return nil, errors.New("Cannot marshal JSON.")
        }
        value := string(jsonRes)
        return plugin.Result{
            Value: &value,
            Ts:    p.urls[url].modified,
        }, nil
    default:
        return nil, plugin.UnsupportedMetricError
    }
}
, Collect Export, .. .
( ):
$ zabbix_get -s zabbix.local -k "httptrace.stats[yoursite.com]"
{
    "median": {
        "dnsLookup": 13,
        "connect": 28,
        "tlsHandshake": 56,
        "firstResponseByte": 126.5,
        "rtt": 126.5
    },
    "p75": {
        "dnsLookup": 20,
        "connect": 31,
        "tlsHandshake": 60,
        "firstResponseByte": 138.5,
        "rtt": 138.5
    },
    "p95": {
        "dnsLookup": 22.5,
        "connect": 35,
        "tlsHandshake": 78.5,
        "firstResponseByte": 159.5,
        "rtt": 159.5
    },
    "p99": {
        "dnsLookup": 50,
        "connect": 51.5,
        "tlsHandshake": 125.5,
        "firstResponseByte": 266.5,
        "rtt": 266.5
    }
}
: https://github.com/VadimIpatov/zabbix-httptrace-plugin.
runtime metrics, . .
:
$ zabbix_agent2 -R metrics
...
[Weather]
active: true
capacity: 0/100
tasks: 0
weather.temp: Returns Celsius temperature.
...
â HTTP. StatusPort=, http://<ZabbixAgentHost>:<Port>/status.

?
. , :
- ( ).
 - , .. .
 - Zabbix. , Dockerâ Mysql.
 
!
, , :
Templates guidelines â .
An official guide to making and managing great templates â Zabbix Summit .
Magic of the new zabbix agent â Zabbix Agent 2.
Zabbix Agent 2.
Zabbix Agent 2 ( : git.zabbix.com). , .
Weather.
HttpTrace.
Writing watcher Zabbix Agent2 MQTT plugin in Go â Watcher .
Go, " Go" , , A Tour of Go ÊâŃ âÊ
Zabbix Agent 2 â Zabbix . Go, , , C Loadable modules.
Stay tuned!
P.S. .