├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── header.png ├── cmd └── main.go ├── docker-compose.yml ├── go.mod ├── go.sum └── pkg ├── analytics └── influx.go └── database └── metadata.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | cmd/__debug* 4 | cmd/gifmo.db -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY cmd ./cmd 9 | COPY pkg ./pkg 10 | 11 | RUN go build -o server ./cmd 12 | 13 | FROM alpine:latest 14 | COPY --from=builder /app/server ./server 15 | 16 | # Expose the port that the server listens on 17 | EXPOSE 8080 18 | 19 | # Set the entry point for the container 20 | CMD ["./server"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aleksei Cherepanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![header](assets/header.png) 2 | 3 | # 🚀 Universal Redirect 4 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/ftp27/go-universal-redirect) 5 | ![GitHub last commit](https://img.shields.io/github/last-commit/ftp27/go-universal-redirect) 6 | ![GitHub issues](https://img.shields.io/github/issues/ftp27/go-universal-redirect) 7 | ![License](https://img.shields.io/github/license/ftp27/go-universal-redirect) 8 | ![Go Version](https://img.shields.io/github/go-mod/go-version/ftp27/go-universal-redirect) 9 | ![Go Report Card](https://goreportcard.com/badge/github.com/ftp27/go-universal-redirect) 10 | 11 | **Universal Redirect** is a lightweight and efficient server designed to redirect incoming requests to platform-specific URLs. Perfect for directing users to the App Store or Play Store based on their device platform. Additionally, you can append custom metadata to the link and retrieve it in your app. 12 | 13 | ## 📚 Table of Contents 14 | 15 | 1. [🚀 Usage](#usage) 16 | 2. [⚙️ How It Works](#how-it-works) 17 | 3. [🛠️ Installation](#installation) 18 | 1. [📋 Requirements](#requirements) 19 | 2. [📂 Clone the Repository](#clone-the-repository) 20 | 3. [🐳 Docker](#docker) 21 | 4. [🌐 Dokku](#dokku) 22 | 4. [⚙️ Configuration](#configuration) 23 | 5. [📈 Analytics](#analytics) 24 | 25 | ## 🚀 Usage 26 | 27 | 1. Deploy the server on your hosting platform. 28 | 2. Create a link using the following format: `https://your-host.com?meta=your-meta`. 29 | 3. Redirect users to this link. 30 | 4. Retrieve the metadata in your app using a GET request to `https://your-host.com/meta`. 31 | 32 | ## ⚙️ How It Works 33 | 34 | 1. The user clicks the link. 35 | 2. The server reads the user agent to determine the user's platform and caches the client's IP. 36 | 3. The server redirects the user to the appropriate platform-specific URL. 37 | 4. Metadata is saved to the Redis cache. 38 | 5. The app retrieves the metadata from the Redis cache. 39 | 40 | ## 🛠️ Installation 41 | 42 | ### 📋 Requirements 43 | 44 | - Redis server 45 | - Default redirect URL 46 | 47 | ### 📂 Clone the Repository 48 | 49 | ```bash 50 | git clone https://github.com/ftp27/go-universal-redirect.git 51 | cd go-universal-redirect 52 | ``` 53 | 54 | ### 🐳 Docker 55 | 56 | #### Build the Container 57 | 58 | ```bash 59 | docker build -t universal-redirect . 60 | ``` 61 | 62 | #### Run the Container 63 | 64 | ```bash 65 | docker compose up -d 66 | ``` 67 | 68 | ### 🌐 Dokku 69 | 70 | #### Create the App 71 | 72 | ```bash 73 | dokku apps:create universal-redirect 74 | ``` 75 | 76 | #### Create the Redis Service 77 | 78 | ```bash 79 | dokku redis:create universal-redirect 80 | dokku redis:link universal-redirect universal-redirect 81 | ``` 82 | 83 | #### Set Configuration 84 | 85 | **Required:** 86 | 87 | ```bash 88 | dokku config:set universal-redirect LINK_DEFAULT=https://example.com 89 | ``` 90 | 91 | **Optional (InfluxDB):** 92 | 93 | ```bash 94 | dokku config:set universal-redirect INFLUX_TOKEN=... INFLUX_HOST=... INFLUX_DATABASE=... 95 | ``` 96 | 97 | **Optional (Platform-Specific Links):** 98 | 99 | ```bash 100 | dokku config:set universal-redirect LINK_APPSTORE=https://apps.apple.com LINK_GOOGLEPLAY=https://play.google.com 101 | ``` 102 | 103 | #### Configure Ports 104 | 105 | ```bash 106 | dokku ports:add universal-redirect http:80:8080 https:443:8080 107 | ``` 108 | 109 | #### Set Domain (Optional) 110 | 111 | ```bash 112 | dokku domains:set universal-redirect example.com 113 | ``` 114 | 115 | #### Set SSL (Optional) 116 | 117 | ```bash 118 | dokku letsencrypt:enable universal-redirect 119 | ``` 120 | 121 | #### Add Certificates (Optional) 122 | 123 | ```bash 124 | tar cvf certs.tar server.crt server.key 125 | dokku certs:add universal-redirect < certs.tar 126 | ``` 127 | 128 | #### Deploy the App 129 | 130 | ```bash 131 | git remote add dokku ... # Add the Dokku remote 132 | git push dokku main 133 | ``` 134 | 135 | ## ⚙️ Configuration 136 | 137 | Customize the server by setting the following environment variables: 138 | 139 | - **`REDIS_URL`** - URL of the Redis server **(required)** 140 | - **`LINK_DEFAULT`** - Default redirect URL **(required)** 141 | - **`LINK_APPSTORE`** - App Store redirect URL __(optional)__ 142 | - **`LINK_GOOGLEPLAY`** - Play Store redirect URL __(optional)__ 143 | - **`PORT`** - Server port (default: 8080) 144 | 145 | For InfluxDB analytics, configure the following variables: 146 | 147 | - **`INFLUX_TOKEN`** - InfluxDB token (optional) 148 | - **`INFLUX_HOST`** - InfluxDB host (optional) 149 | - **`INFLUX_DATABASE`** - InfluxDB database (optional) 150 | 151 | ## 📈 Analytics 152 | 153 | Universal Redirect supports InfluxDB for analytics. It sends each click and install event to the `link` measurement, along with platform and type information (`click` or `install`). Here are some example queries: 154 | 155 | - **Total Clicks By Day:** 156 | ```sql 157 | SELECT 158 | DATE_BIN(INTERVAL '1 day', time, '1970-01-01T00:00:00Z'::TIMESTAMP) AS day, 159 | SUM(value) as value 160 | FROM "link" 161 | WHERE time >= now() - INTERVAL '30 day' AND type = 'click' 162 | GROUP BY day 163 | ``` 164 | 165 | ```flux 166 | from(bucket: "") 167 | |> range(start: v.timeRangeStart, stop: v.timeRangeStop) 168 | |> filter(fn: (r) => r["_measurement"] == "link") 169 | |> filter(fn: (r) => r["type"] == "click") 170 | |> group(columns: []) 171 | |> aggregateWindow(every: 1d, fn: sum, createEmpty: false) 172 | |> yield(name: "sum") 173 | ``` 174 | 175 | - **Total Clicks by Platform:** 176 | ```sql 177 | SELECT 178 | SUM("value") AS "total_value", 179 | platform 180 | FROM "link" 181 | WHERE time >= now() - INTERVAL '30 day' AND type = 'click' 182 | GROUP BY platform 183 | ``` 184 | 185 | ```flux 186 | from(bucket: "") 187 | |> range(start: v.timeRangeStart, stop: v.timeRangeStop) 188 | |> filter(fn: (r) => r["_measurement"] == "link") 189 | |> filter(fn: (r) => r["_field"] == "value") 190 | |> group(columns: ["platform"]) 191 | |> aggregateWindow(every: 1d, fn: sum, createEmpty: false) 192 | |> yield(name: "sum") 193 | ``` 194 | -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftp27/go-universal-redirect/06ba738b7b787c95fa9bbabdc91ed09f54367a4e/assets/header.png -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | // Networking 11 | "github.com/mileusna/useragent" 12 | 13 | // Logging 14 | "github.com/rs/zerolog" 15 | "github.com/rs/zerolog/log" 16 | 17 | // Packages 18 | "github.com/ftp27/go-universal-redirect/pkg/analytics" 19 | "github.com/ftp27/go-universal-redirect/pkg/database" 20 | ) 21 | 22 | var ( 23 | port string 24 | defaultLink string 25 | platformLinks map[string]string 26 | 27 | meta *database.Metadata 28 | tracker *analytics.InfluxAnalytics 29 | ) 30 | 31 | func main() { 32 | setuoLog() 33 | setupConfig() 34 | setupMeta() 35 | setupAnalytics() 36 | 37 | startServer() 38 | } 39 | 40 | func setupMeta() { 41 | host, isExists := os.LookupEnv("REDIS_URL") 42 | if !isExists { 43 | log.Fatal().Str("env", "REDIS_URL").Msg("Not found") 44 | return 45 | } 46 | lifetimeStr, isExists := os.LookupEnv("META_LIFETIME") 47 | lifetime := time.Hour 48 | if isExists { 49 | intLifetime, err := strconv.Atoi(lifetimeStr) 50 | if err != nil { 51 | log.Fatal().Err(err).Str("env", "META_LIFETIME").Msg("Failed to parse") 52 | return 53 | } 54 | lifetime = time.Duration(intLifetime) * time.Second 55 | } 56 | 57 | metadata, err := database.NewMetadata(host, lifetime) 58 | if err != nil { 59 | log.Fatal().Err(err).Msg("Failed to create metadata") 60 | return 61 | } 62 | meta = metadata 63 | } 64 | 65 | func setupAnalytics() { 66 | id, isExists := os.LookupEnv("INFLUX_DATABASE") 67 | if isExists { 68 | tracker = analytics.NewInfluxAnalytics(id) 69 | } 70 | } 71 | 72 | func setuoLog() { 73 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 74 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 75 | } 76 | 77 | func setupConfig() { 78 | serverPort, isExists := os.LookupEnv("PORT") 79 | if !isExists { 80 | port = ":8080" 81 | } else { 82 | port = ":" + serverPort 83 | } 84 | link, isExists := os.LookupEnv("LINK_DEFAULT") 85 | if !isExists { 86 | log.Fatal().Str("env", "LINK_DEFAULT").Msg("Not found") 87 | return 88 | } 89 | links := make(map[string]string) 90 | links["iOS"] = os.Getenv("LINK_APPSTORE") 91 | links["Android"] = os.Getenv("LINK_GOOGLEPLAY") 92 | platformLinks = links 93 | defaultLink = link 94 | } 95 | 96 | func startServer() { 97 | log.Info().Str("addr", port).Msg("Starting server") 98 | http.HandleFunc("/", linkHandler) 99 | http.HandleFunc("/meta", metaHandler) 100 | 101 | http.ListenAndServe(port, nil) 102 | } 103 | 104 | func linkHandler(w http.ResponseWriter, r *http.Request) { 105 | platform := getOS(r) 106 | link := getLink(platform) 107 | ip := getIP(r) 108 | 109 | metaData := r.URL.Query().Get("meta") 110 | if metaData != "" { 111 | err := meta.Set(ip, metaData) 112 | if err != nil { 113 | log.Error().Err(err).Str("ip", ip).Msg("Failed to set metadata") 114 | } 115 | } 116 | if tracker != nil { 117 | err := tracker.LogClick(metaData, platform, context.Background()) 118 | if err != nil { 119 | log.Error().Err(err).Str("ip", ip).Msg("Failed to log click") 120 | } 121 | } 122 | 123 | http.Redirect(w, r, link, http.StatusMovedPermanently) 124 | } 125 | 126 | func metaHandler(w http.ResponseWriter, r *http.Request) { 127 | ip := getIP(r) 128 | metaData, _ := meta.Get(ip) 129 | if metaData != "" && tracker != nil { 130 | platform := getOS(r) 131 | err := tracker.LogInstall(metaData, platform, context.Background()) 132 | if err != nil { 133 | log.Error().Err(err).Str("ip", ip).Msg("Failed to log install") 134 | } 135 | } 136 | w.Write([]byte(metaData)) 137 | } 138 | 139 | func getOS(r *http.Request) string { 140 | s := r.Header.Get("User-Agent") 141 | if s == "" { 142 | return "Unknown" 143 | } 144 | return useragent.Parse(s).OS 145 | } 146 | 147 | func getLink(platform string) string { 148 | link := platformLinks[platform] 149 | if link != "" { 150 | return link 151 | } else { 152 | return defaultLink 153 | } 154 | } 155 | 156 | func getIP(r *http.Request) string { 157 | cloudflareIP := r.Header.Get("CF-Connecting-IP") 158 | if cloudflareIP != "" { 159 | return cloudflareIP 160 | } 161 | ip := r.Header.Get("X-Real-IP") 162 | if ip != "" { 163 | return ip 164 | } 165 | return "" 166 | } 167 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: . 6 | environment: 7 | REDIS_URL: redis://redis:6379 8 | LINK_DEFAULT: https://example.com 9 | # Optional: Uncomment to use InfluxDB 10 | # INFLUX_TOKEN: your_influx_token_here 11 | # INFLUX_HOST: your_influx_host_here 12 | # INFLUX_DATABASE: your_influx_database_here 13 | # Optional: Uncomment for platform-specific links 14 | # LINK_APPSTORE: https://apps.apple.com 15 | # LINK_GOOGLEPLAY: https://play.google.com 16 | ports: 17 | - "80:8080" 18 | - "443:8080" 19 | depends_on: 20 | - redis 21 | redis: 22 | image: redis -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ftp27/go-universal-redirect 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/InfluxCommunity/influxdb3-go v0.9.0 7 | github.com/go-redis/redis v6.15.9+incompatible 8 | github.com/mileusna/useragent v1.3.4 9 | github.com/rs/zerolog v1.33.0 10 | ) 11 | 12 | require ( 13 | github.com/apache/arrow/go/v15 v15.0.2 // indirect 14 | github.com/goccy/go-json v0.10.2 // indirect 15 | github.com/google/flatbuffers v24.3.7+incompatible // indirect 16 | github.com/influxdata/line-protocol/v2 v2.2.1 // indirect 17 | github.com/klauspost/compress v1.17.7 // indirect 18 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 19 | github.com/mattn/go-colorable v0.1.13 // indirect 20 | github.com/mattn/go-isatty v0.0.19 // indirect 21 | github.com/onsi/ginkgo v1.16.5 // indirect 22 | github.com/onsi/gomega v1.34.1 // indirect 23 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 24 | github.com/zeebo/xxh3 v1.0.2 // indirect 25 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 26 | golang.org/x/mod v0.19.0 // indirect 27 | golang.org/x/net v0.27.0 // indirect 28 | golang.org/x/sync v0.7.0 // indirect 29 | golang.org/x/sys v0.22.0 // indirect 30 | golang.org/x/text v0.16.0 // indirect 31 | golang.org/x/tools v0.23.0 // indirect 32 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 33 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect 34 | google.golang.org/grpc v1.65.0 // indirect 35 | google.golang.org/protobuf v1.34.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/InfluxCommunity/influxdb3-go v0.9.0 h1:67Zw0JQti07qV8Wu5dn3GoRD8C0pNWTgmTvXP8A/ct0= 2 | github.com/InfluxCommunity/influxdb3-go v0.9.0/go.mod h1:oK0jPp5mL8s8yKIaMVLL+A/A6G48BPGuXSE0ErAbId8= 3 | github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= 4 | github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= 5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/frankban/quicktest v1.11.0/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= 11 | github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= 12 | github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= 13 | github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= 14 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 15 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 16 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 17 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 18 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 19 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 20 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 21 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 22 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 25 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 26 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 27 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 28 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 29 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 30 | github.com/google/flatbuffers v24.3.7+incompatible h1:BxGUkIQnOciBu33bd5BdvqY8Qvo0O/GR4SPhh7x9Ed0= 31 | github.com/google/flatbuffers v24.3.7+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 32 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 33 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 38 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 39 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 40 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 41 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 42 | github.com/influxdata/line-protocol-corpus v0.0.0-20210519164801-ca6fa5da0184/go.mod h1:03nmhxzZ7Xk2pdG+lmMd7mHDfeVOYFyhOgwO61qWU98= 43 | github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937 h1:MHJNQ+p99hFATQm6ORoLmpUCF7ovjwEFshs/NHzAbig= 44 | github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937/go.mod h1:BKR9c0uHSmRgM/se9JhFHtTT7JTO67X23MtKMHtZcpo= 45 | github.com/influxdata/line-protocol/v2 v2.0.0-20210312151457-c52fdecb625a/go.mod h1:6+9Xt5Sq1rWx+glMgxhcg2c0DUaehK+5TDcPZ76GypY= 46 | github.com/influxdata/line-protocol/v2 v2.1.0/go.mod h1:QKw43hdUBg3GTk2iC3iyCxksNj7PX9aUSeYOYE/ceHY= 47 | github.com/influxdata/line-protocol/v2 v2.2.1 h1:EAPkqJ9Km4uAxtMRgUubJyqAr6zgWM0dznKMLRauQRE= 48 | github.com/influxdata/line-protocol/v2 v2.2.1/go.mod h1:DmB3Cnh+3oxmG6LOBIxce4oaL4CPj3OmMPgvauXh+tM= 49 | github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= 50 | github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 51 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 52 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 53 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 54 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 55 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 56 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 57 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 58 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 59 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 60 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 61 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 62 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 63 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 64 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 65 | github.com/mileusna/useragent v1.3.4 h1:MiuRRuvGjEie1+yZHO88UBYg8YBC/ddF6T7F56i3PCk= 66 | github.com/mileusna/useragent v1.3.4/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= 67 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 68 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 69 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 70 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 71 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 72 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 73 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 74 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 75 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 76 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 77 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 78 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 79 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 80 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 81 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 82 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 83 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 85 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 86 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 87 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 88 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 89 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 90 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 91 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 92 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 93 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 94 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 95 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 96 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 97 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 100 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 101 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 102 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 103 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 104 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 105 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 106 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 107 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 108 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 110 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 111 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 112 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 113 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 117 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 118 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 132 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 133 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 134 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 135 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 136 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 137 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 138 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 139 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 140 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 141 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 142 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 145 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 146 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 147 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 148 | gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= 149 | gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= 150 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= 151 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= 152 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 153 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 154 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 155 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 156 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 157 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 158 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 159 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 160 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 161 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 162 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 163 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 164 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 165 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 166 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 167 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 168 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 169 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 170 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 171 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 172 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 173 | -------------------------------------------------------------------------------- /pkg/analytics/influx.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/InfluxCommunity/influxdb3-go/influxdb3" 8 | ) 9 | 10 | type InfluxAnalytics struct { 11 | database string 12 | } 13 | 14 | type Data struct { 15 | Measurement string `lp:"measurement"` 16 | Type string `lp:"tag,type"` 17 | Platform string `lp:"tag,platform"` 18 | Value int64 `lp:"field,value"` 19 | Time time.Time `lp:"timestamp"` 20 | } 21 | 22 | func NewInfluxAnalytics(database string) *InfluxAnalytics { 23 | return &InfluxAnalytics{database: database} 24 | } 25 | 26 | func (a *InfluxAnalytics) LogClick(meta string, platform string, ctx context.Context) error { 27 | client, err := influxdb3.NewFromEnv() 28 | if err != nil { 29 | return err 30 | } 31 | defer client.Close() 32 | point := Data{ 33 | Measurement: "link", 34 | Type: "click", 35 | Platform: platform, 36 | Value: 1, 37 | Time: time.Now(), 38 | } 39 | points := []any{&point} 40 | return client.WriteData( 41 | ctx, 42 | points, 43 | influxdb3.WithDatabase(a.database), 44 | ) 45 | } 46 | 47 | func (a *InfluxAnalytics) LogInstall(meta string, platform string, ctx context.Context) error { 48 | client, err := influxdb3.NewFromEnv() 49 | if err != nil { 50 | return err 51 | } 52 | defer client.Close() 53 | point := Data{ 54 | Measurement: "link", 55 | Type: "install", 56 | Platform: platform, 57 | Value: 1, 58 | Time: time.Now(), 59 | } 60 | points := []any{&point} 61 | return client.WriteData( 62 | ctx, 63 | points, 64 | influxdb3.WithDatabase(a.database), 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/database/metadata.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis" 7 | ) 8 | 9 | type Metadata struct { 10 | rdClient *redis.Client 11 | metaLifetime time.Duration 12 | } 13 | 14 | func NewMetadata(host string, lifetime time.Duration) (*Metadata, error) { 15 | opt, err := redis.ParseURL(host) 16 | if err != nil { 17 | return nil, err 18 | } 19 | db := redis.NewClient(opt) 20 | return &Metadata{ 21 | rdClient: db, 22 | metaLifetime: lifetime, 23 | }, nil 24 | } 25 | 26 | func (m *Metadata) Get(ip string) (string, error) { 27 | return m.rdClient.Get(ip).Result() 28 | } 29 | 30 | func (m *Metadata) Set(ip, value string) error { 31 | return m.rdClient.Set(ip, value, m.metaLifetime).Err() 32 | } 33 | --------------------------------------------------------------------------------