fix - carto now requires api keys #1
9 changed files with 100 additions and 6 deletions
21
README.md
21
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.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## 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:
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 696 KiB After Width: | Height: | Size: 696 KiB |
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: `© <a href="https://carto.com/">CARTO</a>`,
|
||||
MarkerColor: "#e0956b",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: `© <a href="https://carto.com/">CARTO</a>`,
|
||||
MarkerColor: "#ff00c8",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: `© <a href="https://carto.com/">CARTO</a>`,
|
||||
MarkerColor: "#a78bfa",
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue