From 7805497e43cf19854538cc2d6a57c3e132da0132 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:56:59 +0000 Subject: [PATCH 1/2] Initial plan From eebbe7dedbf50740aeba08420a46cc4a98e874ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:06:24 +0000 Subject: [PATCH 2/2] Add Prometheus metrics endpoint and Grafana dashboard Agent-Logs-Url: https://github.com/rorpage/gee-bee/sessions/1788ae6d-ed09-4815-ae4f-7316667b0251 Co-authored-by: rorpage <1423093+rorpage@users.noreply.github.com> --- Dockerfile | 3 + README.md | 27 ++ cmd/geebee/main.go | 11 + config.example.yml | 1 + docker-compose.yml | 34 ++ go.mod | 13 +- go.sum | 47 ++- grafana/provisioning/dashboards/dashboard.yml | 8 + grafana/provisioning/dashboards/gee-bee.json | 374 ++++++++++++++++++ .../provisioning/datasources/prometheus.yml | 8 + internal/configuration/configuration.go | 11 + internal/geebee/geebee.go | 3 + internal/metrics/metrics.go | 58 +++ prometheus.yml | 9 + 14 files changed, 594 insertions(+), 13 deletions(-) create mode 100644 docker-compose.yml create mode 100644 grafana/provisioning/dashboards/dashboard.yml create mode 100644 grafana/provisioning/dashboards/gee-bee.json create mode 100644 grafana/provisioning/datasources/prometheus.yml create mode 100644 internal/metrics/metrics.go create mode 100644 prometheus.yml diff --git a/Dockerfile b/Dockerfile index 37c6ff7..617b0df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,10 @@ ENV CUSTOM_WEBHOOK_URL="" ENV DISCORD_WEBHOOK_URL="" ENV FETCH_INTERVAL=60 ENV LOG_PLANES_TO_CONSOLE=true +ENV METRICS_PORT=9090 ENV SLACK_WEBHOOK_URL="" ENV TAIL_NUMBERS="28000,29000" +EXPOSE 9090 + CMD ["./gee-bee"] diff --git a/README.md b/README.md index 3458aac..8c92707 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,30 @@ An app that alerts you if it detects airplane tail numbers in flight! - `run`: run the app For instance, `task run` will run the app and pull config values from a `config.yml`, `config.yaml`, or environment variables. + +# Prometheus metrics + +Gee Bee exposes a Prometheus metrics endpoint at `http://localhost:9090/metrics` (port configurable via `metricsPort` / `METRICS_PORT`). + +| Metric | Type | Description | +|---|---|---| +| `geebee_aircraft_spotted_total` | Counter | Total aircraft spotted, labelled by `tail_number` | +| `geebee_api_calls_total` | Counter | Total calls made to the upstream ADS-B API | +| `geebee_api_errors_total` | Counter | Total errors returned by the upstream ADS-B API | +| `geebee_notifications_sent_total` | Counter | Total notifications sent, labelled by `channel` (`discord`, `slack`, `custom_url`, `terminal`) | + +# Grafana dashboard + +A pre-built Grafana dashboard is included and can be imported manually or provisioned automatically. + +## Automatic provisioning (Docker Compose) + +```bash +docker compose up +``` + +This starts Gee Bee, Prometheus (port 9091), and Grafana (port 3000). The dashboard is automatically provisioned and available at [http://localhost:3000](http://localhost:3000). + +## Manual import + +Import `grafana/provisioning/dashboards/gee-bee.json` into your Grafana instance. diff --git a/cmd/geebee/main.go b/cmd/geebee/main.go index 0290912..f6bab1f 100644 --- a/cmd/geebee/main.go +++ b/cmd/geebee/main.go @@ -3,6 +3,7 @@ package main import ( "geebee/internal/configuration" "geebee/internal/geebee" + "geebee/internal/metrics" "geebee/internal/notification" "log" "time" @@ -29,12 +30,17 @@ func sendNotifications(aircraft []geebee.AircraftOutput) error { return nil } + for _, ac := range aircraft { + metrics.AircraftSpottedTotal.WithLabelValues(ac.Registration).Inc() + } + // Custom URL if configuration.CustomWebhookUrl != "" { err := notification.SendCustomUrlMessage(aircraft) if err != nil { return err } + metrics.NotificationsSentTotal.WithLabelValues("custom_url").Inc() } // Discord @@ -43,6 +49,7 @@ func sendNotifications(aircraft []geebee.AircraftOutput) error { if err != nil { return err } + metrics.NotificationsSentTotal.WithLabelValues("discord").Inc() } // Slack @@ -51,10 +58,12 @@ func sendNotifications(aircraft []geebee.AircraftOutput) error { if err != nil { return err } + metrics.NotificationsSentTotal.WithLabelValues("slack").Inc() } // Terminal notification.SendTerminalMessage(aircraft) + metrics.NotificationsSentTotal.WithLabelValues("terminal").Inc() return nil } @@ -74,5 +83,7 @@ func HandleGeeBee() { func main() { configuration.GetConfig() + metrics.StartServer(configuration.MetricsPort) + HandleGeeBee() } diff --git a/config.example.yml b/config.example.yml index 5190ada..b7eea29 100644 --- a/config.example.yml +++ b/config.example.yml @@ -2,5 +2,6 @@ customWebhookUrl: https://your-custom-url.com/endpoint discordWebhookUrl: https://discord.com/api/webhooks/12345/abcdef-ghijkl fetchInterval: 60 logPlanesToConsole: true +metricsPort: 9090 slackWebhookUrl: https://hooks.slack.com/services/random/ids/andstrings tailNumbers: 28000,29000 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..baf85b2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + gee-bee: + build: . + environment: + - CUSTOM_WEBHOOK_URL=${CUSTOM_WEBHOOK_URL:-} + - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL:-} + - FETCH_INTERVAL=${FETCH_INTERVAL:-60} + - LOG_PLANES_TO_CONSOLE=${LOG_PLANES_TO_CONSOLE:-true} + - METRICS_PORT=9090 + - SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL:-} + - TAIL_NUMBERS=${TAIL_NUMBERS:-28000,29000} + ports: + - "9090:9090" + + prometheus: + image: prom/prometheus:latest + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + ports: + - "9091:9090" + depends_on: + - gee-bee + + grafana: + image: grafana/grafana:latest + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning:ro + ports: + - "3000:3000" + depends_on: + - prometheus diff --git a/go.mod b/go.mod index 1f761b3..cde8f63 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,23 @@ go 1.23.0 require ( github.com/bwmarrin/discordgo v0.28.1 github.com/jftuga/geodist v1.0.0 + github.com/prometheus/client_golang v1.23.2 github.com/spf13/viper v1.19.0 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -24,10 +31,12 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.35.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3c74382..26e5af6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ +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/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -8,29 +12,43 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jftuga/geodist v1.0.0 h1:PFPQlZtj10u8ETAYTyxE0DWMl1bwA+Xzrqb4+oLkkC0= github.com/jftuga/geodist v1.0.0/go.mod h1:BohEDxpZ8S5ADAxW/9EKPSKWOVl0+3wHENIT40m4UO4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -53,14 +71,19 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= @@ -68,16 +91,18 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grafana/provisioning/dashboards/dashboard.yml b/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000..69e71e0 --- /dev/null +++ b/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +providers: + - name: Gee Bee + folder: Gee Bee + type: file + options: + path: /etc/grafana/provisioning/dashboards diff --git a/grafana/provisioning/dashboards/gee-bee.json b/grafana/provisioning/dashboards/gee-bee.json new file mode 100644 index 0000000..3821fc1 --- /dev/null +++ b/grafana/provisioning/dashboards/gee-bee.json @@ -0,0 +1,374 @@ +{ + "__inputs": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.0.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + } + ], + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 }, + "id": 1, + "options": { + "legend": { "calcs": ["lastNotNull", "sum"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "increase(geebee_aircraft_spotted_total[$__rate_interval])", + "legendFormat": "{{tail_number}}", + "refId": "A" + } + ], + "title": "Aircraft Spotted (rate)", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 8 }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "sum(geebee_aircraft_spotted_total)", + "legendFormat": "Total", + "refId": "A" + } + ], + "title": "Total Aircraft Spotted", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 8 }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "geebee_api_calls_total", + "legendFormat": "API Calls", + "refId": "A" + } + ], + "title": "Total API Calls", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 1 }, + { "color": "red", "value": 5 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 6, "x": 12, "y": 8 }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "geebee_api_errors_total", + "legendFormat": "API Errors", + "refId": "A" + } + ], + "title": "Total API Errors", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 4, "w": 6, "x": 18, "y": 8 }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "textMode": "auto" + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "sum(geebee_notifications_sent_total)", + "legendFormat": "Notifications", + "refId": "A" + } + ], + "title": "Total Notifications Sent", + "type": "stat" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, + "id": 6, + "options": { + "legend": { "calcs": ["lastNotNull", "sum"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "increase(geebee_notifications_sent_total[$__rate_interval])", + "legendFormat": "{{channel}}", + "refId": "A" + } + ], + "title": "Notifications Sent by Channel (rate)", + "type": "timeseries" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { "legend": false, "tooltip": false, "viz": false }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { "type": "linear" }, + "showPoints": "never", + "spanNulls": false, + "stacking": { "group": "A", "mode": "none" }, + "thresholdsStyle": { "mode": "off" } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "red", "value": 80 } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, + "id": 7, + "options": { + "legend": { "calcs": ["lastNotNull", "sum"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "none" } + }, + "targets": [ + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "increase(geebee_api_calls_total[$__rate_interval])", + "legendFormat": "API Calls", + "refId": "A" + }, + { + "datasource": { "type": "prometheus", "uid": "${datasource}" }, + "expr": "increase(geebee_api_errors_total[$__rate_interval])", + "legendFormat": "API Errors", + "refId": "B" + } + ], + "title": "API Calls & Errors (rate)", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 38, + "tags": ["gee-bee", "aircraft"], + "templating": { + "list": [ + { + "current": {}, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + } + ] + }, + "time": { "from": "now-1h", "to": "now" }, + "timepicker": {}, + "timezone": "browser", + "title": "Gee Bee", + "uid": "gee-bee", + "version": 1 +} diff --git a/grafana/provisioning/datasources/prometheus.yml b/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 0000000..86fd346 --- /dev/null +++ b/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 5b9f6bd..fd3351b 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -13,6 +13,7 @@ var ( DiscordWebhookUrl string FetchInterval int LogPlanesToConsole bool + MetricsPort int SlackWebhookUrl string TailNumbers []string ) @@ -20,12 +21,14 @@ var ( const ( // Defaults defaultFetchInterval int = 60 + defaultMetricsPort int = 9090 // Config IDs config_customWebhookUrl string = "customWebhookUrl" config_discordWebhookUrl string = "discordWebhookUrl" config_fetchInterval string = "fetchInterval" config_logPlanesToConsole string = "logPlanesToConsole" + config_metricsPort string = "metricsPort" config_slackWebhookUrl string = "slackWebhookUrl" config_tailNumbers string = "tailNumbers" @@ -34,6 +37,7 @@ const ( env_discordWebHookUrl string = "DISCORD_WEBHOOK_URL" env_fetchInterval string = "FETCH_INTERVAL" env_logPlanesToConsole string = "LOG_PLANES_TO_CONSOLE" + env_metricsPort string = "METRICS_PORT" env_slackWebhookUrl string = "SLACK_WEBHOOK_URL" env_tailNumbers string = "TAIL_NUMBERS" ) @@ -45,6 +49,7 @@ func GetConfig() { viper.SetDefault(config_discordWebhookUrl, "") viper.SetDefault(config_fetchInterval, defaultFetchInterval) viper.SetDefault(config_logPlanesToConsole, true) + viper.SetDefault(config_metricsPort, defaultMetricsPort) viper.SetDefault(config_slackWebhookUrl, "") viper.SetDefault(config_tailNumbers, []string{"28000", "29000"}) @@ -69,6 +74,11 @@ func GetConfig() { log.Fatal(err) } + err = viper.BindEnv(config_metricsPort, env_metricsPort) + if err != nil { + log.Fatal(err) + } + err = viper.BindEnv(config_slackWebhookUrl, env_slackWebhookUrl) if err != nil { log.Fatal(err) @@ -95,6 +105,7 @@ func GetConfig() { DiscordWebhookUrl = viper.GetString(config_discordWebhookUrl) FetchInterval = viper.GetInt(config_fetchInterval) LogPlanesToConsole = viper.GetBool(config_logPlanesToConsole) + MetricsPort = viper.GetInt(config_metricsPort) SlackWebhookUrl = viper.GetString(config_slackWebhookUrl) tailNumbers := viper.GetString(config_tailNumbers) diff --git a/internal/geebee/geebee.go b/internal/geebee/geebee.go index 2ec5cf8..8c0f33d 100644 --- a/internal/geebee/geebee.go +++ b/internal/geebee/geebee.go @@ -10,6 +10,7 @@ import ( "strings" "geebee/internal/configuration" + "geebee/internal/metrics" "geebee/internal/planespotter" "github.com/jftuga/geodist" @@ -93,8 +94,10 @@ func validateAircraft(allFilteredAircraft []Aircraft, alreadySpottedAircraft *[] func HandleAircraft(alreadySpottedAircraft *[]Aircraft) (aircraft []AircraftOutput, err error) { var newlySpottedAircraft []Aircraft + metrics.APICallsTotal.Inc() allAircraftInRange, err := checkAircraft() if err != nil { + metrics.APIErrorsTotal.Inc() return nil, err } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..eceb5ca --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,58 @@ +package metrics + +import ( + "fmt" + "log" + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + // AircraftSpottedTotal counts the total number of aircraft spotted, labelled by tail number. + AircraftSpottedTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "geebee_aircraft_spotted_total", + Help: "Total number of aircraft spotted, labelled by tail number.", + }, + []string{"tail_number"}, + ) + + // APICallsTotal counts the total number of calls made to the upstream ADS-B API. + APICallsTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "geebee_api_calls_total", + Help: "Total number of calls made to the upstream ADS-B API.", + }) + + // APIErrorsTotal counts the total number of errors returned by the upstream ADS-B API. + APIErrorsTotal = promauto.NewCounter(prometheus.CounterOpts{ + Name: "geebee_api_errors_total", + Help: "Total number of errors returned by the upstream ADS-B API.", + }) + + // NotificationsSentTotal counts notifications sent, labelled by channel type. + NotificationsSentTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "geebee_notifications_sent_total", + Help: "Total number of notifications sent, labelled by channel (discord, slack, custom_url, terminal).", + }, + []string{"channel"}, + ) +) + +// StartServer starts the Prometheus metrics HTTP server on the given port. +func StartServer(port int) { + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + + addr := fmt.Sprintf(":%d", port) + log.Printf("Prometheus metrics available at http://0.0.0.0%s/metrics", addr) + + go func() { + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("Failed to start metrics server on port %d: %v", port, err) + } + }() +} diff --git a/prometheus.yml b/prometheus.yml new file mode 100644 index 0000000..fc66b6b --- /dev/null +++ b/prometheus.yml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: gee-bee + static_configs: + - targets: + - gee-bee:9090