Initial commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
birdc.*
|
||||||
|
/bird-exporter
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/.gocache/
|
||||||
|
debian/go/
|
||||||
|
debian/bird-exporter/
|
||||||
|
debian/files
|
||||||
|
debian/*.substvars
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/*.debhelper
|
||||||
202
LICENSE
Normal file
202
LICENSE
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
30
Makefile
Normal file
30
Makefile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.PHONY: all build test clean install sync-version pkg-deb
|
||||||
|
|
||||||
|
BINARY_NAME=bird-exporter
|
||||||
|
INSTALL_PATH=/usr/local/bin
|
||||||
|
|
||||||
|
all: test build
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o $(BINARY_NAME) ./cmd/bird-exporter/
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -v ./cmd/bird-exporter/
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY_NAME)
|
||||||
|
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||||
|
rm -rf debian/.debhelper debian/.gocache debian/go debian/$(BINARY_NAME) debian/files debian/*.substvars debian/debhelper-build-stamp
|
||||||
|
rm -f ../$(BINARY_NAME)_*.deb ../$(BINARY_NAME)_*.changes ../$(BINARY_NAME)_*.buildinfo
|
||||||
|
|
||||||
|
install: build
|
||||||
|
install -m 755 $(BINARY_NAME) $(INSTALL_PATH)/$(BINARY_NAME)
|
||||||
|
|
||||||
|
sync-version:
|
||||||
|
@echo "Syncing version from debian/changelog to main.go..."
|
||||||
|
@VERSION=$$(head -1 debian/changelog | sed -n 's/.*(\([^)]*\)).*/\1/p'); \
|
||||||
|
sed -i 's/^const Version = ".*"/const Version = "'"$$VERSION"'"/' cmd/bird-exporter/main.go; \
|
||||||
|
echo "Updated Version const to: $$VERSION"
|
||||||
|
|
||||||
|
pkg-deb: sync-version
|
||||||
|
fakeroot dpkg-buildpackage -us -uc -b
|
||||||
47
README.md
Normal file
47
README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# BIRD Exporter
|
||||||
|
|
||||||
|
Prometheus exporter for BIRD routing daemon.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Test
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Create Debian package
|
||||||
|
make pkg-deb
|
||||||
|
|
||||||
|
# Install package
|
||||||
|
sudo dpkg -i ../bird-exporter_*.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The Debian package installs a systemd service that reads configuration from `/etc/default/bird-exporter`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit service configuration
|
||||||
|
sudo nano /etc/default/bird-exporter
|
||||||
|
|
||||||
|
# Start service
|
||||||
|
sudo systemctl start bird-exporter
|
||||||
|
```
|
||||||
|
|
||||||
|
Default configuration:
|
||||||
|
```
|
||||||
|
BIRD_RUN_USER=bird
|
||||||
|
BIRD_RUN_GROUP=bird
|
||||||
|
BIRD_EXPORTER_ARGS="-period=60s -bird.socket=/var/run/bird/bird.ctl"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **Manual page**: `man bird-exporter` (after package installation)
|
||||||
|
- **Detailed documentation**: [doc/DETAILS.md](doc/DETAILS.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See [LICENSE](LICENSE).
|
||||||
816
cmd/bird-exporter/main.go
Normal file
816
cmd/bird-exporter/main.go
Normal file
@@ -0,0 +1,816 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Version = "0.1.0"
|
||||||
|
|
||||||
|
var (
|
||||||
|
listenAddr = flag.String("web.listen-address", ":9324", "Address to listen on for web interface and telemetry.")
|
||||||
|
birdSocket = flag.String("bird.socket", "/var/run/bird/bird.ctl", "Path to BIRD control socket.")
|
||||||
|
scrapePeriod = flag.Duration("period", 60*time.Second, "Period between scrapes of BIRD data.")
|
||||||
|
debug = flag.Bool("debug", false, "Enable debug logging.")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
protocolsMutex sync.RWMutex
|
||||||
|
cachedProtocols []Protocol
|
||||||
|
)
|
||||||
|
|
||||||
|
type Protocol struct {
|
||||||
|
Name string
|
||||||
|
Proto string
|
||||||
|
Table string
|
||||||
|
State string
|
||||||
|
Since string
|
||||||
|
Info string
|
||||||
|
BGPInfo *BGPInfo
|
||||||
|
Channels []Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
type BGPInfo struct {
|
||||||
|
BGPState string
|
||||||
|
NeighborAddr string
|
||||||
|
NeighborAS string
|
||||||
|
LocalAS string
|
||||||
|
NeighborID string
|
||||||
|
HoldTimer float64
|
||||||
|
KeepaliveTimer float64
|
||||||
|
SendHoldTimer float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Channel struct {
|
||||||
|
Name string
|
||||||
|
State string
|
||||||
|
Table string
|
||||||
|
Preference int
|
||||||
|
InputFilter string
|
||||||
|
OutputFilter string
|
||||||
|
Routes RouteStats
|
||||||
|
ImportStats ChangeStats
|
||||||
|
ExportStats ChangeStats
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouteStats struct {
|
||||||
|
Imported int
|
||||||
|
Filtered int
|
||||||
|
Exported int
|
||||||
|
Preferred int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeStats struct {
|
||||||
|
Updates int
|
||||||
|
Withdraws int
|
||||||
|
Rejected int
|
||||||
|
Filtered int
|
||||||
|
Ignored int
|
||||||
|
Accepted int
|
||||||
|
}
|
||||||
|
|
||||||
|
type BirdCollector struct {
|
||||||
|
protocolUp *prometheus.Desc
|
||||||
|
bgpState *prometheus.Desc
|
||||||
|
routeImported *prometheus.Desc
|
||||||
|
routeFiltered *prometheus.Desc
|
||||||
|
routeExported *prometheus.Desc
|
||||||
|
routePreferred *prometheus.Desc
|
||||||
|
importUpdates *prometheus.Desc
|
||||||
|
importWithdraws *prometheus.Desc
|
||||||
|
importRejected *prometheus.Desc
|
||||||
|
importFiltered *prometheus.Desc
|
||||||
|
importIgnored *prometheus.Desc
|
||||||
|
importAccepted *prometheus.Desc
|
||||||
|
exportUpdates *prometheus.Desc
|
||||||
|
exportWithdraws *prometheus.Desc
|
||||||
|
exportRejected *prometheus.Desc
|
||||||
|
exportFiltered *prometheus.Desc
|
||||||
|
exportAccepted *prometheus.Desc
|
||||||
|
bgpHoldTimer *prometheus.Desc
|
||||||
|
bgpKeepaliveTimer *prometheus.Desc
|
||||||
|
bgpSendHoldTimer *prometheus.Desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBirdCollector() *BirdCollector {
|
||||||
|
return &BirdCollector{
|
||||||
|
protocolUp: prometheus.NewDesc(
|
||||||
|
"bird_protocol_up",
|
||||||
|
"Protocol status (1 = up, 0 = down)",
|
||||||
|
[]string{"name", "proto", "table", "info"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
bgpState: prometheus.NewDesc(
|
||||||
|
"bird_bgp_state",
|
||||||
|
"BGP session state (1 = Established, 0 = other)",
|
||||||
|
[]string{"name", "neighbor_address", "neighbor_as", "local_as", "neighbor_id"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
routeImported: prometheus.NewDesc(
|
||||||
|
"bird_route_imported",
|
||||||
|
"Number of imported routes",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
routeFiltered: prometheus.NewDesc(
|
||||||
|
"bird_route_filtered",
|
||||||
|
"Number of filtered routes",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
routeExported: prometheus.NewDesc(
|
||||||
|
"bird_route_exported",
|
||||||
|
"Number of exported routes",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
routePreferred: prometheus.NewDesc(
|
||||||
|
"bird_route_preferred",
|
||||||
|
"Number of preferred routes",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
importUpdates: prometheus.NewDesc(
|
||||||
|
"bird_route_import_updates_total",
|
||||||
|
"Total number of import updates",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
importWithdraws: prometheus.NewDesc(
|
||||||
|
"bird_route_import_withdraws_total",
|
||||||
|
"Total number of import withdraws",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
importRejected: prometheus.NewDesc(
|
||||||
|
"bird_route_import_rejected_total",
|
||||||
|
"Total number of rejected imports",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
importFiltered: prometheus.NewDesc(
|
||||||
|
"bird_route_import_filtered_total",
|
||||||
|
"Total number of filtered imports",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
importIgnored: prometheus.NewDesc(
|
||||||
|
"bird_route_import_ignored_total",
|
||||||
|
"Total number of ignored imports",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
importAccepted: prometheus.NewDesc(
|
||||||
|
"bird_route_import_accepted_total",
|
||||||
|
"Total number of accepted imports",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
exportUpdates: prometheus.NewDesc(
|
||||||
|
"bird_route_export_updates_total",
|
||||||
|
"Total number of export updates",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
exportWithdraws: prometheus.NewDesc(
|
||||||
|
"bird_route_export_withdraws_total",
|
||||||
|
"Total number of export withdraws",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
exportRejected: prometheus.NewDesc(
|
||||||
|
"bird_route_export_rejected_total",
|
||||||
|
"Total number of rejected exports",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
exportFiltered: prometheus.NewDesc(
|
||||||
|
"bird_route_export_filtered_total",
|
||||||
|
"Total number of filtered exports",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
exportAccepted: prometheus.NewDesc(
|
||||||
|
"bird_route_export_accepted_total",
|
||||||
|
"Total number of accepted exports",
|
||||||
|
[]string{"name", "proto", "channel", "table"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
bgpHoldTimer: prometheus.NewDesc(
|
||||||
|
"bird_bgp_hold_timer_seconds",
|
||||||
|
"BGP hold timer in seconds",
|
||||||
|
[]string{"name", "neighbor_address"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
bgpKeepaliveTimer: prometheus.NewDesc(
|
||||||
|
"bird_bgp_keepalive_timer_seconds",
|
||||||
|
"BGP keepalive timer in seconds",
|
||||||
|
[]string{"name", "neighbor_address"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
bgpSendHoldTimer: prometheus.NewDesc(
|
||||||
|
"bird_bgp_send_hold_timer_seconds",
|
||||||
|
"BGP send hold timer in seconds",
|
||||||
|
[]string{"name", "neighbor_address"},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BirdCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||||
|
ch <- c.protocolUp
|
||||||
|
ch <- c.bgpState
|
||||||
|
ch <- c.routeImported
|
||||||
|
ch <- c.routeFiltered
|
||||||
|
ch <- c.routeExported
|
||||||
|
ch <- c.routePreferred
|
||||||
|
ch <- c.importUpdates
|
||||||
|
ch <- c.importWithdraws
|
||||||
|
ch <- c.importRejected
|
||||||
|
ch <- c.importFiltered
|
||||||
|
ch <- c.importIgnored
|
||||||
|
ch <- c.importAccepted
|
||||||
|
ch <- c.exportUpdates
|
||||||
|
ch <- c.exportWithdraws
|
||||||
|
ch <- c.exportRejected
|
||||||
|
ch <- c.exportFiltered
|
||||||
|
ch <- c.exportAccepted
|
||||||
|
ch <- c.bgpHoldTimer
|
||||||
|
ch <- c.bgpKeepaliveTimer
|
||||||
|
ch <- c.bgpSendHoldTimer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BirdCollector) Collect(ch chan<- prometheus.Metric) {
|
||||||
|
protocolsMutex.RLock()
|
||||||
|
protocols := cachedProtocols
|
||||||
|
protocolsMutex.RUnlock()
|
||||||
|
|
||||||
|
for _, p := range protocols {
|
||||||
|
// Protocol status
|
||||||
|
upValue := 0.0
|
||||||
|
if strings.ToLower(p.State) == "up" {
|
||||||
|
upValue = 1.0
|
||||||
|
}
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.protocolUp,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
upValue,
|
||||||
|
p.Name, p.Proto, p.Table, p.Info,
|
||||||
|
)
|
||||||
|
|
||||||
|
// BGP specific metrics
|
||||||
|
if p.BGPInfo != nil {
|
||||||
|
bgpEstablished := 0.0
|
||||||
|
if p.BGPInfo.BGPState == "Established" {
|
||||||
|
bgpEstablished = 1.0
|
||||||
|
}
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.bgpState,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
bgpEstablished,
|
||||||
|
p.Name, p.BGPInfo.NeighborAddr, p.BGPInfo.NeighborAS, p.BGPInfo.LocalAS, p.BGPInfo.NeighborID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if p.BGPInfo.HoldTimer > 0 {
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.bgpHoldTimer,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
p.BGPInfo.HoldTimer,
|
||||||
|
p.Name, p.BGPInfo.NeighborAddr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.BGPInfo.KeepaliveTimer > 0 {
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.bgpKeepaliveTimer,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
p.BGPInfo.KeepaliveTimer,
|
||||||
|
p.Name, p.BGPInfo.NeighborAddr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.BGPInfo.SendHoldTimer > 0 {
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.bgpSendHoldTimer,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
p.BGPInfo.SendHoldTimer,
|
||||||
|
p.Name, p.BGPInfo.NeighborAddr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel metrics
|
||||||
|
for _, channel := range p.Channels {
|
||||||
|
labels := []string{p.Name, p.Proto, channel.Name, channel.Table}
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.routeImported,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(channel.Routes.Imported),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.routeFiltered,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(channel.Routes.Filtered),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.routeExported,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(channel.Routes.Exported),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.routePreferred,
|
||||||
|
prometheus.GaugeValue,
|
||||||
|
float64(channel.Routes.Preferred),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.importUpdates,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ImportStats.Updates),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.importWithdraws,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ImportStats.Withdraws),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.importRejected,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ImportStats.Rejected),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.importFiltered,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ImportStats.Filtered),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.importIgnored,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ImportStats.Ignored),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.importAccepted,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ImportStats.Accepted),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.exportUpdates,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ExportStats.Updates),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.exportWithdraws,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ExportStats.Withdraws),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.exportRejected,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ExportStats.Rejected),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.exportFiltered,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ExportStats.Filtered),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- prometheus.MustNewConstMetric(
|
||||||
|
c.exportAccepted,
|
||||||
|
prometheus.CounterValue,
|
||||||
|
float64(channel.ExportStats.Accepted),
|
||||||
|
labels...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBirdOutput() (io.ReadCloser, error) {
|
||||||
|
// Connect to BIRD socket
|
||||||
|
conn, err := net.Dial("unix", *birdSocket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to BIRD socket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
|
||||||
|
// Read welcome message - BIRD sends "0001 BIRD x.y.z ready."
|
||||||
|
// Lines ending with space (not dash) are the last line of a response
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to read welcome message: %w", err)
|
||||||
|
}
|
||||||
|
// Welcome message ends with "0001 " (code + space)
|
||||||
|
// Check if line starts with 4 digits followed by space
|
||||||
|
if len(line) >= 5 &&
|
||||||
|
line[0] >= '0' && line[0] <= '9' &&
|
||||||
|
line[1] >= '0' && line[1] <= '9' &&
|
||||||
|
line[2] >= '0' && line[2] <= '9' &&
|
||||||
|
line[3] >= '0' && line[3] <= '9' &&
|
||||||
|
line[4] == ' ' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the command
|
||||||
|
command := "show protocols all"
|
||||||
|
if *debug {
|
||||||
|
log.Printf("BIRD: %s", command)
|
||||||
|
}
|
||||||
|
_, err = fmt.Fprintf(conn, "%s\n", command)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to send command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and strip BIRD protocol codes from the response
|
||||||
|
var output strings.Builder
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BIRD protocol format: "CODE-text" or "CODE text" or "CODE"
|
||||||
|
// CODE is 4 digits, dash means more lines follow, space means last line
|
||||||
|
if len(line) >= 4 &&
|
||||||
|
line[0] >= '0' && line[0] <= '9' &&
|
||||||
|
line[1] >= '0' && line[1] <= '9' &&
|
||||||
|
line[2] >= '0' && line[2] <= '9' &&
|
||||||
|
line[3] >= '0' && line[3] <= '9' {
|
||||||
|
|
||||||
|
// 0000 indicates end of output
|
||||||
|
if line[0:4] == "0000" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip lines that are just status codes (header/footer)
|
||||||
|
if len(line) == 5 && (line[4] == '\n' || line[4] == ' ') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If line has content after code and separator, strip the code
|
||||||
|
if len(line) > 5 && (line[4] == '-' || line[4] == ' ') {
|
||||||
|
// Remove the "CODE-" or "CODE " prefix
|
||||||
|
output.WriteString(line[5:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lines without codes (continuation lines) - write as-is
|
||||||
|
output.WriteString(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
// Return a reader for the stripped output
|
||||||
|
return io.NopCloser(strings.NewReader(output.String())), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBirdOutput(input io.Reader) ([]Protocol, error) {
|
||||||
|
var protocols []Protocol
|
||||||
|
scanner := bufio.NewScanner(input)
|
||||||
|
|
||||||
|
var currentProto *Protocol
|
||||||
|
var currentChannel *Channel
|
||||||
|
var inChannelStats bool
|
||||||
|
|
||||||
|
// Regex patterns
|
||||||
|
// Match protocol line - Since field can be "YYYY-MM-DD HH:MM:SS" or just "HH:MM:SS.xxx"
|
||||||
|
protoLineRe := regexp.MustCompile(`^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+(?:\s+\S+)?)\s*(.*)$`)
|
||||||
|
channelRe := regexp.MustCompile(`^\s+Channel (\S+)`)
|
||||||
|
bgpStateRe := regexp.MustCompile(`^\s+BGP state:\s+(.+)`)
|
||||||
|
neighborAddrRe := regexp.MustCompile(`^\s+Neighbor address:\s+(.+)`)
|
||||||
|
neighborASRe := regexp.MustCompile(`^\s+Neighbor AS:\s+(\d+)`)
|
||||||
|
localASRe := regexp.MustCompile(`^\s+Local AS:\s+(\d+)`)
|
||||||
|
neighborIDRe := regexp.MustCompile(`^\s+Neighbor ID:\s+(.+)`)
|
||||||
|
holdTimerRe := regexp.MustCompile(`^\s+Hold timer:\s+([\d.]+)/([\d.]+)`)
|
||||||
|
keepaliveTimerRe := regexp.MustCompile(`^\s+Keepalive timer:\s+([\d.]+)/([\d.]+)`)
|
||||||
|
sendHoldTimerRe := regexp.MustCompile(`^\s+Send hold timer:\s+([\d.]+)/([\d.]+)`)
|
||||||
|
stateRe := regexp.MustCompile(`^\s+State:\s+(.+)`)
|
||||||
|
tableRe := regexp.MustCompile(`^\s+Table:\s+(.+)`)
|
||||||
|
preferenceRe := regexp.MustCompile(`^\s+Preference:\s+(\d+)`)
|
||||||
|
inputFilterRe := regexp.MustCompile(`^\s+Input filter:\s+(.+)`)
|
||||||
|
outputFilterRe := regexp.MustCompile(`^\s+Output filter:\s+(.+)`)
|
||||||
|
// Routes line can be either:
|
||||||
|
// "Routes: X imported, Y exported, Z preferred"
|
||||||
|
// "Routes: X imported, Y filtered, Z exported, W preferred"
|
||||||
|
routesRe := regexp.MustCompile(`^\s+Routes:\s+(\d+) imported,(?:\s+(\d+) filtered,)?\s+(\d+) exported,\s+(\d+) preferred`)
|
||||||
|
importStatsRe := regexp.MustCompile(`^\s+Import updates:\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)`)
|
||||||
|
importWithdrawsRe := regexp.MustCompile(`^\s+Import withdraws:\s+(\d+)\s+(\d+)\s+---\s+(\d+)\s+(\d+)`)
|
||||||
|
exportStatsRe := regexp.MustCompile(`^\s+Export updates:\s+(\d+)\s+(\d+)\s+(\d+)\s+---\s+(\d+)`)
|
||||||
|
exportWithdrawsRe := regexp.MustCompile(`^\s+Export withdraws:\s+(\d+)\s+---\s+---\s+---\s+(\d+)`)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// Skip header lines
|
||||||
|
if strings.HasPrefix(line, "BIRD") || strings.HasPrefix(line, "Name") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty line often signals end of protocol block
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
if currentProto != nil && currentChannel != nil {
|
||||||
|
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||||
|
currentChannel = nil
|
||||||
|
}
|
||||||
|
inChannelStats = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for new protocol
|
||||||
|
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
|
||||||
|
// Save previous protocol
|
||||||
|
if currentProto != nil {
|
||||||
|
if currentChannel != nil {
|
||||||
|
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||||
|
currentChannel = nil
|
||||||
|
}
|
||||||
|
protocols = append(protocols, *currentProto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse new protocol line
|
||||||
|
matches := protoLineRe.FindStringSubmatch(line)
|
||||||
|
if len(matches) > 6 {
|
||||||
|
currentProto = &Protocol{
|
||||||
|
Name: matches[1],
|
||||||
|
Proto: matches[2],
|
||||||
|
Table: matches[3],
|
||||||
|
State: matches[4],
|
||||||
|
Since: matches[5],
|
||||||
|
Info: strings.TrimSpace(matches[6]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentProto.Proto == "BGP" {
|
||||||
|
currentProto.BGPInfo = &BGPInfo{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inChannelStats = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentProto == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse BGP specific info
|
||||||
|
if matches := bgpStateRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
currentProto.BGPInfo.BGPState = strings.TrimSpace(matches[1])
|
||||||
|
}
|
||||||
|
} else if matches := neighborAddrRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
currentProto.BGPInfo.NeighborAddr = strings.TrimSpace(matches[1])
|
||||||
|
}
|
||||||
|
} else if matches := neighborASRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
currentProto.BGPInfo.NeighborAS = matches[1]
|
||||||
|
}
|
||||||
|
} else if matches := localASRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
currentProto.BGPInfo.LocalAS = matches[1]
|
||||||
|
}
|
||||||
|
} else if matches := neighborIDRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
currentProto.BGPInfo.NeighborID = strings.TrimSpace(matches[1])
|
||||||
|
}
|
||||||
|
} else if matches := holdTimerRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||||
|
currentProto.BGPInfo.HoldTimer = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if matches := keepaliveTimerRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||||
|
currentProto.BGPInfo.KeepaliveTimer = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if matches := sendHoldTimerRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentProto.BGPInfo != nil {
|
||||||
|
if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||||
|
currentProto.BGPInfo.SendHoldTimer = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for channel start
|
||||||
|
if matches := channelRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if currentChannel != nil {
|
||||||
|
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||||
|
}
|
||||||
|
currentChannel = &Channel{
|
||||||
|
Name: matches[1],
|
||||||
|
}
|
||||||
|
inChannelStats = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentChannel == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse channel properties
|
||||||
|
if matches := stateRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
currentChannel.State = strings.TrimSpace(matches[1])
|
||||||
|
} else if matches := tableRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
currentChannel.Table = strings.TrimSpace(matches[1])
|
||||||
|
} else if matches := preferenceRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||||
|
currentChannel.Preference = val
|
||||||
|
}
|
||||||
|
} else if matches := inputFilterRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
currentChannel.InputFilter = strings.TrimSpace(matches[1])
|
||||||
|
} else if matches := outputFilterRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
currentChannel.OutputFilter = strings.TrimSpace(matches[1])
|
||||||
|
} else if matches := routesRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
// matches[1] = imported
|
||||||
|
// matches[2] = filtered (optional, may be empty)
|
||||||
|
// matches[3] = exported
|
||||||
|
// matches[4] = preferred
|
||||||
|
if imported, err := strconv.Atoi(matches[1]); err == nil {
|
||||||
|
currentChannel.Routes.Imported = imported
|
||||||
|
}
|
||||||
|
if matches[2] != "" {
|
||||||
|
if filtered, err := strconv.Atoi(matches[2]); err == nil {
|
||||||
|
currentChannel.Routes.Filtered = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exported, err := strconv.Atoi(matches[3]); err == nil {
|
||||||
|
currentChannel.Routes.Exported = exported
|
||||||
|
}
|
||||||
|
if preferred, err := strconv.Atoi(matches[4]); err == nil {
|
||||||
|
currentChannel.Routes.Preferred = preferred
|
||||||
|
}
|
||||||
|
} else if strings.Contains(line, "Route change stats:") {
|
||||||
|
inChannelStats = true
|
||||||
|
} else if inChannelStats {
|
||||||
|
if matches := importStatsRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||||
|
currentChannel.ImportStats.Updates = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(matches[2]); err == nil {
|
||||||
|
currentChannel.ImportStats.Rejected = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(matches[3]); err == nil {
|
||||||
|
currentChannel.ImportStats.Filtered = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(matches[4]); err == nil {
|
||||||
|
currentChannel.ImportStats.Ignored = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(matches[5]); err == nil {
|
||||||
|
currentChannel.ImportStats.Accepted = val
|
||||||
|
}
|
||||||
|
} else if matches := importWithdrawsRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||||
|
currentChannel.ImportStats.Withdraws = val
|
||||||
|
}
|
||||||
|
} else if matches := exportStatsRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||||
|
currentChannel.ExportStats.Updates = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(matches[2]); err == nil {
|
||||||
|
currentChannel.ExportStats.Rejected = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(matches[3]); err == nil {
|
||||||
|
currentChannel.ExportStats.Filtered = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.Atoi(matches[4]); err == nil {
|
||||||
|
currentChannel.ExportStats.Accepted = val
|
||||||
|
}
|
||||||
|
} else if matches := exportWithdrawsRe.FindStringSubmatch(line); matches != nil {
|
||||||
|
if val, err := strconv.Atoi(matches[1]); err == nil {
|
||||||
|
currentChannel.ExportStats.Withdraws = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't forget the last protocol
|
||||||
|
if currentProto != nil {
|
||||||
|
if currentChannel != nil {
|
||||||
|
currentProto.Channels = append(currentProto.Channels, *currentChannel)
|
||||||
|
}
|
||||||
|
protocols = append(protocols, *currentProto)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocols, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrapeOnce() error {
|
||||||
|
input, err := getBirdOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get BIRD output: %w", err)
|
||||||
|
}
|
||||||
|
defer input.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(input)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse BIRD output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protocolsMutex.Lock()
|
||||||
|
cachedProtocols = protocols
|
||||||
|
protocolsMutex.Unlock()
|
||||||
|
|
||||||
|
if *debug {
|
||||||
|
log.Printf("Successfully scraped %d protocols", len(protocols))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startPeriodicScraper() {
|
||||||
|
ticker := time.NewTicker(*scrapePeriod)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if err := scrapeOnce(); err != nil {
|
||||||
|
log.Printf("Error scraping BIRD data: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
log.Printf("BIRD Exporter version %s", Version)
|
||||||
|
|
||||||
|
// Perform initial scrape
|
||||||
|
if *debug {
|
||||||
|
log.Printf("Performing initial scrape...")
|
||||||
|
}
|
||||||
|
if err := scrapeOnce(); err != nil {
|
||||||
|
log.Fatalf("Initial scrape failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start periodic scraper in background
|
||||||
|
log.Printf("Starting periodic scraper with period %s", *scrapePeriod)
|
||||||
|
go startPeriodicScraper()
|
||||||
|
|
||||||
|
collector := NewBirdCollector()
|
||||||
|
prometheus.MustRegister(collector)
|
||||||
|
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = w.Write([]byte(`<html>
|
||||||
|
<head><title>BIRD Exporter</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>BIRD Exporter</h1>
|
||||||
|
<p><a href="/metrics">Metrics</a></p>
|
||||||
|
</body>
|
||||||
|
</html>`))
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("Starting BIRD exporter on %s", *listenAddr)
|
||||||
|
log.Fatal(http.ListenAndServe(*listenAddr, nil))
|
||||||
|
}
|
||||||
416
cmd/bird-exporter/main_test.go
Normal file
416
cmd/bird-exporter/main_test.go
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseBirdOutput(t *testing.T) {
|
||||||
|
file, err := os.Open("testdata/sample_output.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open test file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we got the expected number of protocols
|
||||||
|
expectedProtocols := 8 // device1, direct1, kernel4, static4, ospf4, fogixp_47498_ipv4_3, fogixp_47498_ipv6_1, fogixp_47498_ipv6_2
|
||||||
|
if len(protocols) != expectedProtocols {
|
||||||
|
t.Errorf("Expected %d protocols, got %d", expectedProtocols, len(protocols))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Device protocol
|
||||||
|
device := findProtocol(protocols, "device1")
|
||||||
|
if device == nil {
|
||||||
|
t.Fatal("device1 protocol not found")
|
||||||
|
}
|
||||||
|
if device.Proto != "Device" {
|
||||||
|
t.Errorf("Expected proto 'Device', got '%s'", device.Proto)
|
||||||
|
}
|
||||||
|
if device.State != "up" {
|
||||||
|
t.Errorf("Expected state 'up', got '%s'", device.State)
|
||||||
|
}
|
||||||
|
if device.Table != "---" {
|
||||||
|
t.Errorf("Expected table '---', got '%s'", device.Table)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Direct protocol with channels
|
||||||
|
direct := findProtocol(protocols, "direct1")
|
||||||
|
if direct == nil {
|
||||||
|
t.Fatal("direct1 protocol not found")
|
||||||
|
}
|
||||||
|
if direct.Proto != "Direct" {
|
||||||
|
t.Errorf("Expected proto 'Direct', got '%s'", direct.Proto)
|
||||||
|
}
|
||||||
|
if len(direct.Channels) != 2 {
|
||||||
|
t.Errorf("Expected 2 channels, got %d", len(direct.Channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test direct1 ipv4 channel
|
||||||
|
ipv4Chan := findChannel(direct.Channels, "ipv4")
|
||||||
|
if ipv4Chan == nil {
|
||||||
|
t.Fatal("ipv4 channel not found in direct1")
|
||||||
|
}
|
||||||
|
if ipv4Chan.State != "UP" {
|
||||||
|
t.Errorf("Expected channel state 'UP', got '%s'", ipv4Chan.State)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Table != "master4" {
|
||||||
|
t.Errorf("Expected table 'master4', got '%s'", ipv4Chan.Table)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Preference != 240 {
|
||||||
|
t.Errorf("Expected preference 240, got %d", ipv4Chan.Preference)
|
||||||
|
}
|
||||||
|
if ipv4Chan.InputFilter != "ACCEPT" {
|
||||||
|
t.Errorf("Expected input filter 'ACCEPT', got '%s'", ipv4Chan.InputFilter)
|
||||||
|
}
|
||||||
|
if ipv4Chan.OutputFilter != "REJECT" {
|
||||||
|
t.Errorf("Expected output filter 'REJECT', got '%s'", ipv4Chan.OutputFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test route stats
|
||||||
|
if ipv4Chan.Routes.Imported != 10 {
|
||||||
|
t.Errorf("Expected 10 imported routes, got %d", ipv4Chan.Routes.Imported)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Routes.Exported != 0 {
|
||||||
|
t.Errorf("Expected 0 exported routes, got %d", ipv4Chan.Routes.Exported)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Routes.Preferred != 10 {
|
||||||
|
t.Errorf("Expected 10 preferred routes, got %d", ipv4Chan.Routes.Preferred)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test import stats
|
||||||
|
if ipv4Chan.ImportStats.Updates != 155 {
|
||||||
|
t.Errorf("Expected 155 import updates, got %d", ipv4Chan.ImportStats.Updates)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ImportStats.Rejected != 0 {
|
||||||
|
t.Errorf("Expected 0 import rejected, got %d", ipv4Chan.ImportStats.Rejected)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ImportStats.Filtered != 0 {
|
||||||
|
t.Errorf("Expected 0 import filtered, got %d", ipv4Chan.ImportStats.Filtered)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ImportStats.Ignored != 0 {
|
||||||
|
t.Errorf("Expected 0 import ignored, got %d", ipv4Chan.ImportStats.Ignored)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ImportStats.Accepted != 155 {
|
||||||
|
t.Errorf("Expected 155 import accepted, got %d", ipv4Chan.ImportStats.Accepted)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ImportStats.Withdraws != 153 {
|
||||||
|
t.Errorf("Expected 153 import withdraws, got %d", ipv4Chan.ImportStats.Withdraws)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test export stats
|
||||||
|
if ipv4Chan.ExportStats.Updates != 0 {
|
||||||
|
t.Errorf("Expected 0 export updates, got %d", ipv4Chan.ExportStats.Updates)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ExportStats.Withdraws != 0 {
|
||||||
|
t.Errorf("Expected 0 export withdraws, got %d", ipv4Chan.ExportStats.Withdraws)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test direct1 ipv6 channel
|
||||||
|
ipv6Chan := findChannel(direct.Channels, "ipv6")
|
||||||
|
if ipv6Chan == nil {
|
||||||
|
t.Fatal("ipv6 channel not found in direct1")
|
||||||
|
}
|
||||||
|
if ipv6Chan.Routes.Imported != 5 {
|
||||||
|
t.Errorf("Expected 5 imported routes, got %d", ipv6Chan.Routes.Imported)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBGPProtocol(t *testing.T) {
|
||||||
|
file, err := os.Open("testdata/sample_output.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open test file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test BGP protocol (established)
|
||||||
|
bgp := findProtocol(protocols, "fogixp_47498_ipv4_3")
|
||||||
|
if bgp == nil {
|
||||||
|
t.Fatal("fogixp_47498_ipv4_3 protocol not found")
|
||||||
|
}
|
||||||
|
if bgp.Proto != "BGP" {
|
||||||
|
t.Errorf("Expected proto 'BGP', got '%s'", bgp.Proto)
|
||||||
|
}
|
||||||
|
if bgp.State != "up" {
|
||||||
|
t.Errorf("Expected state 'up', got '%s'", bgp.State)
|
||||||
|
}
|
||||||
|
if bgp.Info != "Established" {
|
||||||
|
t.Errorf("Expected info 'Established', got '%s'", bgp.Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test BGP info
|
||||||
|
if bgp.BGPInfo == nil {
|
||||||
|
t.Fatal("BGP info is nil")
|
||||||
|
}
|
||||||
|
if bgp.BGPInfo.BGPState != "Established" {
|
||||||
|
t.Errorf("Expected BGP state 'Established', got '%s'", bgp.BGPInfo.BGPState)
|
||||||
|
}
|
||||||
|
if bgp.BGPInfo.NeighborAddr != "185.1.147.3" {
|
||||||
|
t.Errorf("Expected neighbor address '185.1.147.3', got '%s'", bgp.BGPInfo.NeighborAddr)
|
||||||
|
}
|
||||||
|
if bgp.BGPInfo.NeighborAS != "47498" {
|
||||||
|
t.Errorf("Expected neighbor AS '47498', got '%s'", bgp.BGPInfo.NeighborAS)
|
||||||
|
}
|
||||||
|
if bgp.BGPInfo.LocalAS != "8298" {
|
||||||
|
t.Errorf("Expected local AS '8298', got '%s'", bgp.BGPInfo.LocalAS)
|
||||||
|
}
|
||||||
|
if bgp.BGPInfo.NeighborID != "185.1.147.3" {
|
||||||
|
t.Errorf("Expected neighbor ID '185.1.147.3', got '%s'", bgp.BGPInfo.NeighborID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test BGP timers
|
||||||
|
if bgp.BGPInfo.HoldTimer != 174.765 {
|
||||||
|
t.Errorf("Expected hold timer 174.765, got %f", bgp.BGPInfo.HoldTimer)
|
||||||
|
}
|
||||||
|
if bgp.BGPInfo.KeepaliveTimer != 30.587 {
|
||||||
|
t.Errorf("Expected keepalive timer 30.587, got %f", bgp.BGPInfo.KeepaliveTimer)
|
||||||
|
}
|
||||||
|
if bgp.BGPInfo.SendHoldTimer != 425.866 {
|
||||||
|
t.Errorf("Expected send hold timer 425.866, got %f", bgp.BGPInfo.SendHoldTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test BGP channel
|
||||||
|
if len(bgp.Channels) != 1 {
|
||||||
|
t.Errorf("Expected 1 channel, got %d", len(bgp.Channels))
|
||||||
|
}
|
||||||
|
bgpChan := findChannel(bgp.Channels, "ipv4")
|
||||||
|
if bgpChan == nil {
|
||||||
|
t.Fatal("ipv4 channel not found in fogixp_47498_ipv4_3")
|
||||||
|
}
|
||||||
|
if bgpChan.Routes.Imported != 44589 {
|
||||||
|
t.Errorf("Expected 44589 imported routes, got %d", bgpChan.Routes.Imported)
|
||||||
|
}
|
||||||
|
if bgpChan.Routes.Filtered != 6 {
|
||||||
|
t.Errorf("Expected 6 filtered routes, got %d", bgpChan.Routes.Filtered)
|
||||||
|
}
|
||||||
|
if bgpChan.Routes.Exported != 27 {
|
||||||
|
t.Errorf("Expected 27 exported routes, got %d", bgpChan.Routes.Exported)
|
||||||
|
}
|
||||||
|
if bgpChan.Routes.Preferred != 4173 {
|
||||||
|
t.Errorf("Expected 4173 preferred routes, got %d", bgpChan.Routes.Preferred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseKernelProtocol(t *testing.T) {
|
||||||
|
file, err := os.Open("testdata/sample_output.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open test file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kernel := findProtocol(protocols, "kernel4")
|
||||||
|
if kernel == nil {
|
||||||
|
t.Fatal("kernel4 protocol not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(kernel.Channels) != 1 {
|
||||||
|
t.Fatalf("Expected 1 channel, got %d", len(kernel.Channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv4Chan := kernel.Channels[0]
|
||||||
|
if ipv4Chan.Routes.Exported != 1044496 {
|
||||||
|
t.Errorf("Expected 1044496 exported routes, got %d", ipv4Chan.Routes.Exported)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ExportStats.Updates != 1684718679 {
|
||||||
|
t.Errorf("Expected 1684718679 export updates, got %d", ipv4Chan.ExportStats.Updates)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ExportStats.Filtered != 143 {
|
||||||
|
t.Errorf("Expected 143 export filtered, got %d", ipv4Chan.ExportStats.Filtered)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ExportStats.Accepted != 1684718536 {
|
||||||
|
t.Errorf("Expected 1684718536 export accepted, got %d", ipv4Chan.ExportStats.Accepted)
|
||||||
|
}
|
||||||
|
if ipv4Chan.ExportStats.Withdraws != 37968736 {
|
||||||
|
t.Errorf("Expected 37968736 export withdraws, got %d", ipv4Chan.ExportStats.Withdraws)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOSPFProtocol(t *testing.T) {
|
||||||
|
file, err := os.Open("testdata/sample_output.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open test file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ospf := findProtocol(protocols, "ospf4")
|
||||||
|
if ospf == nil {
|
||||||
|
t.Fatal("ospf4 protocol not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ospf.Proto != "OSPF" {
|
||||||
|
t.Errorf("Expected proto 'OSPF', got '%s'", ospf.Proto)
|
||||||
|
}
|
||||||
|
if ospf.Info != "Running" {
|
||||||
|
t.Errorf("Expected info 'Running', got '%s'", ospf.Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ospf.Channels) != 1 {
|
||||||
|
t.Fatalf("Expected 1 channel, got %d", len(ospf.Channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv4Chan := ospf.Channels[0]
|
||||||
|
if ipv4Chan.InputFilter != "f_ospf" {
|
||||||
|
t.Errorf("Expected input filter 'f_ospf', got '%s'", ipv4Chan.InputFilter)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Routes.Imported != 26 {
|
||||||
|
t.Errorf("Expected 26 imported routes, got %d", ipv4Chan.Routes.Imported)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Routes.Exported != 4 {
|
||||||
|
t.Errorf("Expected 4 exported routes, got %d", ipv4Chan.Routes.Exported)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Routes.Preferred != 25 {
|
||||||
|
t.Errorf("Expected 25 preferred routes, got %d", ipv4Chan.Routes.Preferred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseStaticProtocol(t *testing.T) {
|
||||||
|
file, err := os.Open("testdata/sample_output.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open test file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
static := findProtocol(protocols, "static4")
|
||||||
|
if static == nil {
|
||||||
|
t.Fatal("static4 protocol not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if static.Proto != "Static" {
|
||||||
|
t.Errorf("Expected proto 'Static', got '%s'", static.Proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(static.Channels) != 1 {
|
||||||
|
t.Fatalf("Expected 1 channel, got %d", len(static.Channels))
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv4Chan := static.Channels[0]
|
||||||
|
if ipv4Chan.Preference != 200 {
|
||||||
|
t.Errorf("Expected preference 200, got %d", ipv4Chan.Preference)
|
||||||
|
}
|
||||||
|
if ipv4Chan.Routes.Imported != 3 {
|
||||||
|
t.Errorf("Expected 3 imported routes, got %d", ipv4Chan.Routes.Imported)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBirdOutputWithRealFile(t *testing.T) {
|
||||||
|
// Test with the actual birdc.show.proto.all file if it exists
|
||||||
|
if _, err := os.Stat("../../birdc.show.proto.all"); os.IsNotExist(err) {
|
||||||
|
t.Skip("Real birdc.show.proto.all file not found, skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open("../../birdc.show.proto.all")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open real file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseBirdOutput() failed on real file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(protocols) == 0 {
|
||||||
|
t.Error("Expected at least one protocol from real file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have various protocol types
|
||||||
|
hasDevice := false
|
||||||
|
hasBGP := false
|
||||||
|
for _, p := range protocols {
|
||||||
|
if p.Proto == "Device" {
|
||||||
|
hasDevice = true
|
||||||
|
}
|
||||||
|
if p.Proto == "BGP" {
|
||||||
|
hasBGP = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasDevice {
|
||||||
|
t.Error("Expected to find at least one Device protocol")
|
||||||
|
}
|
||||||
|
if !hasBGP {
|
||||||
|
t.Error("Expected to find at least one BGP protocol")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEmptyFile(t *testing.T) {
|
||||||
|
// Create a temporary empty file
|
||||||
|
tmpFile, err := os.CreateTemp("", "empty-*.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
file, err := os.Open(tmpFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to open temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
protocols, err := parseBirdOutput(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseBirdOutput() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(protocols) != 0 {
|
||||||
|
t.Errorf("Expected 0 protocols from empty file, got %d", len(protocols))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFileNotFound(t *testing.T) {
|
||||||
|
_, err := os.Open("/tmp/does-not-exist-bird-test-12345.txt")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error when opening non-existent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
func findProtocol(protocols []Protocol, name string) *Protocol {
|
||||||
|
for i := range protocols {
|
||||||
|
if protocols[i].Name == name {
|
||||||
|
return &protocols[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findChannel(channels []Channel, name string) *Channel {
|
||||||
|
for i := range channels {
|
||||||
|
if channels[i].Name == name {
|
||||||
|
return &channels[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMain to handle flag parsing for tests
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
flag.Parse()
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
204
cmd/bird-exporter/testdata/sample_output.txt
vendored
Normal file
204
cmd/bird-exporter/testdata/sample_output.txt
vendored
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
BIRD v2.15.1-4-g280daed5-x ready.
|
||||||
|
Name Proto Table State Since Info
|
||||||
|
device1 Device --- up 2024-04-16 21:05:38
|
||||||
|
|
||||||
|
direct1 Direct --- up 2024-04-16 21:05:38
|
||||||
|
Channel ipv4
|
||||||
|
State: UP
|
||||||
|
Table: master4
|
||||||
|
Preference: 240
|
||||||
|
Input filter: ACCEPT
|
||||||
|
Output filter: REJECT
|
||||||
|
Routes: 10 imported, 0 exported, 10 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 155 0 0 0 155
|
||||||
|
Import withdraws: 153 0 --- 8 145
|
||||||
|
Export updates: 0 0 0 --- 0
|
||||||
|
Export withdraws: 0 --- --- --- 0
|
||||||
|
Channel ipv6
|
||||||
|
State: UP
|
||||||
|
Table: master6
|
||||||
|
Preference: 240
|
||||||
|
Input filter: ACCEPT
|
||||||
|
Output filter: REJECT
|
||||||
|
Routes: 5 imported, 0 exported, 5 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 140 0 0 0 140
|
||||||
|
Import withdraws: 138 0 --- 8 130
|
||||||
|
Export updates: 0 0 0 --- 0
|
||||||
|
Export withdraws: 0 --- --- --- 0
|
||||||
|
|
||||||
|
kernel4 Kernel master4 up 2024-04-16 21:05:38
|
||||||
|
Channel ipv4
|
||||||
|
State: UP
|
||||||
|
Table: master4
|
||||||
|
Preference: 10
|
||||||
|
Input filter: REJECT
|
||||||
|
Output filter: (unnamed)
|
||||||
|
Routes: 0 imported, 1044496 exported, 0 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 0 0 0 0 0
|
||||||
|
Import withdraws: 0 0 --- 0 0
|
||||||
|
Export updates: 1684718679 0 143 --- 1684718536
|
||||||
|
Export withdraws: 37968736 --- --- --- 37968651
|
||||||
|
|
||||||
|
static4 Static master4 up 2024-04-16 21:05:38
|
||||||
|
Channel ipv4
|
||||||
|
State: UP
|
||||||
|
Table: master4
|
||||||
|
Preference: 200
|
||||||
|
Input filter: ACCEPT
|
||||||
|
Output filter: ACCEPT
|
||||||
|
Routes: 3 imported, 0 exported, 3 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 4 0 0 0 4
|
||||||
|
Import withdraws: 1 0 --- 0 1
|
||||||
|
Export updates: 0 0 0 --- 0
|
||||||
|
Export withdraws: 0 --- --- --- 0
|
||||||
|
|
||||||
|
ospf4 OSPF master4 up 2024-06-19 19:34:13 Running
|
||||||
|
Channel ipv4
|
||||||
|
State: UP
|
||||||
|
Table: master4
|
||||||
|
Preference: 150
|
||||||
|
Input filter: f_ospf
|
||||||
|
Output filter: f_ospf
|
||||||
|
Routes: 26 imported, 4 exported, 25 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 11565 0 0 0 11565
|
||||||
|
Import withdraws: 912 0 --- 0 912
|
||||||
|
Export updates: 1521957177 10903 1521945947 --- 327
|
||||||
|
Export withdraws: 35569447 --- --- --- 72
|
||||||
|
|
||||||
|
fogixp_47498_ipv4_3 BGP --- up 2025-12-29 15:33:26 Established
|
||||||
|
Description: FogIXP Route Servers (FogIXP Route Servers)
|
||||||
|
BGP state: Established
|
||||||
|
Neighbor address: 185.1.147.3
|
||||||
|
Neighbor AS: 47498
|
||||||
|
Local AS: 8298
|
||||||
|
Neighbor ID: 185.1.147.3
|
||||||
|
Local capabilities
|
||||||
|
Multiprotocol
|
||||||
|
AF announced: ipv4
|
||||||
|
Route refresh
|
||||||
|
Graceful restart
|
||||||
|
4-octet AS numbers
|
||||||
|
Enhanced refresh
|
||||||
|
Long-lived graceful restart
|
||||||
|
Neighbor capabilities
|
||||||
|
Multiprotocol
|
||||||
|
AF announced: ipv4
|
||||||
|
Route refresh
|
||||||
|
Graceful restart
|
||||||
|
4-octet AS numbers
|
||||||
|
Enhanced refresh
|
||||||
|
Long-lived graceful restart
|
||||||
|
Role: rs_server
|
||||||
|
Session: external AS4
|
||||||
|
Source address: 185.1.147.44
|
||||||
|
Hold timer: 174.765/240
|
||||||
|
Keepalive timer: 30.587/80
|
||||||
|
Send hold timer: 425.866/480
|
||||||
|
Channel ipv4
|
||||||
|
State: UP
|
||||||
|
Table: master4
|
||||||
|
Preference: 100
|
||||||
|
Input filter: ebgp_fogixp_47498_import
|
||||||
|
Output filter: ebgp_fogixp_47498_export
|
||||||
|
Receive limit: 50000
|
||||||
|
Action: restart
|
||||||
|
Routes: 44589 imported, 6 filtered, 27 exported, 4173 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 87409 0 17 2 87390
|
||||||
|
Import withdraws: 22781 0 --- 158 22640
|
||||||
|
Export updates: 5998808 25441 5973153 --- 214
|
||||||
|
Export withdraws: 99952 --- --- --- 46
|
||||||
|
BGP Next hop: 185.1.147.44
|
||||||
|
|
||||||
|
fogixp_47498_ipv6_1 BGP --- up 2025-08-20 22:33:06 Established
|
||||||
|
Description: FogIXP Route Servers (FogIXP Route Servers)
|
||||||
|
BGP state: Established
|
||||||
|
Neighbor address: 2001:7f8:ca:1::111
|
||||||
|
Neighbor AS: 47498
|
||||||
|
Local AS: 8298
|
||||||
|
Neighbor ID: 185.1.147.111
|
||||||
|
Local capabilities
|
||||||
|
Multiprotocol
|
||||||
|
AF announced: ipv6
|
||||||
|
Route refresh
|
||||||
|
Graceful restart
|
||||||
|
4-octet AS numbers
|
||||||
|
Enhanced refresh
|
||||||
|
Long-lived graceful restart
|
||||||
|
Neighbor capabilities
|
||||||
|
Multiprotocol
|
||||||
|
AF announced: ipv6
|
||||||
|
Route refresh
|
||||||
|
Graceful restart
|
||||||
|
4-octet AS numbers
|
||||||
|
Enhanced refresh
|
||||||
|
Long-lived graceful restart
|
||||||
|
Session: external AS4
|
||||||
|
Source address: 2001:7f8:ca:1::44
|
||||||
|
Hold timer: 160.527/240
|
||||||
|
Keepalive timer: 22.845/80
|
||||||
|
Send hold timer: 355.702/480
|
||||||
|
Channel ipv6
|
||||||
|
State: UP
|
||||||
|
Table: master6
|
||||||
|
Preference: 100
|
||||||
|
Input filter: ebgp_fogixp_47498_import
|
||||||
|
Output filter: ebgp_fogixp_47498_export
|
||||||
|
Receive limit: 50000
|
||||||
|
Action: restart
|
||||||
|
Routes: 3510 imported, 6 filtered, 47 exported, 668 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 33247408 0 11645 198185 33037578
|
||||||
|
Import withdraws: 10638021 0 --- 162699 10486967
|
||||||
|
Export updates: 648072150 14962764 633024154 --- 85232
|
||||||
|
Export withdraws: 76736402 --- --- --- 19602
|
||||||
|
BGP Next hop: 2001:7f8:ca:1::44 fe80::6a05:caff:fe32:4617
|
||||||
|
|
||||||
|
fogixp_47498_ipv6_2 BGP --- up 2025-08-20 22:33:05 Established
|
||||||
|
Description: FogIXP Route Servers (FogIXP Route Servers)
|
||||||
|
BGP state: Established
|
||||||
|
Neighbor address: 2001:7f8:ca:1::222
|
||||||
|
Neighbor AS: 47498
|
||||||
|
Local AS: 8298
|
||||||
|
Neighbor ID: 185.1.147.222
|
||||||
|
Local capabilities
|
||||||
|
Multiprotocol
|
||||||
|
AF announced: ipv6
|
||||||
|
Route refresh
|
||||||
|
Graceful restart
|
||||||
|
4-octet AS numbers
|
||||||
|
Enhanced refresh
|
||||||
|
Long-lived graceful restart
|
||||||
|
Neighbor capabilities
|
||||||
|
Multiprotocol
|
||||||
|
AF announced: ipv6
|
||||||
|
Route refresh
|
||||||
|
Graceful restart
|
||||||
|
4-octet AS numbers
|
||||||
|
Enhanced refresh
|
||||||
|
Long-lived graceful restart
|
||||||
|
Session: external AS4
|
||||||
|
Source address: 2001:7f8:ca:1::44
|
||||||
|
Hold timer: 196.907/240
|
||||||
|
Keepalive timer: 2.909/80
|
||||||
|
Send hold timer: 295.173/480
|
||||||
|
Channel ipv6
|
||||||
|
State: UP
|
||||||
|
Table: master6
|
||||||
|
Preference: 100
|
||||||
|
Input filter: ebgp_fogixp_47498_import
|
||||||
|
Output filter: ebgp_fogixp_47498_export
|
||||||
|
Receive limit: 50000
|
||||||
|
Action: restart
|
||||||
|
Routes: 3493 imported, 5 filtered, 47 exported, 2 preferred
|
||||||
|
Route change stats: received rejected filtered ignored accepted
|
||||||
|
Import updates: 34265572 0 10111 198814 34056647
|
||||||
|
Import withdraws: 10602791 0 --- 164049 10448853
|
||||||
|
Export updates: 648071805 7560692 640425882 --- 85231
|
||||||
|
Export withdraws: 76736402 --- --- --- 19602
|
||||||
|
BGP Next hop: 2001:7f8:ca:1::44 fe80::6a05:caff:fe32:4617
|
||||||
8
debian/bird-exporter.default
vendored
Normal file
8
debian/bird-exporter.default
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default settings for bird-exporter
|
||||||
|
|
||||||
|
# User and group to run bird-exporter as
|
||||||
|
BIRD_RUN_USER=bird
|
||||||
|
BIRD_RUN_GROUP=bird
|
||||||
|
|
||||||
|
# Command line arguments to pass to bird-exporter
|
||||||
|
BIRD_EXPORTER_ARGS="-period=60s -bird.socket=/var/run/bird/bird.ctl"
|
||||||
24
debian/bird-exporter.service
vendored
Normal file
24
debian/bird-exporter.service
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=BIRD Exporter for Prometheus
|
||||||
|
Documentation=https://git.ipng.ch/ipng/bird-exporter
|
||||||
|
After=network.target bird.service bird6.service
|
||||||
|
Wants=bird.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=-/etc/default/bird-exporter
|
||||||
|
User=${BIRD_RUN_USER}
|
||||||
|
Group=${BIRD_RUN_GROUP}
|
||||||
|
ExecStart=/usr/bin/bird-exporter $BIRD_EXPORTER_ARGS
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/var/run/bird
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
10
debian/changelog
vendored
Normal file
10
debian/changelog
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
bird-exporter (0.1.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release
|
||||||
|
* BIRD socket communication with periodic scraping
|
||||||
|
* Prometheus metrics export for protocols, routes, and BGP
|
||||||
|
* Support for Device, Direct, Kernel, Static, OSPF, and BGP protocols
|
||||||
|
* Route statistics including imported, filtered, exported, and preferred
|
||||||
|
* BGP-specific metrics including timers and session state
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Tue, 31 Dec 2024 14:00:00 +0000
|
||||||
27
debian/control
vendored
Normal file
27
debian/control
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Source: bird-exporter
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Pim van Pelt <pim@ipng.ch>
|
||||||
|
Build-Depends: debhelper-compat (= 13),
|
||||||
|
golang-go (>= 1.21)
|
||||||
|
Standards-Version: 4.6.0
|
||||||
|
Homepage: https://git.ipng.ch/ipng/bird-exporter
|
||||||
|
Vcs-Git: https://git.ipng.ch/ipng/bird-exporter.git
|
||||||
|
Vcs-Browser: https://git.ipng.ch/ipng/bird-exporter
|
||||||
|
|
||||||
|
Package: bird-exporter
|
||||||
|
Architecture: any
|
||||||
|
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||||
|
Recommends: bird2
|
||||||
|
Description: Prometheus exporter for BIRD routing daemon
|
||||||
|
bird-exporter is a Prometheus metrics exporter for the BIRD routing daemon.
|
||||||
|
It connects to BIRD's control socket and exports metrics about protocols,
|
||||||
|
routes, and BGP sessions.
|
||||||
|
.
|
||||||
|
Features:
|
||||||
|
* Real-time metrics via BIRD control socket
|
||||||
|
* Support for all major BIRD protocol types (BGP, OSPF, Kernel, Static, etc.)
|
||||||
|
* Detailed route statistics (imported, filtered, exported, preferred)
|
||||||
|
* BGP-specific metrics including session state and timers
|
||||||
|
* Configurable scrape interval
|
||||||
|
* Thread-safe periodic updates with caching
|
||||||
31
debian/copyright
vendored
Normal file
31
debian/copyright
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Upstream-Name: bird-exporter
|
||||||
|
Upstream-Contact: Pim van Pelt <pim@ipng.ch>
|
||||||
|
Source: https://git.ipng.ch/ipng/bird-exporter
|
||||||
|
|
||||||
|
Files: *
|
||||||
|
Copyright: 2024 BIRD Exporter Authors
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
Files: debian/*
|
||||||
|
Copyright: 2024 BIRD Exporter Authors
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
.
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
1
debian/install
vendored
Normal file
1
debian/install
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bird-exporter usr/bin/
|
||||||
33
debian/rules
vendored
Executable file
33
debian/rules
vendored
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
export DH_VERBOSE = 1
|
||||||
|
export GO111MODULE = on
|
||||||
|
export GOPROXY = direct
|
||||||
|
export GOCACHE = $(CURDIR)/debian/.gocache
|
||||||
|
export GOPATH = $(CURDIR)/debian/go
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
override_dh_auto_build:
|
||||||
|
go build -v -ldflags="-s -w" -o bird-exporter ./cmd/bird-exporter/
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
install -D -m 0755 bird-exporter debian/bird-exporter/usr/bin/bird-exporter
|
||||||
|
install -D -m 0644 debian/bird-exporter.service debian/bird-exporter/lib/systemd/system/bird-exporter.service
|
||||||
|
install -D -m 0644 debian/bird-exporter.default debian/bird-exporter/etc/default/bird-exporter
|
||||||
|
install -D -m 0644 docs/bird-exporter.1 debian/bird-exporter/usr/share/man/man1/bird-exporter.1
|
||||||
|
|
||||||
|
override_dh_auto_configure:
|
||||||
|
# Skip auto configure
|
||||||
|
|
||||||
|
override_dh_auto_test:
|
||||||
|
go test -v ./cmd/bird-exporter/
|
||||||
|
|
||||||
|
override_dh_auto_clean:
|
||||||
|
rm -f bird-exporter
|
||||||
|
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||||
|
rm -rf debian/.gocache debian/go obj-*
|
||||||
|
|
||||||
|
override_dh_dwz:
|
||||||
|
# Skip dwz compression for Go binaries
|
||||||
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.0 (native)
|
||||||
373
doc/DETAILS.md
Normal file
373
doc/DETAILS.md
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
# BIRD Exporter - Detailed Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
bird-exporter is a Prometheus exporter for the BIRD routing daemon. It parses
|
||||||
|
the output of `birdc show proto all` and exposes metrics in Prometheus format.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The exporter consists of three main components:
|
||||||
|
|
||||||
|
1. **Parser** - Parses BIRD protocol output into structured data
|
||||||
|
2. **Collector** - Implements Prometheus collector interface
|
||||||
|
3. **HTTP Server** - Serves metrics on /metrics endpoint
|
||||||
|
|
||||||
|
## Input Format
|
||||||
|
|
||||||
|
The exporter reads output from `birdc show proto all`, which provides detailed
|
||||||
|
information about all configured protocols. Each protocol block contains:
|
||||||
|
|
||||||
|
- Protocol name, type, table, state, and timestamp
|
||||||
|
- Protocol-specific information (BGP state, OSPF status, etc.)
|
||||||
|
- One or more channels (typically ipv4 and/or ipv6)
|
||||||
|
- Route statistics per channel
|
||||||
|
- Import/export statistics per channel
|
||||||
|
|
||||||
|
### Supported Protocol Types
|
||||||
|
|
||||||
|
- **Device** - Interface tracking
|
||||||
|
- **Direct** - Directly connected routes
|
||||||
|
- **Kernel** - Kernel routing table synchronization
|
||||||
|
- **Static** - Static routes
|
||||||
|
- **OSPF** - OSPF routing protocol
|
||||||
|
- **BGP** - Border Gateway Protocol
|
||||||
|
|
||||||
|
## Exported Metrics
|
||||||
|
|
||||||
|
### Protocol Status Metrics
|
||||||
|
|
||||||
|
**bird_protocol_up**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: Protocol operational status (1=up, 0=down)
|
||||||
|
- Labels: name, proto, table, info
|
||||||
|
|
||||||
|
### BGP-Specific Metrics
|
||||||
|
|
||||||
|
**bird_bgp_state**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: BGP session state (1=Established, 0=other)
|
||||||
|
- Labels: name, neighbor_address, neighbor_as, local_as, neighbor_id
|
||||||
|
|
||||||
|
**bird_bgp_hold_timer_seconds**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: Current BGP hold timer value in seconds
|
||||||
|
- Labels: name, neighbor_address
|
||||||
|
|
||||||
|
**bird_bgp_keepalive_timer_seconds**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: Current BGP keepalive timer value in seconds
|
||||||
|
- Labels: name, neighbor_address
|
||||||
|
|
||||||
|
**bird_bgp_send_hold_timer_seconds**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: Current BGP send hold timer value in seconds
|
||||||
|
- Labels: name, neighbor_address
|
||||||
|
|
||||||
|
### Route Metrics
|
||||||
|
|
||||||
|
All route metrics include labels: name, proto, channel, table
|
||||||
|
|
||||||
|
**bird_route_imported**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: Number of routes currently imported
|
||||||
|
|
||||||
|
**bird_route_exported**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: Number of routes currently exported
|
||||||
|
|
||||||
|
**bird_route_preferred**
|
||||||
|
- Type: Gauge
|
||||||
|
- Description: Number of preferred routes
|
||||||
|
|
||||||
|
### Route Change Statistics
|
||||||
|
|
||||||
|
All statistics are counters with labels: name, proto, channel, table
|
||||||
|
|
||||||
|
**Import Statistics:**
|
||||||
|
- bird_route_import_updates_total
|
||||||
|
- bird_route_import_withdraws_total
|
||||||
|
- bird_route_import_rejected_total
|
||||||
|
- bird_route_import_filtered_total
|
||||||
|
- bird_route_import_ignored_total
|
||||||
|
- bird_route_import_accepted_total
|
||||||
|
|
||||||
|
**Export Statistics:**
|
||||||
|
- bird_route_export_updates_total
|
||||||
|
- bird_route_export_withdraws_total
|
||||||
|
- bird_route_export_rejected_total
|
||||||
|
- bird_route_export_filtered_total
|
||||||
|
- bird_route_export_accepted_total
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Command-Line Flags
|
||||||
|
|
||||||
|
**-web.listen-address** (default: `:9324`)
|
||||||
|
The address and port to listen on for HTTP requests.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```sh
|
||||||
|
./bird-exporter -web.listen-address=:9100
|
||||||
|
```
|
||||||
|
|
||||||
|
**-bird.socket** (default: `/var/run/bird/bird.ctl`)
|
||||||
|
Path to the BIRD control socket for communication with the BIRD daemon.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```sh
|
||||||
|
./bird-exporter -bird.socket=/run/bird.ctl
|
||||||
|
```
|
||||||
|
|
||||||
|
**-period** (default: `60s`)
|
||||||
|
Time interval between BIRD data scrapes. The exporter caches BIRD data and
|
||||||
|
updates it periodically at this interval.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```sh
|
||||||
|
./bird-exporter -period=30s
|
||||||
|
```
|
||||||
|
|
||||||
|
**-debug** (default: `false`)
|
||||||
|
Enable debug logging. When enabled, additional log messages are printed including:
|
||||||
|
- "Performing initial scrape..."
|
||||||
|
- "BIRD: show protocols all"
|
||||||
|
- "Successfully scraped N protocols"
|
||||||
|
|
||||||
|
Without debug mode, only essential startup and operational messages are logged.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```sh
|
||||||
|
./bird-exporter -debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.21 or later
|
||||||
|
- Make (optional)
|
||||||
|
|
||||||
|
### Build Commands
|
||||||
|
|
||||||
|
Using Make:
|
||||||
|
```sh
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
Using Go directly:
|
||||||
|
```sh
|
||||||
|
go build -o bird-exporter ./cmd/bird-exporter/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Output
|
||||||
|
|
||||||
|
The binary will be created in the project root directory:
|
||||||
|
- Binary name: `bird-exporter`
|
||||||
|
- Size: ~12MB (uncompressed)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
Using Make:
|
||||||
|
```sh
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
Using Go directly:
|
||||||
|
```sh
|
||||||
|
go test -v ./cmd/bird-exporter/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
|
||||||
|
The test suite includes:
|
||||||
|
- Protocol parsing tests (Device, Direct, Kernel, Static, OSPF, BGP)
|
||||||
|
- Channel parsing tests
|
||||||
|
- Route statistics parsing tests
|
||||||
|
- BGP-specific field parsing tests
|
||||||
|
- Error handling tests (missing files, empty files)
|
||||||
|
- Integration test with real birdc output
|
||||||
|
|
||||||
|
### Test Fixtures
|
||||||
|
|
||||||
|
Test fixtures are located in `cmd/bird-exporter/testdata/`:
|
||||||
|
- `sample_output.txt` - Minimal test fixture with all protocol types
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Running Locally
|
||||||
|
|
||||||
|
For development or testing:
|
||||||
|
```sh
|
||||||
|
./bird-exporter
|
||||||
|
curl http://localhost:9324/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running in Production
|
||||||
|
|
||||||
|
The exporter communicates with BIRD via its control socket. Ensure the exporter
|
||||||
|
has permission to access the BIRD socket (typically `/var/run/bird/bird.ctl`).
|
||||||
|
|
||||||
|
**Systemd Service**
|
||||||
|
|
||||||
|
Create `/etc/systemd/system/bird-exporter.service`:
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=BIRD Exporter
|
||||||
|
After=network.target bird.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=bird
|
||||||
|
ExecStart=/usr/local/bin/bird-exporter -period=60s
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable and start the service:
|
||||||
|
```sh
|
||||||
|
systemctl enable bird-exporter
|
||||||
|
systemctl start bird-exporter
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prometheus Configuration
|
||||||
|
|
||||||
|
Add to prometheus.yml:
|
||||||
|
```yaml
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'bird'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9324']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prometheus Queries
|
||||||
|
|
||||||
|
### Example Queries
|
||||||
|
|
||||||
|
Count protocols by state:
|
||||||
|
```promql
|
||||||
|
count by (state) (bird_protocol_up)
|
||||||
|
```
|
||||||
|
|
||||||
|
Count established BGP sessions:
|
||||||
|
```promql
|
||||||
|
sum(bird_bgp_state)
|
||||||
|
```
|
||||||
|
|
||||||
|
Total imported routes by protocol type:
|
||||||
|
```promql
|
||||||
|
sum by (proto) (bird_route_imported)
|
||||||
|
```
|
||||||
|
|
||||||
|
BGP sessions with low hold timer (potential flapping):
|
||||||
|
```promql
|
||||||
|
bird_bgp_hold_timer_seconds < 30
|
||||||
|
```
|
||||||
|
|
||||||
|
Import reject rate:
|
||||||
|
```promql
|
||||||
|
rate(bird_route_import_rejected_total[5m])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
1. **BIRD v2 Only** - The parser is designed for BIRD v2 output format. BIRD
|
||||||
|
v1 compatibility is not guaranteed.
|
||||||
|
|
||||||
|
2. **Periodic Updates** - Metrics are updated at the configured scrape interval
|
||||||
|
(default 60s). Changes in BIRD state may not be immediately reflected in metrics.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Debugging Connection Issues
|
||||||
|
|
||||||
|
Enable debug logging to see detailed information about BIRD communication:
|
||||||
|
```sh
|
||||||
|
./bird-exporter -debug
|
||||||
|
```
|
||||||
|
|
||||||
|
This will show:
|
||||||
|
- When the exporter queries BIRD
|
||||||
|
- What command is being sent
|
||||||
|
- How many protocols were successfully parsed
|
||||||
|
|
||||||
|
### No Metrics Appearing
|
||||||
|
|
||||||
|
Check that the BIRD socket is accessible:
|
||||||
|
```sh
|
||||||
|
ls -la /var/run/bird/bird.ctl
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify BIRD is running:
|
||||||
|
```sh
|
||||||
|
birdc show status
|
||||||
|
```
|
||||||
|
|
||||||
|
Check exporter logs for connection errors:
|
||||||
|
```sh
|
||||||
|
journalctl -u bird-exporter -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Incorrect Metric Values
|
||||||
|
|
||||||
|
Check exporter logs for parse errors:
|
||||||
|
```sh
|
||||||
|
journalctl -u bird-exporter | grep -i error
|
||||||
|
```
|
||||||
|
|
||||||
|
### High Memory Usage
|
||||||
|
|
||||||
|
The exporter loads the entire BIRD output into memory. For routers with many
|
||||||
|
BGP sessions and large routing tables, this can consume significant memory.
|
||||||
|
Monitor memory usage and consider resource limits.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
bird-exporter/
|
||||||
|
├── cmd/
|
||||||
|
│ └── bird-exporter/
|
||||||
|
│ ├── main.go # Main program
|
||||||
|
│ ├── main_test.go # Tests
|
||||||
|
│ └── testdata/ # Test fixtures
|
||||||
|
├── doc/
|
||||||
|
│ └── DETAILS.md # This file
|
||||||
|
├── go.mod # Go module definition
|
||||||
|
├── Makefile # Build automation
|
||||||
|
└── README.md # Quick start guide
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding New Metrics
|
||||||
|
|
||||||
|
1. Define metric descriptor in `NewBirdCollector()`
|
||||||
|
2. Add metric to `Describe()` method
|
||||||
|
3. Extract data in parser if needed
|
||||||
|
4. Export metric in `Collect()` method
|
||||||
|
5. Add test coverage
|
||||||
|
|
||||||
|
### Parser Architecture
|
||||||
|
|
||||||
|
The parser uses a state machine approach:
|
||||||
|
- Tracks current protocol being parsed
|
||||||
|
- Tracks current channel within protocol
|
||||||
|
- Uses regex patterns to match and extract fields
|
||||||
|
- Handles hierarchical structure through indentation
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Additional Protocols** - Support for RIP, Babel, BFD details, etc.
|
||||||
|
2. **Neighbor Details** - More detailed BGP neighbor information
|
||||||
|
3. **Route Details** - Export information about specific routes
|
||||||
|
4. **Real-time Updates** - Push updates instead of polling
|
||||||
|
5. **Configuration Reload** - Support SIGHUP for config reload
|
||||||
|
6. **Multiple BIRD Instances** - Support monitoring multiple BIRD instances
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details
|
||||||
164
docs/bird-exporter.1
Normal file
164
docs/bird-exporter.1
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
.TH BIRD-EXPORTER 1 "December 2024" "bird-exporter 0.1.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
bird-exporter \- Prometheus exporter for BIRD routing daemon
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B bird-exporter
|
||||||
|
[\fIOPTIONS\fR]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B bird-exporter
|
||||||
|
is a Prometheus metrics exporter for the BIRD routing daemon. It connects to
|
||||||
|
BIRD's control socket and exports metrics about protocols, routes, and BGP
|
||||||
|
sessions in a format that can be scraped by Prometheus.
|
||||||
|
.PP
|
||||||
|
The exporter periodically queries BIRD for protocol information and caches the
|
||||||
|
results to minimize load on the BIRD daemon. The cache is updated at a
|
||||||
|
configurable interval (default: 60 seconds) and served to Prometheus on each
|
||||||
|
scrape request.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.B \-web.listen\-address \fIstring\fR
|
||||||
|
Address to listen on for web interface and telemetry (default: ":9324")
|
||||||
|
.TP
|
||||||
|
.B \-bird.socket \fIstring\fR
|
||||||
|
Path to BIRD control socket (default: "/var/run/bird/bird.ctl")
|
||||||
|
.TP
|
||||||
|
.B \-period \fIduration\fR
|
||||||
|
Period between scrapes of BIRD data (default: 60s)
|
||||||
|
.TP
|
||||||
|
.B \-debug
|
||||||
|
Enable debug logging (default: false)
|
||||||
|
.SH METRICS
|
||||||
|
The exporter provides the following metrics:
|
||||||
|
.TP
|
||||||
|
.B bird_protocol_up
|
||||||
|
Protocol status (1 = up, 0 = down). Labels: name, proto, table, info
|
||||||
|
.TP
|
||||||
|
.B bird_bgp_state
|
||||||
|
BGP session state (1 = Established, 0 = other). Labels: name, neighbor_address, neighbor_as, local_as, neighbor_id
|
||||||
|
.TP
|
||||||
|
.B bird_route_imported
|
||||||
|
Number of imported routes. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_filtered
|
||||||
|
Number of filtered routes. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_exported
|
||||||
|
Number of exported routes. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_preferred
|
||||||
|
Number of preferred routes. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_import_updates_total
|
||||||
|
Total number of import updates. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_import_withdraws_total
|
||||||
|
Total number of import withdraws. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_import_rejected_total
|
||||||
|
Total number of rejected imports. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_import_filtered_total
|
||||||
|
Total number of filtered imports. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_import_ignored_total
|
||||||
|
Total number of ignored imports. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_import_accepted_total
|
||||||
|
Total number of accepted imports. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_export_updates_total
|
||||||
|
Total number of export updates. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_export_withdraws_total
|
||||||
|
Total number of export withdraws. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_export_rejected_total
|
||||||
|
Total number of rejected exports. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_export_filtered_total
|
||||||
|
Total number of filtered exports. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_route_export_accepted_total
|
||||||
|
Total number of accepted exports. Labels: name, proto, channel, table
|
||||||
|
.TP
|
||||||
|
.B bird_bgp_hold_timer_seconds
|
||||||
|
BGP hold timer in seconds. Labels: name, neighbor_address
|
||||||
|
.TP
|
||||||
|
.B bird_bgp_keepalive_timer_seconds
|
||||||
|
BGP keepalive timer in seconds. Labels: name, neighbor_address
|
||||||
|
.TP
|
||||||
|
.B bird_bgp_send_hold_timer_seconds
|
||||||
|
BGP send hold timer in seconds. Labels: name, neighbor_address
|
||||||
|
.SH CONFIGURATION
|
||||||
|
Default configuration can be set in \fB/etc/default/bird-exporter\fR. The file
|
||||||
|
should contain a BIRD_EXPORTER_ARGS variable with command line arguments.
|
||||||
|
.PP
|
||||||
|
Example:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.RS
|
||||||
|
BIRD_EXPORTER_ARGS="-period=60s -bird.socket=/var/run/bird/bird.ctl"
|
||||||
|
.RE
|
||||||
|
.fi
|
||||||
|
.SH EXAMPLES
|
||||||
|
Start the exporter with default settings:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.RS
|
||||||
|
bird-exporter
|
||||||
|
.RE
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Start with custom BIRD socket path:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.RS
|
||||||
|
bird-exporter -bird.socket=/run/bird.ctl
|
||||||
|
.RE
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Start with custom scrape interval and listen address:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.RS
|
||||||
|
bird-exporter -period=30s -web.listen-address=:9999
|
||||||
|
.RE
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Start with debug logging enabled:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.RS
|
||||||
|
bird-exporter -debug
|
||||||
|
.RE
|
||||||
|
.fi
|
||||||
|
.PP
|
||||||
|
Query metrics using curl:
|
||||||
|
.PP
|
||||||
|
.nf
|
||||||
|
.RS
|
||||||
|
curl http://localhost:9324/metrics
|
||||||
|
.RE
|
||||||
|
.fi
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.B /etc/default/bird-exporter
|
||||||
|
Default configuration file
|
||||||
|
.TP
|
||||||
|
.B /var/run/bird/bird.ctl
|
||||||
|
Default BIRD control socket path
|
||||||
|
.TP
|
||||||
|
.B /lib/systemd/system/bird-exporter.service
|
||||||
|
Systemd service unit file
|
||||||
|
.SH PERMISSIONS
|
||||||
|
The exporter needs read/write access to the BIRD control socket. When running
|
||||||
|
as a systemd service, it runs as the \fBbird\fR user which should have
|
||||||
|
appropriate permissions.
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR bird (1),
|
||||||
|
.BR birdc (1),
|
||||||
|
.BR prometheus (1)
|
||||||
|
.PP
|
||||||
|
Project homepage: https://git.ipng.ch/ipng/bird-exporter
|
||||||
|
.SH AUTHOR
|
||||||
|
BIRD Exporter Authors
|
||||||
15
go.mod
Normal file
15
go.mod
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module github.com/pim/bird-exporter
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/prometheus/client_golang v1.19.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
|
golang.org/x/sys v0.16.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
|
)
|
||||||
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
|
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||||
|
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||||
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
|
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
Reference in New Issue
Block a user