From f22a7e4a2e7ef7b151afc5d438db281a7c4aeed4 Mon Sep 17 00:00:00 2001 From: Dmytro Bondar Date: Sun, 29 Sep 2024 22:10:50 +0200 Subject: [PATCH] feat: Metrics for Prometheus (#309) * feat: prometheus metrics * Added Prometheus resources support to helm chart --- Dockerfile | 4 +- README.md | 46 +++++++- cmd/wg-portal/main.go | 14 ++- deploy/helm/Chart.yaml | 2 +- deploy/helm/README.md | 14 ++- deploy/helm/templates/_helpers.tpl | 20 ++++ deploy/helm/templates/_pod.tpl | 9 +- deploy/helm/templates/monitoring.yaml | 41 +++++++ deploy/helm/templates/secret.yaml | 9 +- deploy/helm/templates/service.yaml | 6 + deploy/helm/values.yaml | 28 +++++ go.mod | 11 ++ go.sum | 16 +++ internal/adapters/metrics.go | 161 ++++++++++++++++++++++++++ internal/app/api/core/server.go | 3 +- internal/app/wireguard/repos.go | 7 ++ internal/app/wireguard/statistics.go | 21 +++- internal/config/config.go | 4 +- internal/util.go | 7 ++ 19 files changed, 398 insertions(+), 25 deletions(-) create mode 100644 deploy/helm/templates/monitoring.yaml create mode 100644 internal/adapters/metrics.go diff --git a/Dockerfile b/Dockerfile index 1dc9444c..e491718f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,8 +59,10 @@ ENV TZ=UTC COPY --from=builder /build/dist/wg-portal /app/wg-portal # Set the Current Working Directory inside the container WORKDIR /app -# by default, the web-portal is reachable on port 8888 +# Expose default ports for metrics, web and wireguard +EXPOSE 8787/tcp EXPOSE 8888/tcp +EXPOSE 51820/udp # the database and config file can be mounted from the host VOLUME [ "/app/data", "/app/config" ] # Command to run the executable diff --git a/README.md b/README.md index f104e9c0..9c241e94 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post * Support for multiple WireGuard interfaces * Peer Expiry Feature * Handle route and DNS settings like wg-quick does + * Exposes Prometheus [metrics](#metrics) * ~~REST API for management and client deployment~~ (coming soon) ![Screenshot](screenshot.png) @@ -79,10 +80,11 @@ The following configuration options are available: | ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. | | ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). | | ping_check_interval | statistics | 1m | The interval time between two ping check runs. | -| data_collection_interval | statistics | 10m | The interval between the data collection cycles. | +| data_collection_interval | statistics | 1m | The interval between the data collection cycles. | | collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. | | collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. | | collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. | +| listening_address | statistics | :8787 | The listening address of the Prometheus metric server. | | host | mail | 127.0.0.1 | The mail-server address. | | port | mail | 25 | The mail-server SMTP port. | | encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. | @@ -204,6 +206,48 @@ make build * [Bootstrap](https://getbootstrap.com/), for the HTML templates * [Vue.JS](https://vuejs.org/), for the frontend +## Metrics + +Metrics are available if interface/peer statistic data collection is enabled. + +Add following scrape job to your Prometheus config file: + +```yaml +# prometheus.yaml +scrape_configs: + - job_name: "wg-portal" + scrape_interval: 60s + static_configs: + - targets: ["wg-portal:8787"] +``` + +Exposed metrics: + +```console +# HELP wireguard_interface_info Interface info. +# TYPE wireguard_interface_info gauge + +# HELP wireguard_interface_received_bytes_total Bytes received througth the interface. +# TYPE wireguard_interface_received_bytes_total gauge + +# HELP wireguard_interface_sent_bytes_total Bytes sent through the interface. +# TYPE wireguard_interface_sent_bytes_total gauge + +# HELP wireguard_peer_info Peer info. +# TYPE wireguard_peer_info gauge + +# HELP wireguard_peer_received_bytes_total Bytes received from the peer. +# TYPE wireguard_peer_received_bytes_total gauge + +# HELP wireguard_peer_sent_bytes_total Bytes sent to the peer. +# TYPE wireguard_peer_sent_bytes_total gauge + +# HELP wireguard_peer_up Peer connection state (boolean: 1/0). +# TYPE wireguard_peer_up gauge + +# HELP wireguard_peer_last_handshake_seconds Seconds from the last handshake with the peer. +# TYPE wireguard_peer_last_handshake_seconds gauge +``` ## License diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index f56a64c4..65713c1a 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -2,6 +2,11 @@ package main import ( "context" + "os" + "strings" + "syscall" + "time" + "github.com/h44z/wg-portal/internal/app/api/core" handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers" "github.com/h44z/wg-portal/internal/app/audit" @@ -11,10 +16,6 @@ import ( "github.com/h44z/wg-portal/internal/app/route" "github.com/h44z/wg-portal/internal/app/users" "github.com/h44z/wg-portal/internal/app/wireguard" - "os" - "strings" - "syscall" - "time" "github.com/h44z/wg-portal/internal" "github.com/h44z/wg-portal/internal/adapters" @@ -49,6 +50,8 @@ func main() { mailer := adapters.NewSmtpMailRepo(cfg.Mail) + metricsServer := adapters.NewMetricsServer(cfg, database) + cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath) internal.AssertNoError(err) @@ -75,7 +78,7 @@ func main() { wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database) internal.AssertNoError(err) - statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard) + statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard, metricsServer) internal.AssertNoError(err) cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem) @@ -103,6 +106,7 @@ func main() { webSrv, err := core.NewServer(cfg, apiFrontend) internal.AssertNoError(err) + go metricsServer.Run(ctx) go webSrv.Run(ctx, cfg.Web.ListeningAddress) // wait until context gets cancelled diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml index 2f2bb872..a7abd35a 100644 --- a/deploy/helm/Chart.yaml +++ b/deploy/helm/Chart.yaml @@ -16,7 +16,7 @@ annotations: # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 +version: 0.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/deploy/helm/README.md b/deploy/helm/README.md index 8cadb518..59149600 100644 --- a/deploy/helm/README.md +++ b/deploy/helm/README.md @@ -1,6 +1,6 @@ # wg-portal -![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square) +![Version: 0.3.0](https://img.shields.io/badge/Version-0.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: latest](https://img.shields.io/badge/AppVersion-latest-informational?style=flat-square) WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication @@ -76,6 +76,7 @@ The [Values](#values) section lists the parameters that can be configured during | service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service | | service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type | | service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. | +| service.metrics.port | int | `8787` | | | ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created | | ingress.className | string | `""` | Ingress class name | | ingress.annotations | object | `{}` | Ingress annotations | @@ -104,3 +105,14 @@ The [Values](#values) section lists the parameters that can be configured during | serviceAccount.annotations | object | `{}` | Service account annotations | | serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials | | serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | +| monitoring.enabled | bool | `true` | Enable Prometheus monitoring. | +| monitoring.apiVersion | string | `"monitoring.coreos.com/v1"` | API version of the Prometheus resource. Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus. | +| monitoring.kind | string | `"PodMonitor"` | Kind of the Prometheus resource. Could be `PodMonitor` or `ServiceMonitor`. | +| monitoring.labels | object | `{}` | Resource labels. | +| monitoring.annotations | object | `{}` | Resource annotations. | +| monitoring.interval | string | `""` | Interval at which metrics should be scraped. If not specified Prometheus' global scrape interval is used. | +| monitoring.metricRelabelings | list | `[]` | Relabelings to samples before ingestion. | +| monitoring.relabelings | list | `[]` | Relabelings to samples before scraping. | +| monitoring.scrapeTimeout | string | `""` | Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. | +| monitoring.jobLabel | string | `""` | The label to use to retrieve the job name from. | +| monitoring.podTargetLabels | object | `{}` | Transfers labels on the Kubernetes Pod onto the target. | diff --git a/deploy/helm/templates/_helpers.tpl b/deploy/helm/templates/_helpers.tpl index a8c9ddeb..71f88747 100644 --- a/deploy/helm/templates/_helpers.tpl +++ b/deploy/helm/templates/_helpers.tpl @@ -107,3 +107,23 @@ Define hostname {{- (urlParse (tpl .Values.config.web.external_url .)).hostname -}} {{- end -}} {{- end -}} + + +{{/* +wg-portal.util.merge will merge two YAML templates or dict with template and output the result. +This takes an array of three values: +- the top context +- the template name or dict of the overrides (destination) +- the template name of the base (source) +{{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") }} +{{- include "wg-portal.util.merge" (list $ "wg-portal.destTemplate" "wg-portal.sourceTemplate") }} +*/}} +{{- define "wg-portal.util.merge" -}} +{{- $top := first . -}} +{{- $overrides := index . 1 -}} +{{- $base := fromYaml (include (index . 2) $top) | default (dict) -}} +{{- if kindIs "string" $overrides -}} + {{- $overrides = fromYaml (include $overrides $top) | default (dict) -}} +{{- end -}} +{{- toYaml (merge $overrides $base) -}} +{{- end -}} diff --git a/deploy/helm/templates/_pod.tpl b/deploy/helm/templates/_pod.tpl index 99bd6c2a..ad6fd473 100644 --- a/deploy/helm/templates/_pod.tpl +++ b/deploy/helm/templates/_pod.tpl @@ -6,11 +6,7 @@ metadata: {{- with .Values.podAnnotations }} {{- tpl (toYaml .) $ | nindent 4 }} {{- end }} - labels: - {{- include "wg-portal.selectorLabels" . | nindent 4 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 4 }} - {{- end }} + labels: {{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") | nindent 4 }} spec: {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 4 }} @@ -36,6 +32,9 @@ spec: envFrom: {{- tpl (toYaml .) $ | nindent 8 }} {{- end }} ports: + - name: metrics + containerPort: {{ .Values.service.metrics.port}} + protocol: TCP - name: web containerPort: {{ .Values.service.web.port }} protocol: TCP diff --git a/deploy/helm/templates/monitoring.yaml b/deploy/helm/templates/monitoring.yaml new file mode 100644 index 00000000..2d6e3ceb --- /dev/null +++ b/deploy/helm/templates/monitoring.yaml @@ -0,0 +1,41 @@ +{{- with .Values.monitoring -}} +{{- if and .enabled ($.Capabilities.APIVersions.Has .apiVersion) -}} +{{- $endpointsKey := (eq .kind "PodMonitor") | ternary "podMetricsEndpoints" "endpoints" -}} +apiVersion: {{ .apiVersion }} +kind: {{ .kind }} +metadata: + {{- with .annotations }} + annotations: {{- toYaml . | nindent 4 }} + {{- end }} + labels: {{- include "wg-portal.util.merge" (list $ .labels "wg-portal.labels") | nindent 4 }} + name: {{ include "wg-portal.fullname" $ }} +spec: + namespaceSelector: + matchNames: + - {{ $.Release.Namespace }} + selector: + matchLabels: + {{- include "wg-portal.selectorLabels" $ | nindent 6 }} + {{ $endpointsKey }}: + - port: metrics + path: /metrics + {{- with .interval }} + interval: {{ . }} + {{- end }} + {{- with .metricRelabelings }} + metricRelabelings: {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .relabelings }} + relabelings: {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + {{- with .jobLabel }} + jobLabel: {{ . }} + {{- end }} + {{- with .podTargetLabels }} + podTargetLabels: {{- toYaml . | nindent 2 }} + {{- end }} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/templates/secret.yaml b/deploy/helm/templates/secret.yaml index 93d27195..93daa0bf 100644 --- a/deploy/helm/templates/secret.yaml +++ b/deploy/helm/templates/secret.yaml @@ -27,9 +27,12 @@ stringData: mail: {{- tpl (toYaml .) $ | nindent 6 }} {{- end }} - {{- with .Values.config.statistics }} - statistics: {{- tpl (toYaml .) $ | nindent 6 }} - {{- end }} + statistics: + listening_address: :{{ .Values.service.metrics.port }} + {{- with .Values.config.statistics }} + {{- tpl (toYaml (omit . "listening_address")) $ | nindent 6 }} + {{- end }} + web: listening_address: :{{ .Values.service.web.port }} {{- with .Values.config.web }} diff --git a/deploy/helm/templates/service.yaml b/deploy/helm/templates/service.yaml index 1b38d3fa..808f9902 100644 --- a/deploy/helm/templates/service.yaml +++ b/deploy/helm/templates/service.yaml @@ -12,3 +12,9 @@ --- {{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.wireguard "ports" $ports "name" "wireguard") }} {{- end -}} + +{{- if and .Values.monitoring.enabled (eq .Values.monitoring.kind "ServiceMonitor") }} +--- +{{- $portsMetrics := list (dict "name" "metrics" "port" .Values.service.metrics.port "protocol" "TCP" "targetPort" "metrics") -}} +{{- include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.metrics "ports" $portsWeb "name" "metrics") }} +{{- end -}} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index c1d4b900..0cff1b49 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -134,6 +134,8 @@ service: # Increment next port by 1 for each additional interface. ports: - 51820 + metrics: + port: 8787 ingress: # -- Specifies whether an ingress resource should be created @@ -202,3 +204,29 @@ serviceAccount: # -- The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: '' + +monitoring: + # -- Enable Prometheus monitoring. + enabled: true + # -- API version of the Prometheus resource. + # Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus. + apiVersion: monitoring.coreos.com/v1 + # -- Kind of the Prometheus resource. + # Could be `PodMonitor` or `ServiceMonitor`. + kind: PodMonitor + # -- Resource labels. + labels: {} + # -- Resource annotations. + annotations: {} + # -- Interval at which metrics should be scraped. If not specified Prometheus' global scrape interval is used. + interval: '' + # -- Relabelings to samples before ingestion. + metricRelabelings: [] + # -- Relabelings to samples before scraping. + relabelings: [] + # -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. + scrapeTimeout: '' + # -- The label to use to retrieve the job name from. + jobLabel: '' + # -- Transfers labels on the Kubernetes Pod onto the target. + podTargetLabels: {} diff --git a/go.mod b/go.mod index 39dce3be..66fba6a7 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-ldap/ldap/v3 v3.4.8 github.com/prometheus-community/pro-bing v0.4.1 + github.com/prometheus/client_golang v1.20.4 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/swaggo/swag v1.16.3 @@ -31,6 +32,16 @@ require ( gorm.io/gorm v1.25.12 ) +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect +) + require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect diff --git a/go.sum b/go.sum index 2bb0d212..b409af22 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +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/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= @@ -40,6 +42,8 @@ github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKz github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -173,6 +177,8 @@ github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -213,6 +219,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -224,6 +232,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.1 h1:aMaJwyifHZO0y+h8+icUz0xbToHbia0wdmzdVZ+Kl3w= github.com/prometheus-community/pro-bing v0.4.1/go.mod h1:aLsw+zqCaDoa2RLVVSX3+UiCkBBXTMtZC3c7EkfWnAE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= diff --git a/internal/adapters/metrics.go b/internal/adapters/metrics.go new file mode 100644 index 00000000..0dfe4a87 --- /dev/null +++ b/internal/adapters/metrics.go @@ -0,0 +1,161 @@ +package adapters + +import ( + "context" + "net/http" + "time" + + "github.com/h44z/wg-portal/internal" + "github.com/h44z/wg-portal/internal/config" + "github.com/h44z/wg-portal/internal/domain" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/sirupsen/logrus" +) + +type MetricsServer struct { + *http.Server + db *SqlRepo + + ifaceInfo *prometheus.GaugeVec + ifaceReceivedBytesTotal *prometheus.GaugeVec + ifaceSendBytesTotal *prometheus.GaugeVec + peerInfo *prometheus.GaugeVec + peerIsConnected *prometheus.GaugeVec + peerLastHandshakeSeconds *prometheus.GaugeVec + peerReceivedBytesTotal *prometheus.GaugeVec + peerSendBytesTotal *prometheus.GaugeVec +} + +// Wireguard metrics labels +var ( + labels = []string{"interface"} + ifaceLabels = []string{} + peerLabels = []string{"addresses", "id", "name"} +) + +// NewMetricsServer returns a new prometheus server +func NewMetricsServer(cfg *config.Config, db *SqlRepo) *MetricsServer { + reg := prometheus.NewRegistry() + + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg})) + + return &MetricsServer{ + Server: &http.Server{ + Addr: cfg.Statistics.ListeningAddress, + Handler: mux, + }, + db: db, + + ifaceInfo: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_interface_info", + Help: "Interface info.", + }, append(labels, ifaceLabels...), + ), + ifaceReceivedBytesTotal: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_interface_received_bytes_total", + Help: "Bytes received througth the interface.", + }, append(labels, ifaceLabels...), + ), + ifaceSendBytesTotal: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_interface_sent_bytes_total", + Help: "Bytes sent through the interface.", + }, append(labels, ifaceLabels...), + ), + + peerInfo: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_peer_info", + Help: "Peer info.", + }, append(labels, peerLabels...), + ), + peerIsConnected: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_peer_up", + Help: "Peer connection state (boolean: 1/0).", + }, append(labels, peerLabels...), + ), + peerLastHandshakeSeconds: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_peer_last_handshake_seconds", + Help: "Seconds from the last handshake with the peer.", + }, append(labels, peerLabels...), + ), + peerReceivedBytesTotal: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_peer_received_bytes_total", + Help: "Bytes received from the peer.", + }, append(labels, peerLabels...), + ), + peerSendBytesTotal: promauto.With(reg).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wireguard_peer_sent_bytes_total", + Help: "Bytes sent to the peer.", + }, append(labels, peerLabels...), + ), + } +} + +// Run starts the metrics server +func (m *MetricsServer) Run(ctx context.Context) { + // Run the metrics server in a goroutine + go func() { + if err := m.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logrus.Errorf("metrics service on %s exited: %v", m.Addr, err) + } + }() + + logrus.Infof("started metrics service on %s", m.Addr) + + // Wait for the context to be done + <-ctx.Done() + + // Create a context with timeout for the shutdown process + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Attempt to gracefully shutdown the metrics server + if err := m.Shutdown(shutdownCtx); err != nil { + logrus.Errorf("metrics service on %s shutdown failed: %v", m.Addr, err) + } else { + logrus.Infof("metrics service on %s shutdown gracefully", m.Addr) + } +} + +// UpdateInterfaceMetrics updates the metrics for the given interface +func (m *MetricsServer) UpdateInterfaceMetrics(status domain.InterfaceStatus) { + labels := []string{string(status.InterfaceId)} + m.ifaceInfo.WithLabelValues(labels...).Set(1) + m.ifaceReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived)) + m.ifaceSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted)) +} + +// UpdatePeerMetrics updates the metrics for the given peer +func (m *MetricsServer) UpdatePeerMetrics(ctx context.Context, status domain.PeerStatus) { + // Fetch peer data from the database + peer, err := m.db.GetPeer(ctx, status.PeerId) + if err != nil { + logrus.Warnf("failed to fetch peer data for labels %s: %v", status.PeerId, err) + return + } + + labels := []string{ + string(peer.InterfaceIdentifier), + string(peer.Interface.AddressStr()), + string(status.PeerId), + string(peer.DisplayName), + } + + m.peerInfo.WithLabelValues(labels...).Set(1) + if status.LastHandshake != nil { + m.peerLastHandshakeSeconds.WithLabelValues(labels...).Set(float64(status.LastHandshake.Unix())) + } + m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived)) + m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted)) + m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected())) +} diff --git a/internal/app/api/core/server.go b/internal/app/api/core/server.go index 2c1f3789..e16cf680 100644 --- a/internal/app/api/core/server.go +++ b/internal/app/api/core/server.go @@ -95,8 +95,6 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server, } func (s *Server) Run(ctx context.Context, listenAddress string) { - logrus.Infof("starting web service on %s", listenAddress) - // Run web service srv := &http.Server{ Addr: listenAddress, @@ -116,6 +114,7 @@ func (s *Server) Run(ctx context.Context, listenAddress string) { cancelFn() } }() + logrus.Infof("started web service on %s", listenAddress) // Wait for the main context to end <-srvContext.Done() diff --git a/internal/app/wireguard/repos.go b/internal/app/wireguard/repos.go index 3ae2ab11..320f8be9 100644 --- a/internal/app/wireguard/repos.go +++ b/internal/app/wireguard/repos.go @@ -2,6 +2,7 @@ package wireguard import ( "context" + "github.com/h44z/wg-portal/internal/domain" ) @@ -27,6 +28,7 @@ type InterfaceAndPeerDatabaseRepo interface { type StatisticsDatabaseRepo interface { GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) + GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) UpdatePeerStatus(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.PeerStatus) (*domain.PeerStatus, error)) error UpdateInterfaceStatus(ctx context.Context, id domain.InterfaceIdentifier, updateFunc func(in *domain.InterfaceStatus) (*domain.InterfaceStatus, error)) error @@ -48,3 +50,8 @@ type WgQuickController interface { SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error UnsetDNS(id domain.InterfaceIdentifier) error } + +type MetricsServer interface { + UpdateInterfaceMetrics(status domain.InterfaceStatus) + UpdatePeerMetrics(ctx context.Context, status domain.PeerStatus) +} diff --git a/internal/app/wireguard/statistics.go b/internal/app/wireguard/statistics.go index e4ab471a..bfc40a7a 100644 --- a/internal/app/wireguard/statistics.go +++ b/internal/app/wireguard/statistics.go @@ -2,12 +2,13 @@ package wireguard import ( "context" + "sync" + "time" + "github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/domain" - "github.com/prometheus-community/pro-bing" + probing "github.com/prometheus-community/pro-bing" "github.com/sirupsen/logrus" - "sync" - "time" ) type StatisticsCollector struct { @@ -18,14 +19,16 @@ type StatisticsCollector struct { db StatisticsDatabaseRepo wg InterfaceController + ms MetricsServer } -func NewStatisticsCollector(cfg *config.Config, db StatisticsDatabaseRepo, wg InterfaceController) (*StatisticsCollector, error) { +func NewStatisticsCollector(cfg *config.Config, db StatisticsDatabaseRepo, wg InterfaceController, ms MetricsServer) (*StatisticsCollector, error) { return &StatisticsCollector{ cfg: cfg, db: db, wg: wg, + ms: ms, }, nil } @@ -70,11 +73,15 @@ func (c *StatisticsCollector) collectInterfaceData(ctx context.Context) { i.UpdatedAt = time.Now() i.BytesReceived = physicalInterface.BytesDownload i.BytesTransmitted = physicalInterface.BytesUpload + + // Update prometheus metrics + go c.ms.UpdateInterfaceMetrics(*i) return i, nil }) if err != nil { logrus.Warnf("failed to update interface status for %s: %v", in.Identifier, err) } + logrus.Tracef("updated interface status for %s", in.Identifier) } } } @@ -126,11 +133,15 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) { p.Endpoint = peer.Endpoint p.LastHandshake = lastHandshake + // Update prometheus metrics + go c.ms.UpdatePeerMetrics(ctx, *p) + return p, nil }) if err != nil { logrus.Warnf("failed to update interface status for %s: %v", in.Identifier, err) } + logrus.Tracef("updated peer status for %s", peer.Identifier) } } } @@ -234,7 +245,7 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) { } func (c *StatisticsCollector) isPeerPingable(ctx context.Context, peer domain.Peer) bool { - if c.cfg.Statistics.UsePingChecks == false { + if !c.cfg.Statistics.UsePingChecks { return false } diff --git a/internal/config/config.go b/internal/config/config.go index e8653e0c..607e8c09 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -48,6 +48,7 @@ type Config struct { CollectInterfaceData bool `yaml:"collect_interface_data"` CollectPeerData bool `yaml:"collect_peer_data"` CollectAuditData bool `yaml:"collect_audit_data"` + ListeningAddress string `yaml:"listening_address"` } `yaml:"statistics"` Mail MailConfig `yaml:"mail"` @@ -117,10 +118,11 @@ func defaultConfig() *Config { cfg.Statistics.PingCheckWorkers = 10 cfg.Statistics.PingUnprivileged = false cfg.Statistics.PingCheckInterval = 1 * time.Minute - cfg.Statistics.DataCollectionInterval = 10 * time.Second + cfg.Statistics.DataCollectionInterval = 1 * time.Minute cfg.Statistics.CollectInterfaceData = true cfg.Statistics.CollectPeerData = true cfg.Statistics.CollectAuditData = true + cfg.Statistics.ListeningAddress = ":8787" cfg.Mail = MailConfig{ Host: "127.0.0.1", diff --git a/internal/util.go b/internal/util.go index 4e8548ce..ae1e2017 100644 --- a/internal/util.go +++ b/internal/util.go @@ -126,3 +126,10 @@ func TruncateString(s string, max int) string { } return s[:max] } + +func BoolToFloat64(b bool) float64 { + if b { + return 1.0 + } + return 0.0 +}