├── LICENSE
├── README.md
├── bin
└── amd64
│ └── unbound-exporter
├── go.mod
├── go.sum
└── unbound-exporter.go
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## unbound exporter
2 |
3 |
4 | 
5 | 
6 | 
7 |
8 |
9 |
10 | ### Summary
11 | 🔸 Exports Unbound DNS server statistic as `Prometheus` metrics.
12 | 🔸 `unbound-exporter` is tailored for [unbound-dashboard](https://github.com/ar51an/unbound-dashboard). Dashboard release includes:
13 | ➟ Prebuilt _unbound-exporter_ `binary` for arm64.
14 | ➟ `Service` to automatically run exporter at startup.
15 | 🔸 Unbound `setup` is available at [unbound-redis](https://github.com/ar51an/unbound-redis).
16 |
17 | #### Prerequisite:
18 | * Go 1.19 or later.
19 |
20 | #
21 | ### Compile:
22 | * Copy `go.mod`, `go.sum` & `unbound-exporter.go` from this repo to local dir.
23 | * Run below cmds:
24 | > Download dependencies:
25 | > `go mod tidy`
26 |
27 | > Build:
28 | > `go build`
29 |
30 | > Reduce size:
31 | > `strip unbound-exporter`
32 |
33 | > `ℹ️` **Note:**
34 | > Make sure `.../go/bin` is in the `PATH`.
35 |
36 | #
37 | ### Usage
38 | * `unbound-exporter -h`
39 |
40 | > 
41 |
42 |
--------------------------------------------------------------------------------
/bin/amd64/unbound-exporter:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ar51an/unbound-exporter/25f963c2bf23de4fcd7910040807888b78ed3859/bin/amd64/unbound-exporter
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module unbound-exporter
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/go-kit/log v0.2.1
7 | github.com/integrii/flaggy v1.5.2
8 | github.com/prometheus/client_golang v1.14.0
9 | github.com/prometheus/common v0.39.0
10 | github.com/sirupsen/logrus v1.9.0
11 | )
12 |
13 | require (
14 | github.com/beorn7/perks v1.0.1 // indirect
15 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
16 | github.com/go-logfmt/logfmt v0.5.1 // indirect
17 | github.com/golang/protobuf v1.5.2 // indirect
18 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
19 | github.com/prometheus/client_model v0.3.0 // indirect
20 | github.com/prometheus/procfs v0.8.0 // indirect
21 | golang.org/x/sys v0.3.0 // indirect
22 | google.golang.org/protobuf v1.28.1 // indirect
23 | )
24 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
3 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
4 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
9 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
10 | github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
11 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
13 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
14 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
15 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
16 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
17 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
18 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
19 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
20 | github.com/integrii/flaggy v1.5.2 h1:bWV20MQEngo4hWhno3i5Z9ISPxLPKj9NOGNwTWb/8IQ=
21 | github.com/integrii/flaggy v1.5.2/go.mod h1:dO13u7SYuhk910nayCJ+s1DeAAGC1THCMj1uSFmwtQ8=
22 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
23 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
26 | github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
27 | github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
28 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
29 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
30 | github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
31 | github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
32 | github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
33 | github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
34 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
35 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
37 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
38 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
39 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
40 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
42 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
44 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
45 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
46 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
47 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
49 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
50 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
51 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
52 |
--------------------------------------------------------------------------------
/unbound-exporter.go:
--------------------------------------------------------------------------------
1 | /*
2 | * Prometheus Unbound Exporter
3 | * Copyright (c) 2023 - ar51an - https://github.com/ar51an/unbound-exporter
4 | * Released under Apache-2.0 license on an "AS-IS" BASIS
5 | * Do not remove above information for distribution purpose
6 | */
7 |
8 | package main
9 |
10 | import (
11 | "bufio"
12 | "bytes"
13 | "crypto/tls"
14 | "crypto/x509"
15 | "fmt"
16 | "io"
17 | "math"
18 | "net"
19 | "net/http"
20 | "os"
21 | "regexp"
22 | "strconv"
23 | "strings"
24 | "time"
25 |
26 | "github.com/integrii/flaggy"
27 | "github.com/prometheus/client_golang/prometheus"
28 | "github.com/prometheus/client_golang/prometheus/promhttp"
29 | log "github.com/sirupsen/logrus"
30 | )
31 |
32 | type UnboundCollector struct {
33 | socketType string
34 | scrapeURI string
35 | tlsConfig *tls.Config
36 | }
37 |
38 | type metric struct {
39 | desc *prometheus.Desc
40 | valueType prometheus.ValueType
41 | }
42 |
43 | const NAMESPACE = `unbound`
44 |
45 | var blockFilename = `/opt/unbound/blocklists/unbound.block.conf`
46 | var listenAddress = `0.0.0.0:9167`
47 | var metricsPath = `/metrics`
48 | var unboundURI = `tcp://localhost:8953`
49 | var unboundServerCert = `/etc/unbound/unbound_server.pem`
50 | var unboundControlCert = `/etc/unbound/unbound_control.pem`
51 | var unboundControlKey = `/etc/unbound/unbound_control.key`
52 |
53 | var blocklistMetric = initMetric(`blocklist_domain_count`, `Blocklist domain count`, nil, prometheus.GaugeValue)
54 |
55 | var multiMetrics = map[*regexp.Regexp]*metric{
56 | regexp.MustCompile(`^thread([0-9]+)\.requestlist\.current\.user$`): initMetric(`request_list_current_user`, `Request list current size`, []string{`thread`}, prometheus.GaugeValue),
57 | regexp.MustCompile(`^time\.up$`): initMetric(`time_up_seconds`, `Unbound uptime in seconds`, []string{"Uptime"}, prometheus.CounterValue),
58 | regexp.MustCompile(`^mem\.cache\.([a-z]+)$`): initMetric(`memory_caches_bytes`, `Caches memory in bytes`, []string{"cache"}, prometheus.GaugeValue),
59 | regexp.MustCompile(`^mem\.mod\.([a-z]+)$`): initMetric("memory_modules_bytes", "Modules memory in bytes", []string{"module"}, prometheus.GaugeValue),
60 | regexp.MustCompile(`^histogram\.([\d\.]+)\.to\.([\d\.]+)$`): initMetric(`response_time_buckets`, `Recursive queries count grouped into lower-upper bound lookup time`, []string{"direct", "lower", "upper"}, prometheus.CounterValue),
61 | regexp.MustCompile(`^num\.query\.type\.([A-Z0-9]+)$`): initMetric(`query_types_count`, `Number of queries with given resource record type`, []string{"type"}, prometheus.CounterValue),
62 | regexp.MustCompile(`^num\.answer\.rcode\.([A-Za-z]+)$`): initMetric(`answer_rcodes_count`, `Number of answers by response code (cache and recursive both)`, []string{"rcode"}, prometheus.CounterValue),
63 | }
64 |
65 | var flatMetrics = map[string]*metric{
66 | `total.num.queries`: initMetric(`queries_total`, `Total number of queries received`, nil, prometheus.CounterValue),
67 | `total.num.cachehits`: initMetric(`cache_hit_total`, `Total number of queries answered from cache`, nil, prometheus.CounterValue),
68 | `total.num.cachemiss`: initMetric(`cache_miss_total`, `Total number of queries that needed recursive processing`, nil, prometheus.CounterValue),
69 | `total.num.prefetch`: initMetric(`prefetch_total`, `Total number of cache prefetches performed`, nil, prometheus.CounterValue),
70 | `total.num.expired`: initMetric(`expired_total`, `Total number of replies that served an expired cache entry`, nil, prometheus.CounterValue),
71 | `total.requestlist.avg`: initMetric(`request_list_avg`, `Average requests in request list on new recursive query`, nil, prometheus.GaugeValue),
72 | `total.requestlist.max`: initMetric(`request_list_max`, `Maximum size attained by recursive request list`, nil, prometheus.CounterValue),
73 | `total.recursion.time.avg`: initMetric(`recursion_time_avg_seconds`, `Average time to answer recursive queries`, nil, prometheus.GaugeValue),
74 | `total.recursion.time.median`: initMetric(`recursion_time_median_seconds`, `Median time to answer recursive queries`, nil, prometheus.GaugeValue),
75 | `num.query.tcpout`: initMetric(`query_tcpout_count`, `Number of TCP outgoing queries`, nil, prometheus.CounterValue),
76 | `num.query.udpout`: initMetric(`query_udpout_count`, `Number of UDP outgoing queries`, nil, prometheus.CounterValue),
77 | `num.query.ipv6`: initMetric(`query_ipv6_count`, `Number of IPv6 incoming queries`, nil, prometheus.CounterValue),
78 | `num.answer.secure`: initMetric(`answer_secure_count`, `Number of answers that were secure`, nil, prometheus.CounterValue),
79 | `num.answer.bogus`: initMetric(`answer_bogus_count`, `Number of answers that were bogus`, nil, prometheus.CounterValue),
80 | `msg.cache.count`: initMetric(`msg_cache_count`, `Number of DNS replies in the message cache`, nil, prometheus.GaugeValue),
81 | `rrset.cache.count`: initMetric("rrset_cache_count", "Number of RRsets in the rrset cache", nil, prometheus.GaugeValue),
82 | `infra.cache.count`: initMetric("infra_cache_count", "Number of items in the infra cache", nil, prometheus.GaugeValue),
83 | `key.cache.count`: initMetric(`key_cache_count`, `Number of items in the key cache`, nil, prometheus.GaugeValue),
84 | }
85 |
86 | func initMetric(name string, help string, labels []string, metricType prometheus.ValueType) *metric {
87 | var m = &metric{}
88 | m.desc = prometheus.NewDesc(prometheus.BuildFQName(NAMESPACE, ``, name), help, labels, nil)
89 | m.valueType = metricType
90 | return m
91 | }
92 |
93 | func addMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, metricType prometheus.ValueType, value float64, labelValue ...string) {
94 | ch <- prometheus.MustNewConstMetric(desc, metricType, value, labelValue[0:]...)
95 | }
96 |
97 | func roundBucket(duration time.Duration) string {
98 | replacer := strings.NewReplacer(`µs`, ` µs`, `ms`, ` ms`)
99 |
100 | switch {
101 | case duration > time.Millisecond:
102 | duration = duration.Round(time.Millisecond)
103 | case duration > time.Microsecond:
104 | duration = duration.Round(time.Microsecond)
105 | }
106 | return replacer.Replace(duration.String())
107 | }
108 |
109 | func formatBucket(labelValue string) string {
110 | var bound time.Duration
111 | var formattedBound string
112 | value, _ := strconv.ParseFloat(labelValue, 64)
113 |
114 | switch {
115 | case value == 0.0:
116 | formattedBound = strconv.FormatFloat(value, 'f', -1, 64) + ` µs`
117 | case value < 1.0:
118 | _, fraction := math.Modf(value)
119 | bound = time.Duration(fraction/1e-06) * time.Microsecond
120 | formattedBound = roundBucket(bound)
121 | case value >= 1.0:
122 | formattedBound = strconv.FormatFloat(value, 'f', -1, 64) + ` s`
123 | }
124 | return formattedBound
125 | }
126 |
127 | func formatUptime(sec int64) string {
128 | var uptime string
129 | ds, sec := sec/86400, sec%86400
130 | hrs, sec := sec/3600, sec%3600
131 | mins := sec / 60
132 |
133 | if ds != 0 {
134 | uptime += strconv.FormatInt(ds, 10) + ` d `
135 | }
136 | if hrs != 0 {
137 | uptime += strconv.FormatInt(hrs, 10) + ` h `
138 | }
139 | if mins != 0 && (ds < 1 || hrs < 1) {
140 | uptime += strconv.FormatInt(mins, 10) + ` m`
141 | }
142 | return strings.TrimRight(uptime, ` `)
143 | }
144 |
145 | func bsToF(bs []byte) float64 {
146 | f, err := strconv.ParseFloat(string(bs), 64)
147 | if err != nil {
148 | log.Error(err)
149 | }
150 | return f
151 | }
152 |
153 | func scrapeStats(stream io.Reader, ch chan<- prometheus.Metric) error {
154 | scanner := bufio.NewScanner(stream)
155 | var labelValues []string
156 | var sKey string
157 | var value float64
158 | var skip bool
159 | var direct = 1.0
160 |
161 | for scanner.Scan() {
162 | bKey, bValue, found := bytes.Cut(scanner.Bytes(), []byte(`=`))
163 | if found {
164 | skip = false
165 | sKey = string(bKey)
166 | for k := range multiMetrics {
167 | if matches := k.FindStringSubmatch(sKey); matches != nil {
168 | value = bsToF(bValue)
169 | switch {
170 | case strings.Contains(sKey, `histogram`):
171 | labelValues = make([]string, 3)
172 | labelValues[0] = strconv.FormatFloat(direct, 'f', 1, 64)
173 | labelValues[1] = formatBucket(matches[1])
174 | labelValues[2] = formatBucket(matches[2])
175 | direct += 0.1
176 | case strings.Contains(sKey, `time.up`):
177 | uptime := formatUptime(int64(value))
178 | labelValues = []string{uptime}
179 | default:
180 | labelValues = matches[1:]
181 | }
182 | addMetric(ch, multiMetrics[k].desc, multiMetrics[k].valueType, value, labelValues[0:]...)
183 | labelValues = nil
184 | skip = true
185 | break
186 | }
187 | }
188 | if skip {
189 | continue
190 | }
191 | if m, ok := flatMetrics[sKey]; ok {
192 | value = bsToF(bValue)
193 | addMetric(ch, m.desc, m.valueType, value)
194 | }
195 | }
196 | }
197 | return scanner.Err()
198 | }
199 |
200 | func collectStats(collector *UnboundCollector, ch chan<- prometheus.Metric) error {
201 | var conn net.Conn
202 | var err error
203 | var stats = []byte("UBCT1 stats_noreset\n")
204 |
205 | switch collector.socketType {
206 | case "unix":
207 | conn, err = net.Dial(collector.socketType, collector.scrapeURI)
208 | case "tcp":
209 | conn, err = tls.Dial(collector.socketType, collector.scrapeURI, collector.tlsConfig)
210 | default:
211 | err = fmt.Errorf("invalid socket type")
212 | }
213 | if err != nil {
214 | return err
215 | }
216 | defer conn.Close()
217 | _, err = conn.Write(stats)
218 | if err != nil {
219 | return err
220 | }
221 | return scrapeStats(conn, ch)
222 | }
223 |
224 | func collectBlocklist(ch chan<- prometheus.Metric) {
225 | var count = 0
226 | var err error
227 | var numBytes int
228 | var isLocalData = false
229 | var pattern = []byte("\n")
230 |
231 | blocklist, fileErr := os.Open(blockFilename)
232 | if fileErr != nil {
233 | log.Error("blocklist not found: ", fileErr)
234 | return
235 | }
236 | buffer := make([]byte, bufio.MaxScanTokenSize)
237 | for {
238 | numBytes, err = blocklist.Read(buffer)
239 | count += bytes.Count(buffer[:numBytes], pattern)
240 | if err == io.EOF {
241 | break
242 | }
243 | }
244 | if bytes.Contains(buffer, []byte(`local-data:`)) {
245 | isLocalData = true
246 | }
247 | count -= 1
248 | if isLocalData {
249 | count /= 2
250 | }
251 | addMetric(ch, blocklistMetric.desc, blocklistMetric.valueType, float64(count))
252 | buffer = nil
253 | blocklist.Close()
254 | }
255 |
256 | func (collector *UnboundCollector) Collect(ch chan<- prometheus.Metric) {
257 | collectBlocklist(ch)
258 | err := collectStats(collector, ch)
259 | if err != nil {
260 | log.Error("failed to scrape unbound statistics: ", err)
261 | }
262 | }
263 |
264 | func (collector *UnboundCollector) Describe(ch chan<- *prometheus.Desc) {
265 | for _, m := range multiMetrics {
266 | ch <- m.desc
267 | }
268 | for _, m := range flatMetrics {
269 | ch <- m.desc
270 | }
271 | ch <- blocklistMetric.desc
272 | }
273 |
274 | func initCollector() (*UnboundCollector, error) {
275 | var collector = &UnboundCollector{}
276 | var err error
277 |
278 | parsedAddr := strings.Split(unboundURI, `://`)
279 | if len(parsedAddr) != 2 {
280 | return collector, fmt.Errorf("invalid unbound socket uri format")
281 | }
282 | scheme := parsedAddr[0]
283 | address := parsedAddr[1]
284 | switch scheme {
285 | case "unix":
286 | collector.socketType = scheme
287 | collector.scrapeURI = `/` + address
288 | return collector, nil
289 | case "tcp":
290 | serverCert, err := os.ReadFile(unboundServerCert)
291 | if err != nil {
292 | return collector, err
293 | }
294 | serverCertPool := x509.NewCertPool()
295 | if !serverCertPool.AppendCertsFromPEM(serverCert) {
296 | return collector, fmt.Errorf("failed to parse unbound server certificate")
297 | }
298 | controlCert, err := os.ReadFile(unboundControlCert)
299 | if err != nil {
300 | return collector, err
301 | }
302 | controlKey, err := os.ReadFile(unboundControlKey)
303 | if err != nil {
304 | return collector, err
305 | }
306 | keyPair, err := tls.X509KeyPair(controlCert, controlKey)
307 | if err != nil {
308 | return collector, err
309 | }
310 | collector.socketType = scheme
311 | collector.scrapeURI = address
312 | collector.tlsConfig = &tls.Config{Certificates: []tls.Certificate{keyPair}, RootCAs: serverCertPool, ServerName: `unbound`}
313 | return collector, nil
314 | default:
315 | err = fmt.Errorf("invalid socket type")
316 | }
317 | return collector, err
318 | }
319 |
320 | func init() {
321 | flaggy.DefaultParser.DisableShowVersionWithVersion()
322 | flaggy.String(&blockFilename, `b`, `block-file`, `Unbound blocklist file.`)
323 | flaggy.String(&listenAddress, `a`, `web.listen-address`, `Address to listen on for web interface.`)
324 | flaggy.String(&metricsPath, `p`, `web.metrics-path`, `Path under which to expose metrics.`)
325 | flaggy.String(&unboundURI, `u`, `unbound.uri`, `Unix/TCP unbound socket path for scraping.`)
326 | flaggy.String(&unboundServerCert, `s`, `unbound.server-cert`, `Unbound server certificate.`)
327 | flaggy.String(&unboundControlCert, `c`, `unbound.control-cert`, `Unbound control certificate.`)
328 | flaggy.String(&unboundControlKey, `k`, `unbound.control-key`, `Unbound control private key.`)
329 | }
330 |
331 | func main() {
332 | flaggy.Parse()
333 | collector, err := initCollector()
334 | if err != nil {
335 | log.Fatal(err)
336 | }
337 | registry := prometheus.NewRegistry()
338 | registry.MustRegister(collector)
339 | http.Handle(metricsPath, promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
340 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
341 | _, _ = w.Write([]byte(`
342 |
343 |
344 | Unbound Exporter
345 |
346 | Unbound Metrics
347 | Metrics
348 |
349 | `))
350 | })
351 | log.Info("Providing metrics at ", listenAddress, metricsPath)
352 | log.Fatal(http.ListenAndServe(listenAddress, nil))
353 | os.Exit(1)
354 | }
355 |
--------------------------------------------------------------------------------