Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs.mikrotik): Add plugin #16081

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions plugins/inputs/all/mikrotik.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !custom || inputs || inputs.mikrotik

package all

import _ "github.com/influxdata/telegraf/plugins/inputs/mikrotik" // register plugin
90 changes: 90 additions & 0 deletions plugins/inputs/mikrotik/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Mikrotik Input Plugin

This plugin gathers metrics from [Mikrotik's RouterOS][mikrotik] such as
interface statistics, uptime etc

[mikrotik]: https://mikrotik.com/software

## Global configuration options <!-- @/docs/includes/plugin_config.md -->

In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.

[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins

## Configuration

```toml @sample.conf
[[inputs.mikrotik]]
## Mikrotik's address to query. Make sure that REST API is enabled: https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API
address = "https://192.168.88.1"

## User to use. Read access rights will be enough
username = "admin"
password = "password"

## Mikrotik's entities whose comments contain this strings will be ignored
# ignore_comments = [
# "block",
# "doNotGatherMetricsFromThis"
# ]

## Modules available to use (default: system_resourses)
# include_modules = [
# "interface",
# "interface_wireguard_peers",
# "interface_wireless_registration",
# "ip_dhcp_server_lease",
# "ip_firewall_connection",
# "ip_firewall_filter",
# "ip_firewall_nat",
# "ip_firewall_mangle",
# "ipv6_firewall_connection",
# "ipv6_firewall_filter",
# "ipv6_firewall_nat",
# "ipv6_firewall_mangle",
# "system_script",
# "system_resourses"
# ]

## Optional TLS Config
# tls_ca = "/etc/telegraf/ca.pem"
# tls_cert = "/etc/telegraf/cert.pem"
# tls_key = "/etc/telegraf/key.pem"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false

## HTTP response timeout
# response_timeout = "5s"
```

## Metrics

For each specific module, a unique set of metrics and tags will be provided
based on the JSON structure returned by the REST endpoint. You can refer to
the `tagFields` and `valueFields` lists in `types.go` for a full set of
tags and values.

When querying Mikrotik, all available fields across different metrics
will be requested. However, Mikrotik’s design only returns fields that are
present in the current module’s response, ignoring any fields that don’t
apply to the specific endpoint. Disabled entities in Mikrotik are
automatically excluded from the response.

## Example Output

```text
mikrotik,.id=*1,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,default-name=ether1,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:B6,model=RBD52G-5HacD2HnD,name=ether1,platform=MikroTik,running=true,serial-number=SERIALNUMBER,source-module=interface,type=ether,version=7.16\ (stable) fp-rx-byte=23815497595i,fp-rx-packet=18015083i,fp-tx-byte=0i,fp-tx-packet=0i,link-downs=1i,rx-byte=23887557927i,rx-drop=0i,rx-error=0i,rx-packet=18015083i,tx-byte=1129765037i,tx-drop=0i,tx-error=0i,tx-packet=5384706i,tx-queue-drop=0i 1730320979000000000
mikrotik,.id=*2,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,default-name=ether2,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:B7,model=RBD52G-5HacD2HnD,name=ether2,platform=MikroTik,running=false,serial-number=SERIALNUMBER,slave=true,source-module=interface,type=ether,version=7.16\ (stable) fp-rx-byte=0i,fp-rx-packet=0i,fp-tx-byte=0i,fp-tx-packet=0i,link-downs=0i,rx-byte=0i,rx-drop=0i,rx-error=0i,rx-packet=0i,tx-byte=0i,tx-drop=0i,tx-error=0i,tx-packet=0i,tx-queue-drop=0i 1730320979000000000
mikrotik,.id=*3,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,default-name=ether3,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:B8,model=RBD52G-5HacD2HnD,name=ether3,platform=MikroTik,running=true,serial-number=SERIALNUMBER,slave=true,source-module=interface,type=ether,version=7.16\ (stable) fp-rx-byte=91438550i,fp-rx-packet=1244518i,fp-tx-byte=4403947442i,fp-tx-packet=2914053i,link-downs=1i,rx-byte=96416622i,rx-drop=0i,rx-error=0i,rx-packet=1244518i,tx-byte=4422341564i,tx-drop=0i,tx-error=0i,tx-packet=2929010i,tx-queue-drop=0i 1730320979000000000
mikrotik,.id=*4,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,default-name=ether4,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:B9,model=RBD52G-5HacD2HnD,name=ether4,platform=MikroTik,running=false,serial-number=SERIALNUMBER,slave=true,source-module=interface,type=ether,version=7.16\ (stable) fp-rx-byte=0i,fp-rx-packet=0i,fp-tx-byte=0i,fp-tx-packet=0i,link-downs=0i,rx-byte=0i,rx-drop=0i,rx-error=0i,rx-packet=0i,tx-byte=0i,tx-drop=0i,tx-error=0i,tx-packet=0i,tx-queue-drop=0i 1730320979000000000
mikrotik,.id=*5,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,default-name=ether5,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:BA,model=RBD52G-5HacD2HnD,name=ether5,platform=MikroTik,running=false,serial-number=SERIALNUMBER,slave=true,source-module=interface,type=ether,version=7.16\ (stable) fp-rx-byte=0i,fp-rx-packet=0i,fp-tx-byte=0i,fp-tx-packet=0i,link-downs=0i,rx-byte=0i,rx-drop=0i,rx-error=0i,rx-packet=0i,tx-byte=0i,tx-drop=0i,tx-error=0i,tx-packet=0i,tx-queue-drop=0i 1730320979000000000
mikrotik,.id=*6,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,default-name=wlan1,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:BB,model=RBD52G-5HacD2HnD,name=wlan1,platform=MikroTik,running=false,serial-number=SERIALNUMBER,slave=true,source-module=interface,type=wlan,version=7.16\ (stable) fp-rx-byte=1984519i,fp-rx-packet=8740i,fp-tx-byte=0i,fp-tx-packet=0i,link-downs=12i,rx-byte=1984519i,rx-drop=0i,rx-error=0i,rx-packet=8740i,tx-byte=17087451i,tx-drop=0i,tx-error=0i,tx-packet=47921i,tx-queue-drop=1i 1730320979000000000
mikrotik,.id=*7,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,default-name=wlan2,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:BC,model=RBD52G-5HacD2HnD,name=wlan2,platform=MikroTik,running=true,serial-number=SERIALNUMBER,slave=true,source-module=interface,type=wlan,version=7.16\ (stable) fp-rx-byte=5525090211i,fp-rx-packet=8360162i,fp-tx-byte=87942233i,fp-tx-packet=1222212i,link-downs=0i,rx-byte=5525090211i,rx-drop=0i,rx-error=0i,rx-packet=8360162i,tx-byte=23832544176i,tx-drop=0i,tx-error=0i,tx-packet=19198103i,tx-queue-drop=52532i 1730320979000000000
mikrotik,.id=*8,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=11:22:33:44:55:BB,model=RBD52G-5HacD2HnD,name=lan,platform=MikroTik,running=true,serial-number=SERIALNUMBER,source-module=interface,type=bridge,version=7.16\ (stable) fp-rx-byte=1107864899i,fp-rx-packet=5393891i,fp-tx-byte=0i,fp-tx-packet=0i,link-downs=0i,rx-byte=1124747646i,rx-drop=0i,rx-error=0i,rx-packet=5463554i,tx-byte=23815054209i,tx-drop=0i,tx-error=0i,tx-packet=18013585i,tx-queue-drop=0i 1730320979000000000
mikrotik,.id=*14,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,disabled=false,firmware-type=ipq4000L,host=localhost,mac-address=00:00:00:00:00:00,model=RBD52G-5HacD2HnD,name=lo,platform=MikroTik,running=true,serial-number=SERIALNUMBER,source-module=interface,type=loopback,version=7.16\ (stable) fp-rx-byte=0i,fp-rx-packet=0i,fp-tx-byte=0i,fp-tx-packet=0i,link-downs=0i,rx-byte=491147i,rx-drop=0i,rx-error=0i,rx-packet=2839i,tx-byte=491147i,tx-drop=0i,tx-error=0i,tx-packet=2839i,tx-queue-drop=0i 1730320979000000000
mikrotik,architecture-name=arm,board-name=hAP\ ac^2,cpu=ARM,current-firmware=7.15.3,firmware-type=ipq4000L,host=localhost,model=RBD52G-5HacD2HnD,platform=MikroTik,serial-number=SERIALNUMBER,source-module=system_resourses,version=7.16\ (stable) cpu-frequency=672i,cpu-load=0i,free-hdd-space=1482752i,free-memory=55685120i,total-memory=134217728i,uptime=85201i,write-sect-since-reboot=2344i,write-sect-total=30313i 1730320979000000000

```
224 changes: 224 additions & 0 deletions plugins/inputs/mikrotik/mikrotik.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//go:generate ../../../tools/readme_config_includer/generator
package mikrotik

import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"

"github.com/influxdata/telegraf/plugins/inputs"

common_tls "github.com/influxdata/telegraf/plugins/common/tls"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
)

//go:embed sample.conf
var sampleConfig string

type Mikrotik struct {
Address string `toml:"address"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it an idea to support an array of URLs?

IgnoreCert bool `toml:"ignore_cert,omitempty"`
ResponseTimeout config.Duration `toml:"response_timeout"`
IgnoreComments []string `toml:"ignore_comments"`
IncludeModules []string `toml:"include_modules"`
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
Log telegraf.Logger `toml:"-"`
common_tls.ClientConfig
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to use client config from plugins/common/http?

Suggested change
common_tls.ClientConfig
http.HTTPClientConfig


tags map[string]string

url []mikrotikEndpoint

client *http.Client

systemTagsURL []string
}

func (*Mikrotik) SampleConfig() string {
return sampleConfig
}
s-r-engineer marked this conversation as resolved.
Show resolved Hide resolved

func (h *Mikrotik) Start() error {
return h.getSystemTags()
}

func (h *Mikrotik) Init() error {
if h.Username.Empty() {
return errors.New("mikrotik init -> username must be present")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return errors.New("mikrotik init -> username must be present")
return errors.New("username is required")

}

if len(h.IncludeModules) == 0 {
h.IncludeModules = append(h.IncludeModules, "system_resourses")
}

mainPropList, systemResourcesPropList, systemRouterBoardPropList := createPropLists()

h.systemTagsURL = []string{
h.Address + "/rest/system/resource?" + systemResourcesPropList,
h.Address + "/rest/system/routerboard?" + systemRouterBoardPropList,
}

for _, selectedModule := range h.IncludeModules {
if _, ok := modules[selectedModule]; !ok {
return fmt.Errorf("mikrotik init -> module %s does not exist or has a typo. Correct modules are: %s", selectedModule, getModuleNames())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("mikrotik init -> module %s does not exist or has a typo. Correct modules are: %s", selectedModule, getModuleNames())
return fmt.Errorf("module %s does not exist or has a typo. Correct modules are: %s", selectedModule, getModuleNames())

These error prefixes are not needed, just make sure the rest of the message is specific enough.

}
h.url = append(h.url, mikrotikEndpoint{name: selectedModule, url: fmt.Sprintf("%s%s?%s", h.Address, modules[selectedModule], mainPropList)})
}

ignoreCommentsFunction = basicCommentAndDisableFilter(h.IgnoreComments)

return h.getClient()
}

func (h *Mikrotik) Gather(acc telegraf.Accumulator) error {
var wg sync.WaitGroup
s-r-engineer marked this conversation as resolved.
Show resolved Hide resolved
for _, u := range h.url {
wg.Add(1)
go func(url mikrotikEndpoint) {
defer wg.Done()

if err := h.gatherURL(url, acc); err != nil {
acc.AddError(fmt.Errorf("gather -> %w", err))
}
}(u)
}
wg.Wait()

return nil
}

func (h *Mikrotik) getClient() (err error) {
tlsCfg, err := h.ClientConfig.TLSConfig()
if err != nil {
return fmt.Errorf("getClient -> %w", err)
}

h.client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
},
Timeout: time.Duration(h.ResponseTimeout),
}

return nil
}

func (h *Mikrotik) getSystemTags() error {
h.tags = make(map[string]string)
for _, tagURL := range h.systemTagsURL {
request, err := http.NewRequest("GET", tagURL, nil)
if err != nil {
return fmt.Errorf("getSystemTags -> %w", err)
}

err = h.setRequestAuth(request)
if err != nil {
return fmt.Errorf("getSystemTags -> %w", err)
}

binaryData, err := h.queryData(request)
if err != nil {
return fmt.Errorf("getSystemTags -> %w", err)
}

err = json.Unmarshal(binaryData, &h.tags)
if err != nil {
return fmt.Errorf("getSystemTags -> %w", err)
}
}
return nil
}

func (h *Mikrotik) gatherURL(endpoint mikrotikEndpoint, acc telegraf.Accumulator) error {
request, err := http.NewRequest("GET", endpoint.url, nil)
if err != nil {
return fmt.Errorf("gatherURL -> %w", err)
}

err = h.setRequestAuth(request)
if err != nil {
return fmt.Errorf("gatherURL -> %w", err)
}

binaryData, err := h.queryData(request)
if err != nil {
return fmt.Errorf("gatherURL -> %w", err)
}

timestamp := time.Now()

result, err := binToCommon(binaryData)
if err != nil {
return fmt.Errorf("gatherURL -> %w", err)
}

parsedData, err := parse(result)
if err != nil {
return fmt.Errorf("gatherURL -> %w", err)
}

for _, point := range parsedData {
point.Tags["source-module"] = endpoint.name
for n, v := range h.tags {
point.Tags[n] = v
}
acc.AddFields("mikrotik", point.Fields, point.Tags, timestamp)
}
Comment on lines +163 to +174
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this parse method could already return a telegraf metric instead of something that almost looks like a metric (only Tags and Fields).

Suggested change
parsedData, err := parse(result)
if err != nil {
return fmt.Errorf("gatherURL -> %w", err)
}
for _, point := range parsedData {
point.Tags["source-module"] = endpoint.name
for n, v := range h.tags {
point.Tags[n] = v
}
acc.AddFields("mikrotik", point.Fields, point.Tags, timestamp)
}
metrics, err := parse(result, endpoint.name)
if err != nil {
return fmt.Errorf("could not parse result: %w", err)
}
for _, metric := range metrics {
acc.AddMetric(metric)
}

It also looks like the name for the metric should be endpoint.name. Currently, You are only putting that into a tag, meaning there will be a lot of metrics with the same name but with totally different tags- and fields-set.


return nil
}

func (h *Mikrotik) setRequestAuth(request *http.Request) error {
username, err := h.Username.Get()
if err != nil {
return fmt.Errorf("setRequestAuth: username -> %w", err)
}
defer username.Destroy()

password, err := h.Password.Get()
if err != nil {
return fmt.Errorf("setRequestAuth: pasword -> %w", err)
}
defer password.Destroy()

request.SetBasicAuth(username.String(), password.String())

return nil
}

func (h *Mikrotik) queryData(request *http.Request) (data []byte, err error) {
resp, err := h.client.Do(request)
if err != nil {
return data, fmt.Errorf("queryData -> %w", err)
}

defer resp.Body.Close()
defer h.client.CloseIdleConnections()

if resp.StatusCode != http.StatusOK {
return data, fmt.Errorf("queryData -> received status code %d (%s), expected 200",
resp.StatusCode,
http.StatusText(resp.StatusCode))
}

data, err = io.ReadAll(resp.Body)
if err != nil {
return data, fmt.Errorf("queryData -> %w", err)
}

return data, err
}

func init() {
inputs.Add("mikrotik", func() telegraf.Input {
return &Mikrotik{ResponseTimeout: config.Duration(time.Second * 5)}
})
}
Loading
Loading