From d2260ffb9e3f50c00bd7698f2199b7828a7796e4 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 6 May 2025 13:58:52 -0400 Subject: [PATCH 1/2] feat: geoip collector and refactor --- cmd/serve.go | 10 +- go.mod | 1 + go.sum | 2 + pkg/collectors/autodetect/README.md | 14 ++ .../autodetect/manifestd}/denom_info.go | 21 +- .../autodetect/manifestd/manifestd.go | 5 +- .../autodetect/manifestd}/registry.go | 2 +- .../autodetect/manifestd}/token_count.go | 15 +- pkg/{ => collectors}/autodetect/registry.go | 0 pkg/{ => collectors}/autodetect/utils.go | 0 .../manifestd => }/collectors/common.go | 10 +- pkg/collectors/geoip.go | 191 ++++++++++++++++++ 12 files changed, 242 insertions(+), 29 deletions(-) create mode 100644 pkg/collectors/autodetect/README.md rename pkg/{autodetect/manifestd/collectors => collectors/autodetect/manifestd}/denom_info.go (87%) rename pkg/{ => collectors}/autodetect/manifestd/manifestd.go (94%) rename pkg/{autodetect/manifestd/collectors => collectors/autodetect/manifestd}/registry.go (98%) rename pkg/{autodetect/manifestd/collectors => collectors/autodetect/manifestd}/token_count.go (84%) rename pkg/{ => collectors}/autodetect/registry.go (100%) rename pkg/{ => collectors}/autodetect/utils.go (100%) rename pkg/{autodetect/manifestd => }/collectors/common.go (72%) create mode 100644 pkg/collectors/geoip.go diff --git a/cmd/serve.go b/cmd/serve.go index deed078..d384fff 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -11,8 +11,9 @@ import ( "time" "github.com/liftedinit/manifest-node-exporter/pkg" - "github.com/liftedinit/manifest-node-exporter/pkg/autodetect" - _ "github.com/liftedinit/manifest-node-exporter/pkg/autodetect/manifestd" // RegisterMonitor the manifestd monitor (side-effect) + "github.com/liftedinit/manifest-node-exporter/pkg/collectors" + "github.com/liftedinit/manifest-node-exporter/pkg/collectors/autodetect" + _ "github.com/liftedinit/manifest-node-exporter/pkg/collectors/autodetect/manifestd" // RegisterMonitor the manifestd monitor (side-effect) "github.com/prometheus/client_golang/prometheus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -36,11 +37,14 @@ var serveCmd = &cobra.Command{ defer rootCancel() handleInterrupt(rootCancel) + geoIpCollector := collectors.NewGeoIPCollector() + // Setup process monitors and fetch all registered collectors - allCollectors, err := setupMonitors(rootCtx) + monitorCollectors, err := setupMonitors(rootCtx) if err != nil { return fmt.Errorf("failed to setup monitors: %w", err) } + allCollectors := append(monitorCollectors, geoIpCollector) // Register all collectors with Prometheus registerCollectors(allCollectors) diff --git a/go.mod b/go.mod index 7160e84..765669d 100644 --- a/go.mod +++ b/go.mod @@ -50,4 +50,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + resty.dev/v3 v3.0.0-beta.2 // indirect ) diff --git a/go.sum b/go.sum index a9899ef..066a845 100644 --- a/go.sum +++ b/go.sum @@ -134,3 +134,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +resty.dev/v3 v3.0.0-beta.2 h1:xu4mGAdbCLuc3kbk7eddWfWm4JfhwDtdapwss5nCjnQ= +resty.dev/v3 v3.0.0-beta.2/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4= diff --git a/pkg/collectors/autodetect/README.md b/pkg/collectors/autodetect/README.md new file mode 100644 index 0000000..c417c31 --- /dev/null +++ b/pkg/collectors/autodetect/README.md @@ -0,0 +1,14 @@ +# Autodetect Module + +## Overview + +The `autodetect` module provides automatic detection functionality for the Manifest Node Exporter. It helps in +automatically discovering and configuring monitoring settings based on the environment. + +## Purpose + +This module is responsible for: + +- Detecting available services and endpoints +- Configuring appropriate monitoring parameters +- Providing sensible defaults for the exporter configuration diff --git a/pkg/autodetect/manifestd/collectors/denom_info.go b/pkg/collectors/autodetect/manifestd/denom_info.go similarity index 87% rename from pkg/autodetect/manifestd/collectors/denom_info.go rename to pkg/collectors/autodetect/manifestd/denom_info.go index b59ec2f..1e9aa78 100644 --- a/pkg/autodetect/manifestd/collectors/denom_info.go +++ b/pkg/collectors/autodetect/manifestd/denom_info.go @@ -1,4 +1,4 @@ -package collectors +package manifestd import ( "log/slog" @@ -6,6 +6,7 @@ import ( bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" "github.com/liftedinit/manifest-node-exporter/pkg/client" + "github.com/liftedinit/manifest-node-exporter/pkg/collectors" "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -71,10 +72,10 @@ func (c *DenomInfoCollector) Describe(ch chan<- *prometheus.Desc) { // Collect implements the prometheus.Collector interface. func (c *DenomInfoCollector) Collect(ch chan<- prometheus.Metric) { // Check for initialization or connection errors first. - if err := validateClient(c.grpcClient, c.initialError); err != nil { - reportUpMetric(ch, c.upDesc, 0) // Report gRPC down - reportInvalidMetric(ch, c.totalSupplyDesc, err) - reportInvalidMetric(ch, c.denomInfoDesc, err) + if err := collectors.ValidateClient(c.grpcClient, c.initialError); err != nil { + collectors.ReportUpMetric(ch, c.upDesc, 0) // Report gRPC down + collectors.ReportInvalidMetric(ch, c.totalSupplyDesc, err) + collectors.ReportInvalidMetric(ch, c.denomInfoDesc, err) return } @@ -94,7 +95,7 @@ func (c *DenomInfoCollector) Collect(ch chan<- prometheus.Metric) { if denomMetaErr == nil && totalSupplyErr == nil { upValue = 1.0 } - reportUpMetric(ch, c.upDesc, upValue) + collectors.ReportUpMetric(ch, c.upDesc, upValue) c.collectDenomMetadata(ch, denomMetaResp, denomMetaErr) c.collectTotalSupply(ch, totalSupplyResp, totalSupplyErr) @@ -102,7 +103,7 @@ func (c *DenomInfoCollector) Collect(ch chan<- prometheus.Metric) { func (c *DenomInfoCollector) collectDenomMetadata(ch chan<- prometheus.Metric, resp *bankv1beta1.QueryDenomMetadataResponse, queryErr error) { if queryErr != nil { - reportInvalidMetric(ch, c.denomInfoDesc, queryErr) + collectors.ReportInvalidMetric(ch, c.denomInfoDesc, queryErr) return } if resp == nil { @@ -131,7 +132,7 @@ func (c *DenomInfoCollector) collectDenomMetadata(ch chan<- prometheus.Metric, r func (c *DenomInfoCollector) collectTotalSupply(ch chan<- prometheus.Metric, resp *bankv1beta1.QuerySupplyOfResponse, queryErr error) { if queryErr != nil { - reportInvalidMetric(ch, c.totalSupplyDesc, queryErr) + collectors.ReportInvalidMetric(ch, c.totalSupplyDesc, queryErr) return } if resp == nil { @@ -140,7 +141,7 @@ func (c *DenomInfoCollector) collectTotalSupply(ch chan<- prometheus.Metric, res coin := resp.Amount if coin == nil { slog.Warn("Total supply response is nil") - reportInvalidMetric(ch, c.totalSupplyDesc, status.Error(codes.Internal, "total supply response is nil")) + collectors.ReportInvalidMetric(ch, c.totalSupplyDesc, status.Error(codes.Internal, "total supply response is nil")) return } @@ -148,7 +149,7 @@ func (c *DenomInfoCollector) collectTotalSupply(ch chan<- prometheus.Metric, res if err != nil { parseErr := status.Errorf(codes.Internal, "failed to parse amount '%s' for denom '%s': %v", coin.Amount, coin.Denom, err) slog.Warn("Failed to parse total supply amount", "denom", coin.Denom, "amount", coin.Amount, "error", err) - reportInvalidMetric(ch, c.totalSupplyDesc, parseErr) + collectors.ReportInvalidMetric(ch, c.totalSupplyDesc, parseErr) return } diff --git a/pkg/autodetect/manifestd/manifestd.go b/pkg/collectors/autodetect/manifestd/manifestd.go similarity index 94% rename from pkg/autodetect/manifestd/manifestd.go rename to pkg/collectors/autodetect/manifestd/manifestd.go index 4f78a5a..ecae831 100644 --- a/pkg/autodetect/manifestd/manifestd.go +++ b/pkg/collectors/autodetect/manifestd/manifestd.go @@ -8,9 +8,8 @@ import ( "slices" "strconv" - "github.com/liftedinit/manifest-node-exporter/pkg/autodetect" - "github.com/liftedinit/manifest-node-exporter/pkg/autodetect/manifestd/collectors" "github.com/liftedinit/manifest-node-exporter/pkg/client" + "github.com/liftedinit/manifest-node-exporter/pkg/collectors/autodetect" "github.com/liftedinit/manifest-node-exporter/pkg/utils" "github.com/prometheus/client_golang/prometheus" ) @@ -108,7 +107,7 @@ func (m *manifestdMonitor) CollectCollectors(ctx context.Context, processInfo *a } var resultCollectors []prometheus.Collector - for _, collector := range collectors.GetAllCollectorFactories() { + for _, collector := range GetAllCollectorFactories() { resultCollectors = append(resultCollectors, collector(grpcClient)) } diff --git a/pkg/autodetect/manifestd/collectors/registry.go b/pkg/collectors/autodetect/manifestd/registry.go similarity index 98% rename from pkg/autodetect/manifestd/collectors/registry.go rename to pkg/collectors/autodetect/manifestd/registry.go index 63ae551..ad15d63 100644 --- a/pkg/autodetect/manifestd/collectors/registry.go +++ b/pkg/collectors/autodetect/manifestd/registry.go @@ -1,4 +1,4 @@ -package collectors +package manifestd import ( "maps" diff --git a/pkg/autodetect/manifestd/collectors/token_count.go b/pkg/collectors/autodetect/manifestd/token_count.go similarity index 84% rename from pkg/autodetect/manifestd/collectors/token_count.go rename to pkg/collectors/autodetect/manifestd/token_count.go index 44064b7..dc7c97c 100644 --- a/pkg/autodetect/manifestd/collectors/token_count.go +++ b/pkg/collectors/autodetect/manifestd/token_count.go @@ -1,4 +1,4 @@ -package collectors +package manifestd import ( "log/slog" @@ -6,6 +6,7 @@ import ( bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" queryv1beta1 "cosmossdk.io/api/cosmos/base/query/v1beta1" "github.com/liftedinit/manifest-node-exporter/pkg/client" + "github.com/liftedinit/manifest-node-exporter/pkg/collectors" "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -57,9 +58,9 @@ func (c *TokenCountCollector) Describe(ch chan<- *prometheus.Desc) { // Collect implements the prometheus.Collector interface. func (c *TokenCountCollector) Collect(ch chan<- prometheus.Metric) { // Check for initialization or connection errors first. - if err := validateClient(c.grpcClient, c.initialError); err != nil { - reportUpMetric(ch, c.upDesc, 0) // Report gRPC down - reportInvalidMetric(ch, c.tokenCountDesc, err) + if err := collectors.ValidateClient(c.grpcClient, c.initialError); err != nil { + collectors.ReportUpMetric(ch, c.upDesc, 0) // Report gRPC down + collectors.ReportInvalidMetric(ch, c.tokenCountDesc, err) return } @@ -74,15 +75,15 @@ func (c *TokenCountCollector) Collect(ch chan<- prometheus.Metric) { if denomsMetaErr == nil { upValue = 1.0 } - reportUpMetric(ch, c.upDesc, upValue) + collectors.ReportUpMetric(ch, c.upDesc, upValue) if denomsMetaResp == nil { - reportInvalidMetric(ch, c.tokenCountDesc, status.Error(codes.Internal, "DenomsMetadata response is nil")) + collectors.ReportInvalidMetric(ch, c.tokenCountDesc, status.Error(codes.Internal, "DenomsMetadata response is nil")) return } if denomsMetaResp.Pagination == nil { - reportInvalidMetric(ch, c.tokenCountDesc, status.Error(codes.Internal, "Pagination response is nil")) + collectors.ReportInvalidMetric(ch, c.tokenCountDesc, status.Error(codes.Internal, "Pagination response is nil")) return } diff --git a/pkg/autodetect/registry.go b/pkg/collectors/autodetect/registry.go similarity index 100% rename from pkg/autodetect/registry.go rename to pkg/collectors/autodetect/registry.go diff --git a/pkg/autodetect/utils.go b/pkg/collectors/autodetect/utils.go similarity index 100% rename from pkg/autodetect/utils.go rename to pkg/collectors/autodetect/utils.go diff --git a/pkg/autodetect/manifestd/collectors/common.go b/pkg/collectors/common.go similarity index 72% rename from pkg/autodetect/manifestd/collectors/common.go rename to pkg/collectors/common.go index ea18971..ad0d9e2 100644 --- a/pkg/autodetect/manifestd/collectors/common.go +++ b/pkg/collectors/common.go @@ -9,7 +9,7 @@ import ( "google.golang.org/grpc/status" ) -func reportUpMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value float64) { +func ReportUpMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value float64) { metric, err := prometheus.NewConstMetric(desc, prometheus.GaugeValue, value) if err != nil { slog.Error("Failed to create up metric", "error", err) @@ -18,11 +18,11 @@ func reportUpMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, value fl } } -func reportInvalidMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, err error) { +func ReportInvalidMetric(ch chan<- prometheus.Metric, desc *prometheus.Desc, err error) { ch <- prometheus.NewInvalidMetric(desc, err) } -func validateGrpcClient(client *client.GRPCClient) error { +func ValidateGrpcClient(client *client.GRPCClient) error { if client == nil { return status.Error(codes.Internal, "gRPC client is nil during collect") } @@ -32,11 +32,11 @@ func validateGrpcClient(client *client.GRPCClient) error { return nil } -func validateClient(client *client.GRPCClient, initErr error) error { +func ValidateClient(client *client.GRPCClient, initErr error) error { if initErr != nil { return initErr } - if clientErr := validateGrpcClient(client); clientErr != nil { + if clientErr := ValidateGrpcClient(client); clientErr != nil { return clientErr } return nil diff --git a/pkg/collectors/geoip.go b/pkg/collectors/geoip.go new file mode 100644 index 0000000..bf3413b --- /dev/null +++ b/pkg/collectors/geoip.go @@ -0,0 +1,191 @@ +package collectors + +import ( + "encoding/json" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "time" + + "github.com/prometheus/client_golang/prometheus" + "resty.dev/v3" +) + +type GeoIPCollector struct { + latitude *prometheus.Desc + longitude *prometheus.Desc + info *prometheus.Desc + client *resty.Client +} + +type IPResponse struct { + IP string `json:"ip"` +} + +type GeoIPResponse struct { + IP string `json:"ip"` + CountryCode string `json:"country_code"` + CountryName string `json:"country_name"` + RegionCode string `json:"region_code"` + RegionName string `json:"region_name"` + City string `json:"city"` + ZipCode string `json:"zip_code"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +const ( + clientTimeout = 5 * time.Second // Timeout for HTTP requests + clientRetry = 3 // Number of retries for HTTP requests + ipifyURL = "https://api.ipify.org?format=json" + freeGeoIPURLFormat = "https://freegeoip.live/json/%s" +) + +func NewGeoIPCollector() *GeoIPCollector { + return &GeoIPCollector{ + client: resty.New().SetHeader("Accept", "application/json").SetTimeout(clientTimeout).SetRetryCount(clientRetry), + latitude: prometheus.NewDesc( + prometheus.BuildFQName("manifest", "geo", "latitude"), + "Node's geographical latitude", + []string{"ip"}, + prometheus.Labels{"source": "geoip"}, + ), + longitude: prometheus.NewDesc( + prometheus.BuildFQName("manifest", "geo", "longitude"), + "Node's geographical longitude", + []string{"ip"}, + prometheus.Labels{"source": "geoip"}, + ), + info: prometheus.NewDesc( + prometheus.BuildFQName("manifest", "geo", "info"), + "Node's geographical information", + []string{"ip", "country_code", "country_name", "region_code", "region_name", "city", "zip_code"}, + prometheus.Labels{"source": "geoip"}, + ), + } +} + +func (c *GeoIPCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.latitude + ch <- c.longitude + ch <- c.info +} + +func (c *GeoIPCollector) Collect(ch chan<- prometheus.Metric) { + ip, err := getPublicIP(c.client) + if err != nil { + ReportInvalidMetric(ch, c.info, fmt.Errorf("failed to get public ip address: %w", err)) + return + } + + maybeIp := net.ParseIP(ip) + if maybeIp == nil { + ReportInvalidMetric(ch, c.info, fmt.Errorf("invalid IP address: %s", ip)) + return + } + + geoIP, err := getGeoIP(c.client, ip) + if err != nil { + ReportInvalidMetric(ch, c.info, err) + return + } + + if geoIP == nil { + ReportInvalidMetric(ch, c.info, fmt.Errorf("geoIP response is nil")) + return + } + + geoMetric, err := prometheus.NewConstMetric( + c.info, + prometheus.GaugeValue, + 1, + ip, + geoIP.CountryCode, + geoIP.CountryName, + geoIP.RegionCode, + geoIP.RegionName, + geoIP.City, + geoIP.ZipCode, + ) + if err != nil { + slog.Error("Failed to create geo metric", "error", err) + return + } + + latMetric, err := prometheus.NewConstMetric( + c.latitude, + prometheus.GaugeValue, + geoIP.Latitude, + ip, + ) + if err != nil { + slog.Error("Failed to create latitude metric", "error", err) + return + } + + lonMetric, err := prometheus.NewConstMetric( + c.longitude, + prometheus.GaugeValue, + geoIP.Longitude, + ip, + ) + if err != nil { + slog.Error("Failed to create longitude metric", "error", err) + return + } + + ch <- geoMetric + ch <- latMetric + ch <- lonMetric +} + +func getPublicIP(client *resty.Client) (string, error) { + resp, err := client.R().Get(ipifyURL) + if err != nil { + return "", fmt.Errorf("error getting public ip: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode() != http.StatusOK { + return "", fmt.Errorf("error getting public ip: %s", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response body: %w", err) + } + + var ipResp IPResponse + if err := json.Unmarshal(body, &ipResp); err != nil { + return "", fmt.Errorf("error unmarshalling response body: %w", err) + } + + return ipResp.IP, nil +} + +func getGeoIP(client *resty.Client, ip string) (*GeoIPResponse, error) { + url := fmt.Sprintf(freeGeoIPURLFormat, ip) + resp, err := client.R().Get(url) + if err != nil { + return nil, fmt.Errorf("error getting geoip: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("error getting geoip: %s", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + + var geoIP GeoIPResponse + if err := json.Unmarshal(body, &geoIP); err != nil { + return nil, fmt.Errorf("error unmarshalling response body: %w", err) + } + + return &geoIP, nil +} From 215b33d44a10156d72a30b9650a94efc97a4d7b1 Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Tue, 6 May 2025 14:01:27 -0400 Subject: [PATCH 2/2] fix: status call --- pkg/collectors/geoip.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/collectors/geoip.go b/pkg/collectors/geoip.go index bf3413b..098dd96 100644 --- a/pkg/collectors/geoip.go +++ b/pkg/collectors/geoip.go @@ -149,7 +149,7 @@ func getPublicIP(client *resty.Client) (string, error) { defer resp.Body.Close() if resp.StatusCode() != http.StatusOK { - return "", fmt.Errorf("error getting public ip: %s", resp.Status) + return "", fmt.Errorf("error getting public ip: %s", resp.Status()) } body, err := io.ReadAll(resp.Body) @@ -174,7 +174,7 @@ func getGeoIP(client *resty.Client, ip string) (*GeoIPResponse, error) { defer resp.Body.Close() if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("error getting geoip: %s", resp.Status) + return nil, fmt.Errorf("error getting geoip: %s", resp.Status()) } body, err := io.ReadAll(resp.Body)