Implement Rybbit changes
This commit is contained in:
parent
8d7198941f
commit
6def58012a
14 changed files with 142 additions and 345 deletions
BIN
.assets/icon.png
BIN
.assets/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 292 KiB |
16
.traefik.yml
16
.traefik.yml
|
|
@ -1,17 +1,13 @@
|
||||||
displayName: Traefik Umami Feeder
|
displayName: Traefik Rybbit Feeder
|
||||||
|
|
||||||
import: github.com/astappiev/traefik-umami-feeder
|
import: github.com/foxxmd/traefik-rybbit-feeder
|
||||||
type: middleware
|
type: middleware
|
||||||
|
|
||||||
summary: A Traefik plugin that feeds Umami Analytics with pageview events
|
summary: A Traefik plugin that feeds Rybbit Analytics with pageview events
|
||||||
iconPath: .assets/icon.png
|
iconPath: .assets/icon.png
|
||||||
|
|
||||||
testData:
|
testData:
|
||||||
umamiHost: ""
|
Host: ""
|
||||||
umamiToken: ""
|
ApiKey: ""
|
||||||
umamiUsername: ""
|
|
||||||
umamiPassword: ""
|
|
||||||
umamiTeamId: ""
|
|
||||||
websites:
|
websites:
|
||||||
domain: websiteId
|
localhost: siteId
|
||||||
createNewWebsites: false
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
umami:
|
rybbit:
|
||||||
plugin:
|
plugin:
|
||||||
umami-feeder:
|
rybbit-feeder:
|
||||||
debug: true
|
debug: true
|
||||||
umamiHost: http://umami:3000
|
host: https://rybbit.mysite.com
|
||||||
umamiUsername: "admin"
|
apiKey: rb_a0938f333a88239c6250c2c3707a
|
||||||
umamiPassword: "umami"
|
websites:
|
||||||
umamiTeamId: "8e39c6ad-e44a-4d3e-be98-015db2d62d40"
|
"localhost": "1"
|
||||||
createNewWebsites: true
|
batchSize: 1
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:3
|
image: traefik:3
|
||||||
depends_on:
|
|
||||||
- umami
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.traefik.service=api@internal"
|
- "traefik.http.routers.traefik.service=api@internal"
|
||||||
|
|
@ -24,7 +22,7 @@ services:
|
||||||
- ./traefik.yml:/traefik.yml:ro
|
- ./traefik.yml:/traefik.yml:ro
|
||||||
- ./config.yml:/config/config.yml:ro
|
- ./config.yml:/config/config.yml:ro
|
||||||
# mount plugin
|
# mount plugin
|
||||||
- ../:/plugins-local/src/github.com/astappiev/traefik-umami-feeder
|
- ../:/plugins-local/src/github.com/foxxmd/traefik-rybbit-feeder
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
@ -32,7 +30,7 @@ services:
|
||||||
image: containous/whoami
|
image: containous/whoami
|
||||||
ports:
|
ports:
|
||||||
- target: 80
|
- target: 80
|
||||||
published: 80
|
published: 7654
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
|
|
@ -41,44 +39,10 @@ services:
|
||||||
- "traefik.http.routers.whoami.rule=Host(`localhost`)"
|
- "traefik.http.routers.whoami.rule=Host(`localhost`)"
|
||||||
- "traefik.http.routers.whoami.entrypoints=whoami"
|
- "traefik.http.routers.whoami.entrypoints=whoami"
|
||||||
# middleware
|
# middleware
|
||||||
- "traefik.http.routers.whoami.middlewares=umami@file"
|
- "traefik.http.routers.whoami.middlewares=rybbit@file"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
|
|
||||||
umami:
|
|
||||||
image: ghcr.io/umami-software/umami:postgresql-latest
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
- DEBUG=umami:middleware
|
|
||||||
- ENABLE_TEST_CONSOLE=1
|
|
||||||
- DATABASE_URL=postgres://umami:umami@postgres:5432/umami
|
|
||||||
ports:
|
|
||||||
- target: 3000
|
|
||||||
published: 3000
|
|
||||||
protocol: tcp
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
- umami_postgres
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:16
|
|
||||||
environment:
|
|
||||||
- POSTGRES_PASSWORD=umami
|
|
||||||
- POSTGRES_DB=umami
|
|
||||||
- POSTGRES_USER=umami
|
|
||||||
networks:
|
|
||||||
- umami_postgres
|
|
||||||
volumes:
|
|
||||||
- ./postgres:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: [ "CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'" ]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
traefik:
|
traefik:
|
||||||
umami_postgres:
|
name: traefik
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,11 @@ entryPoints:
|
||||||
address: ":8080"
|
address: ":8080"
|
||||||
"whoami": # http
|
"whoami": # http
|
||||||
address: ":8081"
|
address: ":8081"
|
||||||
"umami": # umami dashboard
|
|
||||||
address: ":3000"
|
|
||||||
|
|
||||||
api:
|
api:
|
||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
experimental:
|
experimental:
|
||||||
localPlugins:
|
localPlugins:
|
||||||
umami-feeder:
|
rybbit-feeder:
|
||||||
moduleName: "github.com/astappiev/traefik-umami-feeder"
|
moduleName: "github.com/foxxmd/traefik-rybbit-feeder"
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
||||||
module github.com/astappiev/traefik-umami-feeder
|
module github.com/FoxxMD/traefik-rybbit-feeder
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
|
||||||
91
readme.md
91
readme.md
|
|
@ -1,14 +1,13 @@
|
||||||
# Traefik Umami Feeder Plugin
|
# Traefik Rybbit Feeder Plugin
|
||||||
|
|
||||||
A [Traefik](https://traefik.io/traefik/) middleware plugin that sends visits to your [Umami](https://umami.is) instance.
|
A [Traefik](https://traefik.io/traefik/) middleware plugin that sends visits to your [Rybbit](https://www.rybbit.io/) instance.
|
||||||
|
|
||||||
It was created as an alternative to [traefik-umami-plugin](https://github.com/1cedsoda/traefik-umami-plugin) and
|
**This is a fork of [traefik-umami-feeder](https://github.com/astappiev/traefik-umami-feeder) modified to work with Rybbit.**
|
||||||
inspired by the [Plausible Feeder Traefik Plugin](https://github.com/safing/plausiblefeeder).
|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
This plugin integrates your Traefik-proxied services with Umami, a simple, fast, privacy-focused analytics solution. It
|
This plugin integrates your Traefik-proxied services with Rybbit, a simple, fast, privacy-focused analytics solution. It
|
||||||
captures basic request information (path, user-agent, referrer, screen size, IP) and forwards it to your Umami instance,
|
captures basic request information (path, user-agent, referrer, IP) and forwards it to your Rybbit instance,
|
||||||
enabling server-side analytics.
|
enabling server-side analytics.
|
||||||
|
|
||||||
Key features:
|
Key features:
|
||||||
|
|
@ -26,51 +25,42 @@ Declare the plugin in your Traefik **static configuration**.
|
||||||
```yaml
|
```yaml
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
umami-feeder:
|
rybbit-feeder:
|
||||||
moduleName: github.com/astappiev/traefik-umami-feeder
|
moduleName: github.com/foxxmd/traefik-rybbit-feeder
|
||||||
version: v1.3.0 # Replace with the latest version
|
version: v0.13.0 # Replace with the latest version
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2. Configure the middleware
|
### Step 2. Configure the middleware
|
||||||
|
|
||||||
Once the plugin is declared, configure it as a middleware in your Traefik **dynamic configuration**.
|
Once the plugin is declared, configure it as a middleware in your Traefik **dynamic configuration**.
|
||||||
|
|
||||||
You can specify which websites to track in two ways:
|
You will need to specify at least three pieces of data:
|
||||||
|
|
||||||
1. **Manual**: Directly provide a `websites` map, associating hostnames with their Umami Website IDs.
|
* `host` - Your Rybbit instance base URL. This is URL **without** `/api/track`.
|
||||||
2. **Automatic**: Configure the plugin with your Umami API `umamiToken`, or `umamiUsername` and `umamiPassword`. The
|
* `apiKey` - The [**Api Key**](https://www.rybbit.io/docs/api#steps) generated for the website you wish to track.
|
||||||
plugin will then automatically fetch the list of websites and their IDs from your Umami instance.
|
* `websites` - A map of `domain` - `site-id` properties to tell the feeder what traffic should go to your Rybbit Site
|
||||||
* Optionally, use `umamiTeamId` to scope website retrieval to a specific team.
|
|
||||||
* Optionally, enable `createNewWebsites` to allow the plugin to create new website entries in Umami if they don't
|
|
||||||
already exist.
|
|
||||||
|
|
||||||
See the [Middleware Options](#middleware-options) section for detailed configuration options.
|
There are additional, optional [Middleware Options](#middleware-options) to configure more behavior below.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
my-umami-middleware:
|
my-rybbit-middleware:
|
||||||
plugin:
|
plugin:
|
||||||
umami-feeder:
|
rybbit-feeder:
|
||||||
umamiHost: "http://umami:3000" # URL of your Umami instance
|
host: "http://rybbit.mysite.com" # URL of your Rybbit instance
|
||||||
|
|
||||||
# Option 1: Define the list of websites
|
apiKey: rb_a0938250c2c2efd061c8250c2c3707a
|
||||||
# websites:
|
|
||||||
# "example.com": "d4617504-241c-4797-8eab-5939b367b3ad"
|
|
||||||
|
|
||||||
# Option 2: Use Umami credentials to fetch websites
|
websites:
|
||||||
umamiUsername: "your-umami-username"
|
# domain to capture traffic for and the site-id from Rybbit
|
||||||
umamiPassword: "your-umami-password"
|
"example.com": "1"
|
||||||
# umamiToken: "your-umami-api-token" # Alternative to username/password
|
|
||||||
|
|
||||||
# Optional: allow creation of new websites in Umami
|
|
||||||
createNewWebsites: true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3. Attach the middleware to your routers
|
### Step 3. Attach the middleware to your routers
|
||||||
|
|
||||||
Apply the [configured middleware](https://doc.traefik.io/traefik/routing/routers/#middlewares_1) to the Traefik routers
|
Apply the [configured middleware](https://doc.traefik.io/traefik/routing/routers/#middlewares_1) to the Traefik routers
|
||||||
you want to track with Umami. This is also done in your **dynamic configuration**.
|
you want to track with Rybbit. This is also done in your **dynamic configuration**.
|
||||||
|
|
||||||
Remember to use the
|
Remember to use the
|
||||||
correct [provider namespace](https://doc.traefik.io/traefik/providers/overview/#provider-namespace) (e.g., `@file` if
|
correct [provider namespace](https://doc.traefik.io/traefik/providers/overview/#provider-namespace) (e.g., `@file` if
|
||||||
|
|
@ -79,7 +69,7 @@ your middleware is defined in a file, `@docker` if defined via Docker labels).
|
||||||
**Example using Docker labels:**
|
**Example using Docker labels:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- "traefik.http.routers.whoami.middlewares=my-umami-middleware@file"
|
- "traefik.http.routers.whoami.middlewares=my-rybbit-middleware@file"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example using a dynamic configuration file (e.g., `dynamic_conf.yml`):**
|
**Example using a dynamic configuration file (e.g., `dynamic_conf.yml`):**
|
||||||
|
|
@ -90,7 +80,7 @@ http:
|
||||||
whoami:
|
whoami:
|
||||||
rule: "Host(`example.com`)"
|
rule: "Host(`example.com`)"
|
||||||
middlewares:
|
middlewares:
|
||||||
- my-umami-middleware@file
|
- my-rybbit-middleware@file
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example using static configuration (e.g., `traefik.yml`), by attaching the middleware to an entryPoint to apply it
|
**Example using static configuration (e.g., `traefik.yml`), by attaching the middleware to an entryPoint to apply it
|
||||||
|
|
@ -101,30 +91,25 @@ entryPoints:
|
||||||
web:
|
web:
|
||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
- my-umami-middleware@file
|
- my-rybbit-middleware@file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Middleware Options
|
## Middleware Options
|
||||||
|
|
||||||
| key | default | type | description |
|
| key | default | type | description |
|
||||||
|---------------------|-----------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ------------------- | :-------------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `disabled` | `false` | `bool` | Set to `true` to disable the plugin. |
|
| `disabled` | `false` | `bool` | Set to `true` to disable the plugin. |
|
||||||
| `debug` | `false` | `bool` | Set to `true` for verbose logging. Useful for troubleshooting as plugins don't inherit Traefik's global log level. |
|
| `debug` | `false` | `bool` | Set to `true` for verbose logging. Useful for troubleshooting as plugins don't inherit Traefik's global log level. |
|
||||||
| `queueSize` | `1000` | `int` | Maximum number of tracking events to queue before sending to the Umami server. |
|
| `host` | **required** | `string` | URL of your Rybbit instance, reachable from Traefik (e.g., `https://rybbit.mydomain.com`). |
|
||||||
| `umamiHost` | **required** | `string` | URL of your Umami instance, reachable from Traefik (e.g., `http://umami:3000`). |
|
| `apiKey` | **required** | `string` | [Rybbit API Key](https://www.rybbit.io/docs/api#steps) for authenticating with your Rybbit instance. |
|
||||||
| `umamiToken` | - | `string` | [Umami API Token](https://umami.is/docs/api/authentication) for authenticating with your Umami instance. Use this *or* `umamiUsername`/`umamiPassword`. Required for automatic website fetching or creation. |
|
| `websites` | **required** | `map` | A map of `hostname: site-id` |
|
||||||
| `umamiUsername` | - | `string` | Username for Umami authentication. Use this with `umamiPassword` if not using `umamiToken`. Required for automatic website fetching or creation. |
|
| `trackErrors` | `false` | `bool` | If `true`, tracks errors (status codes >= 400). |
|
||||||
| `umamiPassword` | - | `string` | Password for Umami authentication, used in conjunction with `umamiUsername`. |
|
| `trackAllResources` | `false` | `bool` | If `true`, tracks requests for all resources. By default, only requests likely to be page views (e.g., HTML, or no specific extension) are tracked. |
|
||||||
| `umamiTeamId` | - | `string` | Optional. If using automatic mode, specifies the Umami Team ID to scope website fetching/creation. |
|
| `trackExtensions` | `[see sources]` | `string[]` | A list of specific file extensions to track (e.g., `[".html", ".php"]`). |
|
||||||
| `websites` | - | `map` | A map of `hostname: umamiWebsiteID`. Used for manual website configuration or to override/extend websites fetched in automatic mode. |
|
| `ignoreUserAgents` | `[]` | `string[]` | A list of user-agent substrings. Requests with matching user-agents will be ignored (e.g., `["Googlebot", "Uptime-Kuma"]`). Matched with `strings.Contains`. |
|
||||||
| `createNewWebsites` | `false` | `bool` | If `true` and using automatic mode, the plugin will attempt to create a new website entry in Umami if the domain is not found. |
|
| `ignoreURLs` | `[]` | `string[]` | A list of regular expressions. Requests with URLs matching any of these patterns will be ignored (e.g., `["/health", "https?://[^/]+/health$"]`). Matched with `regexp.Compile.MatchString`. |
|
||||||
| `trackErrors` | `false` | `bool` | If `true`, tracks errors (status codes >= 400). |
|
| `ignoreIPs` | `[]` | `string[]` | A list of IP addresses or CIDR ranges to ignore (e.g., `["127.0.0.1", "10.0.0.1/16"]`). Matched with `netip.ParsePrefix.Contains`. |
|
||||||
| `trackAllResources` | `false` | `bool` | If `true`, tracks requests for all resources. By default, only requests likely to be page views (e.g., HTML, or no specific extension) are tracked. |
|
| `headerIp` | `X-Real-Ip` | `string` | The HTTP header to inspect for the client's real IP address, typically used when Traefik is behind another proxy. |
|
||||||
| `trackExtensions` | `[see sources]` | `string[]` | A list of specific file extensions to track (e.g., `[".html", ".php"]`). |
|
|
||||||
| `ignoreUserAgents` | `[]` | `string[]` | A list of user-agent substrings. Requests with matching user-agents will be ignored (e.g., `["Googlebot", "Uptime-Kuma"]`). Matched with `strings.Contains`. |
|
|
||||||
| `ignoreURLs` | `[]` | `string[]` | A list of regular expressions. Requests with URLs matching any of these patterns will be ignored (e.g., `["/health", "https?://[^/]+/health$"]`). Matched with `regexp.Compile.MatchString`. |
|
|
||||||
| `ignoreIPs` | `[]` | `string[]` | A list of IP addresses or CIDR ranges to ignore (e.g., `["127.0.0.1", "10.0.0.1/16"]`). Matched with `netip.ParsePrefix.Contains`. |
|
|
||||||
| `headerIp` | `X-Real-Ip` | `string` | The HTTP header to inspect for the client's real IP address, typically used when Traefik is behind another proxy. |
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package traefik_umami_feeder
|
package traefik_rybbit_feeder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
|
||||||
107
umami.go
107
umami.go
|
|
@ -1,4 +1,4 @@
|
||||||
package traefik_umami_feeder
|
package traefik_rybbit_feeder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -21,29 +21,20 @@ type Config struct {
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
// Debug enables debug logging, be prepared for flooding.
|
// Debug enables debug logging, be prepared for flooding.
|
||||||
Debug bool `json:"debug"`
|
Debug bool `json:"debug"`
|
||||||
// QueueSize defines the size of queue, i.e. the amount of events that are waiting to be submitted to Umami.
|
// QueueSize defines the size of queue, i.e. the amount of events that are waiting to be submitted to Rybbit.
|
||||||
QueueSize int `json:"queueSize"`
|
QueueSize int `json:"queueSize"`
|
||||||
// BatchSize defines the amount of events that are submitted to Umami in one request.
|
// BatchSize defines the amount of events that are submitted to Rybbit in one request, should always be 1.
|
||||||
BatchSize int `json:"batchSize"`
|
BatchSize int `json:"batchSize"`
|
||||||
// BatchMaxWait defines the maximum time to wait before submitting the batch.
|
// BatchMaxWait defines the maximum time to wait before submitting the batch. Should be 1 second.
|
||||||
BatchMaxWait time.Duration `json:"batchMaxWait"`
|
BatchMaxWait time.Duration `json:"batchMaxWait"`
|
||||||
|
|
||||||
// UmamiHost is the URL of the Umami instance.
|
// Host is the URL of the Rybbit instance.
|
||||||
UmamiHost string `json:"umamiHost"`
|
Host string `json:"host"`
|
||||||
// UmamiToken is an API KEY, which is optional, but either UmamiToken or Websites should be set.
|
// APIKey is the API Key generated in Site Settings for a Rybbit Website
|
||||||
UmamiToken string `json:"umamiToken"`
|
APIKey string `json:"apiKey"`
|
||||||
// UmamiUsername could be provided as an alternative to UmamiToken, used to retrieve the token.
|
|
||||||
UmamiUsername string `json:"umamiUsername"`
|
|
||||||
// UmamiPassword is required if UmamiUsername is set.
|
|
||||||
UmamiPassword string `json:"umamiPassword"`
|
|
||||||
// UmamiTeamId defines a team, which will be used to retrieve the websites.
|
|
||||||
UmamiTeamId string `json:"umamiTeamId"`
|
|
||||||
|
|
||||||
// Websites is a map of domain to websiteId, which is required if UmamiToken is not set.
|
// Websites is a map of domain to site-id, which is required
|
||||||
// If both UmamiToken and Websites are set, Websites will override/extend domains retrieved from the API.
|
|
||||||
Websites map[string]string `json:"websites"`
|
Websites map[string]string `json:"websites"`
|
||||||
// CreateNewWebsites when set to true, the plugin will create new websites using API, UmamiToken is required.
|
|
||||||
CreateNewWebsites bool `json:"createNewWebsites"`
|
|
||||||
|
|
||||||
// TrackErrors defines whether errors (status codes >= 400) should be tracked.
|
// TrackErrors defines whether errors (status codes >= 400) should be tracked.
|
||||||
TrackErrors bool `json:"trackErrors"`
|
TrackErrors bool `json:"trackErrors"`
|
||||||
|
|
@ -73,14 +64,10 @@ func CreateConfig() *Config {
|
||||||
BatchMaxWait: 5 * time.Second,
|
BatchMaxWait: 5 * time.Second,
|
||||||
TrackErrors: false,
|
TrackErrors: false,
|
||||||
|
|
||||||
UmamiHost: "",
|
Host: "",
|
||||||
UmamiToken: "",
|
APIKey: "",
|
||||||
UmamiUsername: "",
|
|
||||||
UmamiPassword: "",
|
|
||||||
UmamiTeamId: "",
|
|
||||||
|
|
||||||
Websites: map[string]string{},
|
Websites: map[string]string{},
|
||||||
CreateNewWebsites: false,
|
|
||||||
|
|
||||||
TrackAllResources: false,
|
TrackAllResources: false,
|
||||||
TrackExtensions: []string{},
|
TrackExtensions: []string{},
|
||||||
|
|
@ -99,14 +86,13 @@ type UmamiFeeder struct {
|
||||||
isDebug bool
|
isDebug bool
|
||||||
isDisabled bool
|
isDisabled bool
|
||||||
logHandler *log.Logger
|
logHandler *log.Logger
|
||||||
queue chan *UmamiEvent
|
queue chan *RybbitEvent
|
||||||
|
|
||||||
batchSize int
|
batchSize int
|
||||||
batchMaxWait time.Duration
|
batchMaxWait time.Duration
|
||||||
|
|
||||||
umamiHost string
|
host string
|
||||||
umamiToken string
|
apiKey string
|
||||||
umamiTeamId string
|
|
||||||
websites map[string]string
|
websites map[string]string
|
||||||
websitesMutex sync.RWMutex
|
websitesMutex sync.RWMutex
|
||||||
createNewWebsites bool
|
createNewWebsites bool
|
||||||
|
|
@ -131,16 +117,14 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||||
isDisabled: config.Disabled,
|
isDisabled: config.Disabled,
|
||||||
logHandler: log.New(os.Stdout, "", 0),
|
logHandler: log.New(os.Stdout, "", 0),
|
||||||
|
|
||||||
queue: make(chan *UmamiEvent, config.QueueSize),
|
queue: make(chan *RybbitEvent, config.QueueSize),
|
||||||
batchSize: config.BatchSize,
|
batchSize: config.BatchSize,
|
||||||
batchMaxWait: config.BatchMaxWait,
|
batchMaxWait: 1 * time.Second,
|
||||||
|
|
||||||
umamiHost: config.UmamiHost,
|
host: config.Host,
|
||||||
umamiToken: config.UmamiToken,
|
apiKey: config.APIKey,
|
||||||
umamiTeamId: config.UmamiTeamId,
|
websites: config.Websites,
|
||||||
websites: config.Websites,
|
websitesMutex: sync.RWMutex{},
|
||||||
websitesMutex: sync.RWMutex{},
|
|
||||||
createNewWebsites: config.CreateNewWebsites,
|
|
||||||
|
|
||||||
trackErrors: config.TrackErrors,
|
trackErrors: config.TrackErrors,
|
||||||
trackAllResources: config.TrackAllResources,
|
trackAllResources: config.TrackAllResources,
|
||||||
|
|
@ -154,6 +138,8 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
||||||
|
|
||||||
if !h.isDisabled {
|
if !h.isDisabled {
|
||||||
h.isDisabled = true
|
h.isDisabled = true
|
||||||
|
h.debug("batchSize %d", h.batchSize)
|
||||||
|
h.debug("batchMaxWait %v", h.batchMaxWait)
|
||||||
go h.retryConnection(ctx, config)
|
go h.retryConnection(ctx, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,11 +164,11 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) {
|
||||||
select {
|
select {
|
||||||
case <-time.After(currentDelay):
|
case <-time.After(currentDelay):
|
||||||
retryAttempt++
|
retryAttempt++
|
||||||
h.debug("Attempting to connect to Umami (attempt #%d)...", retryAttempt)
|
h.debug("Attempting to connect to Rybbit (attempt #%d)...", retryAttempt)
|
||||||
|
|
||||||
err := h.connect(ctx, config)
|
err := h.connect(ctx, config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
h.debug("Successfully connected to Umami. Verifying configuration...")
|
h.debug("Successfully connected to Rybbit. Verifying configuration...")
|
||||||
|
|
||||||
err = h.verifyConfig(config)
|
err = h.verifyConfig(config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -197,7 +183,7 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) {
|
||||||
return // Exit retry goroutine, plugin remains disabled.
|
return // Exit retry goroutine, plugin remains disabled.
|
||||||
}
|
}
|
||||||
|
|
||||||
h.error("Failed to reconnect to Umami: " + err.Error())
|
h.error("Failed to reconnect to Rybbit: " + err.Error())
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
h.debug("Context cancelled during retryConnection, stopping connection retries.")
|
h.debug("Context cancelled during retryConnection, stopping connection retries.")
|
||||||
return
|
return
|
||||||
|
|
@ -206,42 +192,21 @@ func (h *UmamiFeeder) retryConnection(ctx context.Context, config *Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UmamiFeeder) connect(ctx context.Context, config *Config) error {
|
func (h *UmamiFeeder) connect(ctx context.Context, config *Config) error {
|
||||||
if h.umamiHost == "" {
|
if h.host == "" {
|
||||||
return fmt.Errorf("`umamiHost` is not set")
|
return fmt.Errorf("`host` is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.UmamiUsername != "" && config.UmamiPassword != "" {
|
if h.apiKey == "" {
|
||||||
token, err := getToken(ctx, h.umamiHost, config.UmamiUsername, config.UmamiPassword)
|
return fmt.Errorf("`apiKey` should be set")
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get token: %w", err)
|
|
||||||
}
|
|
||||||
if token == "" {
|
|
||||||
return fmt.Errorf("retrieved token is empty")
|
|
||||||
}
|
|
||||||
h.debug("token received %s", token)
|
|
||||||
h.umamiToken = token
|
|
||||||
}
|
|
||||||
if h.umamiToken == "" && len(h.websites) == 0 {
|
|
||||||
return fmt.Errorf("either `umamiToken` or `websites` should be set")
|
|
||||||
}
|
|
||||||
if h.umamiToken == "" && h.createNewWebsites {
|
|
||||||
return fmt.Errorf("`umamiToken` is required to create new websites")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.umamiToken != "" {
|
if len(h.websites) == 0 {
|
||||||
websites, err := fetchWebsites(ctx, h.umamiHost, h.umamiToken, h.umamiTeamId)
|
return fmt.Errorf("`websites` should not be empty")
|
||||||
if err != nil {
|
}
|
||||||
return fmt.Errorf("failed to fetch websites: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, website := range *websites {
|
_, err := sendRequest(ctx, h.host+"/health", nil, nil)
|
||||||
if _, ok := h.websites[website.Domain]; ok {
|
if err != nil {
|
||||||
continue
|
return fmt.Errorf("Failed to get health for rybbit: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
h.websites[website.Domain] = website.ID
|
|
||||||
}
|
|
||||||
h.debug("websites fetched: %v", h.websites)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package traefik_umami_feeder
|
package traefik_rybbit_feeder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -9,11 +9,11 @@ import (
|
||||||
|
|
||||||
func TestTraefikUmamiFeeder(t *testing.T) {
|
func TestTraefikUmamiFeeder(t *testing.T) {
|
||||||
cfg := CreateConfig()
|
cfg := CreateConfig()
|
||||||
cfg.UmamiHost = "http://localhost:3000"
|
cfg.Host = "http://localhost:3000"
|
||||||
cfg.UmamiUsername = "admin"
|
cfg.APIKey = "8e39c6ad-e44a-4d3e-be98-015db2d62d40"
|
||||||
cfg.UmamiPassword = "umami"
|
cfg.Websites = map[string]string{
|
||||||
cfg.UmamiTeamId = "8e39c6ad-e44a-4d3e-be98-015db2d62d40"
|
"localhost": "4",
|
||||||
cfg.CreateNewWebsites = true
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
|
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package traefik_umami_feeder
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type authRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type authResponse struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getToken(ctx context.Context, umamiHost string, umamiUsername string, umamiPassword string) (string, error) {
|
|
||||||
var result authResponse
|
|
||||||
err := sendRequestAndParse(ctx, umamiHost+"/api/auth/login", authRequest{
|
|
||||||
Username: umamiUsername,
|
|
||||||
Password: umamiPassword,
|
|
||||||
}, nil, &result)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Token, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package traefik_umami_feeder
|
package traefik_rybbit_feeder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
package traefik_umami_feeder
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type websitesResponse struct {
|
|
||||||
Data []Website `json:"data"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"pageSize"`
|
|
||||||
OrderBy string `json:"orderBy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Website struct {
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
TeamId string `json:"teamId,omitempty"`
|
|
||||||
Domain string `json:"domain,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func createWebsite(ctx context.Context, umamiHost string, umamiToken string, teamId string, websiteDomain string) (*Website, error) {
|
|
||||||
var headers = make(http.Header)
|
|
||||||
headers.Set("Authorization", "Bearer "+umamiToken)
|
|
||||||
|
|
||||||
var result Website
|
|
||||||
err := sendRequestAndParse(ctx, umamiHost+"/api/websites", Website{
|
|
||||||
Name: websiteDomain,
|
|
||||||
Domain: websiteDomain,
|
|
||||||
TeamId: teamId,
|
|
||||||
}, headers, &result)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchWebsites(ctx context.Context, umamiHost string, umamiToken string, teamId string) (*[]Website, error) {
|
|
||||||
var headers = make(http.Header)
|
|
||||||
headers.Set("Authorization", "Bearer "+umamiToken)
|
|
||||||
|
|
||||||
url := umamiHost + "/api/websites?pageSize=200"
|
|
||||||
if len(teamId) != 0 {
|
|
||||||
url = umamiHost + "/api/teams/" + teamId + "/websites?pageSize=200"
|
|
||||||
}
|
|
||||||
|
|
||||||
var result websitesResponse
|
|
||||||
err := sendRequestAndParse(ctx, url, nil, headers, &result)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &result.Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWebsiteId(h *UmamiFeeder, hostname string) string {
|
|
||||||
h.websitesMutex.RLock()
|
|
||||||
websiteId, ok := h.websites[hostname]
|
|
||||||
h.websitesMutex.RUnlock()
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return websiteId
|
|
||||||
}
|
|
||||||
|
|
||||||
h.websitesMutex.Lock()
|
|
||||||
defer h.websitesMutex.Unlock()
|
|
||||||
|
|
||||||
// Create a background context for the API call
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
website, err := createWebsite(ctx, h.umamiHost, h.umamiToken, h.umamiTeamId, hostname)
|
|
||||||
if err != nil {
|
|
||||||
h.error("failed to create website: " + err.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
h.websites[website.Domain] = website.ID
|
|
||||||
h.debug("website created '%s': %s", website.Domain, website.ID)
|
|
||||||
return website.ID
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package traefik_umami_feeder
|
package traefik_rybbit_feeder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -8,48 +8,48 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UmamiEvent struct {
|
type RybbitEvent struct {
|
||||||
Website string `json:"website"` // Website ID
|
APIKey string `json:"api_key"`
|
||||||
Hostname string `json:"hostname"` // Name of host
|
SiteID string `json:"site_id"`
|
||||||
Language string `json:"language,omitempty"` // Language of visitor (ex. "en-US")
|
Type string `json:"type"`
|
||||||
Referrer string `json:"referrer,omitempty"` // Referrer URL
|
Pathname string `json:"pathname"`
|
||||||
Url string `json:"url"` // Page URL
|
Hostname string `json:"hostname,omitempty"`
|
||||||
Ip string `json:"ip,omitempty"` // IP address
|
IP string `json:"ip_address,omitempty"`
|
||||||
UserAgent string `json:"userAgent,omitempty"` // User agent
|
UserAgent string `json:"user_agent,omitempty"`
|
||||||
Timestamp int64 `json:"timestamp,omitempty"` // UNIX timestamp in seconds
|
Language string `json:"language,omitempty"`
|
||||||
//Data map[string]interface{} `json:"data,omitempty"` // Additional data for the event
|
EventName string `json:"event_name,omitempty"`
|
||||||
//Name string `json:"name,omitempty"` // Event name (for custom events)
|
Referrer string `json:"referrer,omitempty"`
|
||||||
//Screen string `json:"screen,omitempty"` // Screen resolution (ex. "1920x1080")
|
Properties string `json:"properties,omitempty"`
|
||||||
//Title string `json:"title,omitempty"` // Page title
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SendBody struct {
|
type SendBody struct {
|
||||||
Payload *UmamiEvent `json:"payload"`
|
Payload *RybbitEvent `json:"payload"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) {
|
func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) {
|
||||||
hostname := parseDomainFromHost(req.Host)
|
hostname := parseDomainFromHost(req.Host)
|
||||||
websiteId := getWebsiteId(h, hostname)
|
websiteId, ok := h.websites[hostname]
|
||||||
|
|
||||||
if websiteId == "" {
|
if !ok {
|
||||||
h.error("tracking skipped, websiteId is unknown: " + hostname)
|
h.error("tracking skipped, site-id is unknown: " + hostname)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
event := &UmamiEvent{
|
rEvent := &RybbitEvent{
|
||||||
|
APIKey: h.apiKey,
|
||||||
|
SiteID: websiteId,
|
||||||
|
Type: "pageview",
|
||||||
|
Pathname: req.URL.Path,
|
||||||
Hostname: hostname,
|
Hostname: hostname,
|
||||||
Language: parseAcceptLanguage(req.Header.Get("Accept-Language")),
|
IP: extractRemoteIP(req),
|
||||||
Referrer: req.Referer(),
|
|
||||||
Url: req.URL.String(),
|
|
||||||
Ip: extractRemoteIP(req),
|
|
||||||
UserAgent: req.Header.Get("User-Agent"),
|
UserAgent: req.Header.Get("User-Agent"),
|
||||||
Timestamp: time.Now().Unix(),
|
Referrer: req.Referer(),
|
||||||
Website: websiteId,
|
Language: parseAcceptLanguage(req.Header.Get("Accept-Language")),
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case h.queue <- event:
|
case h.queue <- rEvent:
|
||||||
default:
|
default:
|
||||||
h.error("failed to submit event: queue full")
|
h.error("failed to submit event: queue full")
|
||||||
}
|
}
|
||||||
|
|
@ -108,16 +108,18 @@ func (h *UmamiFeeder) umamiEventFeeder(ctx context.Context) (err error) {
|
||||||
|
|
||||||
func (h *UmamiFeeder) reportEventsToUmami(ctx context.Context, events []*SendBody) {
|
func (h *UmamiFeeder) reportEventsToUmami(ctx context.Context, events []*SendBody) {
|
||||||
h.debug("reporting %d events", len(events))
|
h.debug("reporting %d events", len(events))
|
||||||
resp, err := sendRequest(ctx, h.umamiHost+"/api/batch", events, nil)
|
for _, value := range events {
|
||||||
if err != nil {
|
resp, err := sendRequest(ctx, h.host+"/api/track", value.Payload, nil)
|
||||||
h.error("failed to send tracking: " + err.Error())
|
if err != nil {
|
||||||
return
|
h.error("failed to send tracking: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.isDebug {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
h.debug("%v: %s", resp.Status, string(bodyBytes))
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
if h.isDebug {
|
|
||||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
||||||
h.debug("%v: %s", resp.Status, string(bodyBytes))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue