From 3a3307e19061dd3f3f4b22418559e7d275ac85c5 Mon Sep 17 00:00:00 2001 From: mr0xb <47467008+mr0xb@users.noreply.github.com> Date: Wed, 26 Aug 2026 22:41:52 -0400 Subject: [PATCH] fix - carto now requires api keys --- README.md | 21 ++++++++- screenshot1.png => assets/screenshot1.png | Bin screenshot2.png => assets/screenshot2.png | Bin cmd/root.go | 5 ++ internal/report/report.go | 21 ++++++++- internal/report/report_test.go | 53 ++++++++++++++++++++++ internal/report/theme_cactus.go | 2 +- internal/report/theme_cyberpunk.go | 2 +- internal/report/theme_purplerain.go | 2 +- 9 files changed, 100 insertions(+), 6 deletions(-) rename screenshot1.png => assets/screenshot1.png (100%) rename screenshot2.png => assets/screenshot2.png (100%) diff --git a/README.md b/README.md index 561b5cd..3d23ff3 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ A fast nginx log parser that generates themeable HTML reports with charts, searchable tables, and optional GeoIP2 hit maps. -![Dashboard overview](screenshot1.png) +![Dashboard overview](assets/screenshot1.png) -![Access log table](screenshot2.png) +![Access log table](assets/screenshot2.png) ## Features @@ -54,6 +54,7 @@ nxstats [flags] | `--no-gzip` | | `false` | Skip `.gz` compressed rotated logs | | `--split-by-day` | | `false` | Write one HTML report per calendar day plus index | | `--theme` | | `cyberpunk` | Report visual theme: `cyberpunk`, `purplerain`, `cactus` | +| `--map-api-key` | | | CARTO basemap API key, appended to tile URLs as `?key=` (env: `NXSTATS_MAP_API_KEY`) | ### Examples @@ -78,6 +79,12 @@ nxstats --dir /var/log/nginx --split-by-day -o index.html # Writes index.html + 2024-01-15.html, 2024-01-16.html, etc. ``` +**With a CARTO basemap API key (required for map tiles):** +```bash +nxstats --geoip ./GeoLite2-City.mmdb --map-api-key XYZ -o report.html +# or: NXSTATS_MAP_API_KEY=XYZ nxstats --geoip ./GeoLite2-City.mmdb -o report.html +``` + **With a different theme:** ```bash nxstats --dir /var/log/nginx --theme purplerain -o report.html @@ -91,6 +98,16 @@ The geographic distribution map requires a free MaxMind GeoLite2-City database. 2. Download `GeoLite2-City.mmdb` 3. Pass the path via `--geoip ./GeoLite2-City.mmdb` +## Basemap API Key + +The map's background tiles come from [CARTO](https://carto.com/), which now +requires an API key. Create one in the CARTO dashboard and pass it with +`--map-api-key` (or set `NXSTATS_MAP_API_KEY`); it is appended to the theme's +tile URL as `?key=...` in the generated report. Note that the key is embedded +in the HTML output, so treat generated reports as you would the key itself. + +Without a key the map still renders, but tiles may come back blank. + ## Log Format nxstats expects the standard nginx combined log format for access logs: diff --git a/screenshot1.png b/assets/screenshot1.png similarity index 100% rename from screenshot1.png rename to assets/screenshot1.png diff --git a/screenshot2.png b/assets/screenshot2.png similarity index 100% rename from screenshot2.png rename to assets/screenshot2.png diff --git a/cmd/root.go b/cmd/root.go index 1867ab6..38b077b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,6 +25,7 @@ var ( flagGeoIP string flagSplitByDay bool flagTheme string + flagMapAPIKey string ) var rootCmd = &cobra.Command{ @@ -46,6 +47,8 @@ func init() { rootCmd.Flags().BoolVar(&flagSplitByDay, "split-by-day", false, "Write one HTML report per calendar day plus an index page") rootCmd.Flags().StringVar(&flagTheme, "theme", report.DefaultTheme, "Report visual theme ("+strings.Join(report.ThemeNames(), ", ")+")") + rootCmd.Flags().StringVar(&flagMapAPIKey, "map-api-key", os.Getenv("NXSTATS_MAP_API_KEY"), + "CARTO basemap API key, appended to map tile URLs as ?key= (env: NXSTATS_MAP_API_KEY)") } // validateTheme returns an error if name is not a recognized theme. @@ -108,6 +111,7 @@ func runE(cmd *cobra.Command, args []string) error { AccessEntries: accessEntries, ErrorEntries: errorEntries, Theme: flagTheme, + MapAPIKey: flagMapAPIKey, } report.ComputeStats(&data) @@ -212,6 +216,7 @@ func runSplitByDay( for i := range days { days[i].Data.IndexFile = indexBase days[i].Data.DayTitle = days[i].Date.Format("2006-01-02") + days[i].Data.MapAPIKey = flagMapAPIKey // Per-day geo: set Count from this day's access entries if len(geoPoints) > 0 { diff --git a/internal/report/report.go b/internal/report/report.go index 12218be..e5c9216 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "html/template" + "net/url" "path/filepath" "sort" "strings" @@ -66,6 +67,10 @@ type ReportData struct { // Theme selects the visual theme (see themes.go). Empty or unrecognized // falls back to DefaultTheme. Theme string + + // MapAPIKey is the basemap provider API key. When non-empty it is appended + // to the theme's tile URL as a "key" query parameter (CARTO's format). + MapAPIKey string } // DaySummary is one row in the index page's daily breakdown table. @@ -338,6 +343,20 @@ type reportRenderData struct { MarkerColor string } +// tileURLWithKey appends the basemap provider API key to tileURL as a "key" +// query parameter, e.g. ".../{z}/{x}/{y}.png?key=XYZ". The URL is returned +// unchanged when no key is set. +func tileURLWithKey(tileURL, key string) string { + if key == "" || tileURL == "" { + return tileURL + } + sep := "?" + if strings.Contains(tileURL, "?") { + sep = "&" + } + return tileURL + sep + "key=" + url.QueryEscape(key) +} + // GenerateHTML renders the full HTML report from data. func GenerateHTML(data ReportData) (string, error) { th := themeFor(data.Theme) @@ -354,7 +373,7 @@ func GenerateHTML(data ReportData) (string, error) { Chart3: th.Chart3, ChartWarn: th.ChartWarn, ChartErr: th.ChartErr, - MapTileURL: th.MapTileURL, + MapTileURL: tileURLWithKey(th.MapTileURL, data.MapAPIKey), MapAttribution: th.MapAttribution, MarkerColor: th.MarkerColor, } diff --git a/internal/report/report_test.go b/internal/report/report_test.go index c0f2589..a0e6391 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -624,3 +624,56 @@ func TestGenerateHTML_DayTitleInHeader(t *testing.T) { t.Error("expected DayTitle in report HTML") } } + +// ── map API key ──────────────────────────────────────────────────────────── + +func TestTileURLWithKey(t *testing.T) { + cases := []struct{ url, key, want string }{ + {"https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png", "XYZ", + "https://basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png?key=XYZ"}, + {"https://tiles.example/{z}/{x}/{y}.png?style=dark", "XYZ", + "https://tiles.example/{z}/{x}/{y}.png?style=dark&key=XYZ"}, + {"https://tiles.example/{z}/{x}/{y}.png", "a b&c", + "https://tiles.example/{z}/{x}/{y}.png?key=a+b%26c"}, + {"https://tiles.example/{z}/{x}/{y}.png", "", + "https://tiles.example/{z}/{x}/{y}.png"}, + } + for _, c := range cases { + if got := tileURLWithKey(c.url, c.key); got != c.want { + t.Errorf("tileURLWithKey(%q, %q) = %q, want %q", c.url, c.key, got, c.want) + } + } +} + +func TestGenerateHTML_MapAPIKeyInTileURL(t *testing.T) { + data := makeTestData() + ComputeStats(&data) + data.GeoLocations = []GeoPoint{ + {IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2}, + } + data.MapAPIKey = "secret-key" + + html, err := GenerateHTML(data) + if err != nil { + t.Fatalf("GenerateHTML error: %v", err) + } + if !strings.Contains(html, "?key=secret-key") { + t.Error("expected map API key appended to the tile URL") + } +} + +func TestGenerateHTML_NoMapAPIKeyLeavesTileURL(t *testing.T) { + data := makeTestData() + ComputeStats(&data) + data.GeoLocations = []GeoPoint{ + {IP: "1.1.1.1", Lat: 37.751, Lon: -97.822, Country: "United States", CountryCode: "US", Count: 2}, + } + + html, err := GenerateHTML(data) + if err != nil { + t.Fatalf("GenerateHTML error: %v", err) + } + if strings.Contains(html, "key=") { + t.Error("did not expect a key query parameter when MapAPIKey is empty") + } +} diff --git a/internal/report/theme_cactus.go b/internal/report/theme_cactus.go index fb0f03c..849a9a6 100644 --- a/internal/report/theme_cactus.go +++ b/internal/report/theme_cactus.go @@ -241,7 +241,7 @@ var cactusTheme = themeAssets{ Chart3: "#e0956b", ChartWarn: "#d99a3d", ChartErr: "#c96a5b", - MapTileURL: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", + MapTileURL: "https://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}{r}.png", MapAttribution: `© CARTO`, MarkerColor: "#e0956b", } diff --git a/internal/report/theme_cyberpunk.go b/internal/report/theme_cyberpunk.go index 8173ba8..df2e61f 100644 --- a/internal/report/theme_cyberpunk.go +++ b/internal/report/theme_cyberpunk.go @@ -258,7 +258,7 @@ var cyberpunkTheme = themeAssets{ Chart3: "#ff00c8", ChartWarn: "#ffcc00", ChartErr: "#ff4444", - MapTileURL: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", + MapTileURL: "https://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png", MapAttribution: `© CARTO`, MarkerColor: "#ff00c8", } diff --git a/internal/report/theme_purplerain.go b/internal/report/theme_purplerain.go index db9020a..36c59a6 100644 --- a/internal/report/theme_purplerain.go +++ b/internal/report/theme_purplerain.go @@ -243,7 +243,7 @@ var purplerainTheme = themeAssets{ Chart3: "#e879f9", ChartWarn: "#fbbf24", ChartErr: "#fb7185", - MapTileURL: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", + MapTileURL: "https://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png", MapAttribution: `© CARTO`, MarkerColor: "#a78bfa", } -- 2.47.3