diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml deleted file mode 100644 index bbc93b7..0000000 --- a/.forgejo/workflows/release.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: Release - -on: - push: - branches: - - main - -# Requires Forgejo v12+; older versions ignore this block. -concurrency: - group: release - cancel-in-progress: false - -jobs: - tag: - runs-on: debian - outputs: - new_tag: ${{ steps.tag.outputs.new_tag }} - steps: - - uses: https://git.sbriggs.io/actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a - with: - fetch-depth: 0 - - - name: Bump version and push tag - id: tag - shell: bash - env: - DEFAULT_BUMP: patch - run: | - set -euo pipefail - git config user.name "forgejo-actions[bot]" - git config user.email "forgejo-actions[bot]@noreply.localhost" - - # Already tagged? Reuse it instead of creating a duplicate. - if existing=$(git describe --exact-match --tags HEAD 2>/dev/null); then - echo "HEAD already tagged as $existing" - echo "new_tag=$existing" >> "$GITHUB_OUTPUT" - exit 0 - fi - - latest=$(git tag -l 'v[0-9]*' --sort=-v:refname | head -n1) - if [ -z "$latest" ]; then - major=0; minor=0; patch=0 - log=$(git log --pretty=%B) - else - ver=${latest#v} - IFS=. read -r major minor patch <<< "$ver" - log=$(git log --pretty=%B "$latest..HEAD") - fi - - bump="$DEFAULT_BUMP" - case "$log" in - *'#major'*) bump=major ;; - *'#minor'*) bump=minor ;; - *'#patch'*) bump=patch ;; - esac - - case "$bump" in - major) major=$((major + 1)); minor=0; patch=0 ;; - minor) minor=$((minor + 1)); patch=0 ;; - patch) patch=$((patch + 1)) ;; - esac - - new_tag="v${major}.${minor}.${patch}" - echo "Bumping ${latest:-} -> $new_tag ($bump)" - - git tag -a "$new_tag" -m "$new_tag" - git push origin "$new_tag" - echo "new_tag=$new_tag" >> "$GITHUB_OUTPUT" - - build-and-release: - needs: tag - runs-on: debian - steps: - #- uses: https://code.forgejo.org/actions/checkout@v4 - - uses: https://git.sbriggs.io/actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a - with: - fetch-depth: 0 - ref: ${{ needs.tag.outputs.new_tag }} - - - uses: https://code.forgejo.org/actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Build release binaries - run: make build - - - name: Compress Linux binaries - run: upx --best --lzma dist/*-linux-* - - - name: Generate release notes - id: notes - shell: bash - env: - TAG: ${{ needs.tag.outputs.new_tag }} - run: | - set -euo pipefail - prev=$(git tag -l 'v[0-9]*' --sort=-v:refname | grep -vFx "$TAG" | head -n1) - if [ -n "$prev" ]; then - range="$prev..$TAG" - header="## Changes since $prev" - else - range="$TAG" - header="## Changes" - fi - { - echo "notes<<__EOF__" - echo "$header" - echo - git log --no-merges --pretty='- %s (%h)' "$range" - echo "__EOF__" - } >> "$GITHUB_OUTPUT" - - - name: Publish Forgejo release - uses: https://git.sbriggs.io/actions/forgejo-release@585a6614e80a6bcfc429b956ae34831889cabb78 - with: - direction: upload - url: ${{ github.server_url }} - repo: ${{ github.repository }} - tag: ${{ needs.tag.outputs.new_tag }} - sha: ${{ github.sha }} - token: ${{ secrets.GITHUB_TOKEN }} - release-dir: dist - release-notes: ${{ steps.notes.outputs.notes }} - override: true - verbose: true \ No newline at end of file diff --git a/.github/workflows/release.yml.OLD b/.github/workflows/release.yml similarity index 100% rename from .github/workflows/release.yml.OLD rename to .github/workflows/release.yml diff --git a/README.md b/README.md index 3d23ff3..561b5cd 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](assets/screenshot1.png) +![Dashboard overview](screenshot1.png) -![Access log table](assets/screenshot2.png) +![Access log table](screenshot2.png) ## Features @@ -54,7 +54,6 @@ 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 @@ -79,12 +78,6 @@ 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 @@ -98,16 +91,6 @@ 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/cmd/root.go b/cmd/root.go index 38b077b..1867ab6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,7 +25,6 @@ var ( flagGeoIP string flagSplitByDay bool flagTheme string - flagMapAPIKey string ) var rootCmd = &cobra.Command{ @@ -47,8 +46,6 @@ 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. @@ -111,7 +108,6 @@ func runE(cmd *cobra.Command, args []string) error { AccessEntries: accessEntries, ErrorEntries: errorEntries, Theme: flagTheme, - MapAPIKey: flagMapAPIKey, } report.ComputeStats(&data) @@ -216,7 +212,6 @@ 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 e5c9216..12218be 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "html/template" - "net/url" "path/filepath" "sort" "strings" @@ -67,10 +66,6 @@ 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. @@ -343,20 +338,6 @@ 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) @@ -373,7 +354,7 @@ func GenerateHTML(data ReportData) (string, error) { Chart3: th.Chart3, ChartWarn: th.ChartWarn, ChartErr: th.ChartErr, - MapTileURL: tileURLWithKey(th.MapTileURL, data.MapAPIKey), + MapTileURL: th.MapTileURL, MapAttribution: th.MapAttribution, MarkerColor: th.MarkerColor, } diff --git a/internal/report/report_test.go b/internal/report/report_test.go index a0e6391..c0f2589 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -624,56 +624,3 @@ 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 849a9a6..fb0f03c 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://basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}{r}.png", + MapTileURL: "https://{s}.basemaps.cartocdn.com/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 df2e61f..8173ba8 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://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png", + MapTileURL: "https://{s}.basemaps.cartocdn.com/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 36c59a6..db9020a 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://basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}{r}.png", + MapTileURL: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", MapAttribution: `© CARTO`, MarkerColor: "#a78bfa", } diff --git a/assets/screenshot1.png b/screenshot1.png similarity index 100% rename from assets/screenshot1.png rename to screenshot1.png diff --git a/assets/screenshot2.png b/screenshot2.png similarity index 100% rename from assets/screenshot2.png rename to screenshot2.png