this repo has no description
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add telemetry

Signed-off-by: Xe Iaso <me@xeiaso.net>

+1266 -32
+8
buf.gen.yaml
··· 1 + version: v2 2 + plugins: 3 + - local: protoc-gen-go 4 + out: . 5 + opt: paths=source_relative 6 + - local: protoc-gen-connect-go 7 + out: . 8 + opt: paths=source_relative
+6
buf.lock
··· 1 + # Generated by buf. DO NOT EDIT. 2 + version: v2 3 + deps: 4 + - name: buf.build/googleapis/googleapis 5 + commit: 553fd4b4b3a640be9b69a3fa0c17b383 6 + digest: b5:9e7cb39758f3487f0751765227dc0b9f0ef7a401ab37fa2e97c363446810f4bd0dca1f46dbb793a12d98c019f4028e48d0831b5cb3f09190f068ec1866c69c1d
+7
buf.yaml
··· 1 + version: v2 2 + lint: 3 + use: 4 + - DEFAULT 5 + breaking: 6 + use: 7 + - FILE
+407
bundler/bundler.go
··· 1 + // Copyright 2016 Google LLC. 2 + // Use of this source code is governed by a BSD-style 3 + // license that can be found in the LICENSE file. 4 + 5 + // Package bundler supports bundling (batching) of items. Bundling amortizes an 6 + // action with fixed costs over multiple items. For example, if an API provides 7 + // an RPC that accepts a list of items as input, but clients would prefer 8 + // adding items one at a time, then a Bundler can accept individual items from 9 + // the client and bundle many of them into a single RPC. 10 + // 11 + // This package is experimental and subject to change without notice. 12 + package bundler 13 + 14 + import ( 15 + "context" 16 + "errors" 17 + "sync" 18 + "time" 19 + 20 + "golang.org/x/sync/semaphore" 21 + ) 22 + 23 + type mode int 24 + 25 + const ( 26 + DefaultDelayThreshold = time.Second 27 + DefaultBundleHandlerDeadline = time.Minute 28 + DefaultBundleCountThreshold = 10 29 + DefaultBundleByteThreshold = 1e6 // 1M 30 + DefaultBufferedByteLimit = 1e9 // 1G 31 + ) 32 + 33 + const ( 34 + none mode = iota 35 + add 36 + addWait 37 + ) 38 + 39 + var ( 40 + // ErrOverflow indicates that Bundler's stored bytes exceeds its BufferedByteLimit. 41 + ErrOverflow = errors.New("bundler: reached buffered byte limit") 42 + 43 + // ErrOversizedItem indicates that an item's size exceeds the maximum bundle size. 44 + ErrOversizedItem = errors.New("bundler: item size exceeds bundle byte limit") 45 + 46 + // errMixedMethods indicates that mutually exclusive methods has been 47 + // called subsequently. 48 + errMixedMethods = errors.New("bundler: calls to Add and AddWait cannot be mixed") 49 + ) 50 + 51 + // A Bundler collects items added to it into a bundle until the bundle 52 + // exceeds a given size, then calls a user-provided function to handle the 53 + // bundle. 54 + // 55 + // The exported fields are only safe to modify prior to the first call to Add 56 + // or AddWait. 57 + type Bundler[T any] struct { 58 + // Starting from the time that the first message is added to a bundle, once 59 + // this delay has passed, handle the bundle. The default is DefaultDelayThreshold. 60 + DelayThreshold time.Duration 61 + 62 + // Once a bundle has this many items, handle the bundle. Since only one 63 + // item at a time is added to a bundle, no bundle will exceed this 64 + // threshold, so it also serves as a limit. The default is 65 + // DefaultBundleCountThreshold. 66 + BundleCountThreshold int 67 + 68 + // Once the number of bytes in current bundle reaches this threshold, handle 69 + // the bundle. The default is DefaultBundleByteThreshold. This triggers handling, 70 + // but does not cap the total size of a bundle. 71 + BundleByteThreshold int 72 + 73 + // The maximum size of a bundle, in bytes. Zero means unlimited. 74 + BundleByteLimit int 75 + 76 + // The maximum number of bytes that the Bundler will keep in memory before 77 + // returning ErrOverflow. The default is DefaultBufferedByteLimit. 78 + BufferedByteLimit int 79 + 80 + // The maximum number of handler invocations that can be running at once. 81 + // The default is 1. 82 + HandlerLimit int 83 + 84 + // ContextDeadline is the deadline for the context attached to bundle handling. 85 + ContextDeadline time.Duration 86 + 87 + handler func(context.Context, []T) // called to handle a bundle 88 + 89 + mu sync.Mutex // guards access to fields below 90 + flushTimer *time.Timer // implements DelayThreshold 91 + handlerCount int // # of bundles currently being handled (i.e. handler is invoked on them) 92 + sem *semaphore.Weighted // enforces BufferedByteLimit 93 + semOnce sync.Once // guards semaphore initialization 94 + // The current bundle we're adding items to. Not yet in the queue. 95 + // Appended to the queue once the flushTimer fires or the bundle 96 + // thresholds/limits are reached. If curBundle is nil and tail is 97 + // not, we first try to add items to tail. Once tail is full or handled, 98 + // we create a new curBundle for the incoming item. 99 + curBundle *bundle[T] 100 + // The next bundle in the queue to be handled. Nil if the queue is 101 + // empty. 102 + head *bundle[T] 103 + // The last bundle in the queue to be handled. Nil if the queue is 104 + // empty. If curBundle is nil and tail isn't, we attempt to add new 105 + // items to the tail until if becomes full or has been passed to the 106 + // handler. 107 + tail *bundle[T] 108 + curFlush *sync.WaitGroup // counts outstanding bundles since last flush 109 + prevFlush chan bool // signal used to wait for prior flush 110 + 111 + // The first call to Add or AddWait, mode will be add or addWait respectively. 112 + // If there wasn't call yet then mode is none. 113 + mode mode 114 + // TODO: consider alternative queue implementation for head/tail bundle. see: 115 + // https://code-review.googlesource.com/c/google-api-go-client/+/47991/4/support/bundler/bundler.go#74 116 + } 117 + 118 + // A bundle is a group of items that were added individually and will be passed 119 + // to a handler as a slice. 120 + type bundle[T any] struct { 121 + items []T // slice of T 122 + size int // size in bytes of all items 123 + next *bundle[T] // bundles are handled in order as a linked list queue 124 + flush *sync.WaitGroup // the counter that tracks flush completion 125 + } 126 + 127 + // add appends item to this bundle and increments the total size. It requires 128 + // that b.mu is locked. 129 + func (bu *bundle[T]) add(item T, size int) { 130 + bu.items = append(bu.items, item) 131 + bu.size += size 132 + } 133 + 134 + // New creates a new Bundler. 135 + // 136 + // handler is a function that will be called on each bundle. If itemExample is 137 + // of type T, the argument to handler is of type []T. handler is always called 138 + // sequentially for each bundle, and never in parallel. 139 + // 140 + // Configure the Bundler by setting its thresholds and limits before calling 141 + // any of its methods. 142 + func New[T any](handler func(context.Context, []T)) *Bundler[T] { 143 + b := &Bundler[T]{ 144 + DelayThreshold: DefaultDelayThreshold, 145 + BundleCountThreshold: DefaultBundleCountThreshold, 146 + BundleByteThreshold: DefaultBundleByteThreshold, 147 + BufferedByteLimit: DefaultBufferedByteLimit, 148 + HandlerLimit: 1, 149 + 150 + handler: handler, 151 + curFlush: &sync.WaitGroup{}, 152 + } 153 + return b 154 + } 155 + 156 + func (b *Bundler[T]) initSemaphores() { 157 + // Create the semaphores lazily, because the user may set limits 158 + // after NewBundler. 159 + b.semOnce.Do(func() { 160 + b.sem = semaphore.NewWeighted(int64(b.BufferedByteLimit)) 161 + }) 162 + } 163 + 164 + // enqueueCurBundle moves curBundle to the end of the queue. The bundle may be 165 + // handled immediately if we are below HandlerLimit. It requires that b.mu is 166 + // locked. 167 + func (b *Bundler[T]) enqueueCurBundle() { 168 + // We don't require callers to check if there is a pending bundle. It 169 + // may have already been appended to the queue. If so, return early. 170 + if b.curBundle == nil { 171 + return 172 + } 173 + // If we are below the HandlerLimit, the queue must be empty. Handle 174 + // immediately with a new goroutine. 175 + if b.handlerCount < b.HandlerLimit { 176 + b.handlerCount++ 177 + go b.handle(b.curBundle) 178 + } else if b.tail != nil { 179 + // There are bundles on the queue, so append to the end 180 + b.tail.next = b.curBundle 181 + b.tail = b.curBundle 182 + } else { 183 + // The queue is empty, so initialize the queue 184 + b.head = b.curBundle 185 + b.tail = b.curBundle 186 + } 187 + b.curBundle = nil 188 + if b.flushTimer != nil { 189 + b.flushTimer.Stop() 190 + b.flushTimer = nil 191 + } 192 + } 193 + 194 + // setMode sets the state of Bundler's mode. If mode was defined before 195 + // and passed state is different from it then return an error. 196 + func (b *Bundler[T]) setMode(m mode) error { 197 + b.mu.Lock() 198 + defer b.mu.Unlock() 199 + if b.mode == m || b.mode == none { 200 + b.mode = m 201 + return nil 202 + } 203 + return errMixedMethods 204 + } 205 + 206 + // canFit returns true if bu can fit an additional item of size bytes based 207 + // on the limits of Bundler b. 208 + func (b *Bundler[T]) canFit(bu *bundle[T], size int) bool { 209 + return (b.BundleByteLimit <= 0 || bu.size+size <= b.BundleByteLimit) && 210 + (b.BundleCountThreshold <= 0 || len(bu.items) < b.BundleCountThreshold) 211 + } 212 + 213 + // Add adds item to the current bundle. It marks the bundle for handling and 214 + // starts a new one if any of the thresholds or limits are exceeded. 215 + // The type of item must be assignable to the itemExample parameter of the NewBundler 216 + // method, otherwise there will be a panic. 217 + // 218 + // If the item's size exceeds the maximum bundle size (Bundler.BundleByteLimit), then 219 + // the item can never be handled. Add returns ErrOversizedItem in this case. 220 + // 221 + // If adding the item would exceed the maximum memory allowed 222 + // (Bundler.BufferedByteLimit) or an AddWait call is blocked waiting for 223 + // memory, Add returns ErrOverflow. 224 + // 225 + // Add never blocks. 226 + func (b *Bundler[T]) Add(item T, size int) error { 227 + if err := b.setMode(add); err != nil { 228 + return err 229 + } 230 + // If this item exceeds the maximum size of a bundle, 231 + // we can never send it. 232 + if b.BundleByteLimit > 0 && size > b.BundleByteLimit { 233 + return ErrOversizedItem 234 + } 235 + 236 + // If adding this item would exceed our allotted memory 237 + // footprint, we can't accept it. 238 + // (TryAcquire also returns false if anything is waiting on the semaphore, 239 + // so calls to Add and AddWait shouldn't be mixed.) 240 + b.initSemaphores() 241 + if !b.sem.TryAcquire(int64(size)) { 242 + return ErrOverflow 243 + } 244 + 245 + b.mu.Lock() 246 + defer b.mu.Unlock() 247 + return b.add(item, size) 248 + } 249 + 250 + // add adds item to the tail of the bundle queue or curBundle depending on space 251 + // and nil-ness (see inline comments). It marks curBundle for handling (by 252 + // appending it to the queue) if any of the thresholds or limits are exceeded. 253 + // curBundle is lazily initialized. It requires that b.mu is locked. 254 + func (b *Bundler[T]) add(item T, size int) error { 255 + // If we don't have a curBundle, see if we can add to the queue tail. 256 + if b.tail != nil && b.curBundle == nil && b.canFit(b.tail, size) { 257 + b.tail.add(item, size) 258 + return nil 259 + } 260 + 261 + // If we can't fit in the existing curBundle, move it onto the queue. 262 + if b.curBundle != nil && !b.canFit(b.curBundle, size) { 263 + b.enqueueCurBundle() 264 + } 265 + 266 + // Create a curBundle if we don't have one. 267 + if b.curBundle == nil { 268 + b.curFlush.Add(1) 269 + b.curBundle = &bundle[T]{ 270 + items: []T{}, 271 + flush: b.curFlush, 272 + } 273 + } 274 + 275 + // Add the item. 276 + b.curBundle.add(item, size) 277 + 278 + // If curBundle is ready for handling, move it to the queue. 279 + if b.curBundle.size >= b.BundleByteThreshold || 280 + len(b.curBundle.items) == b.BundleCountThreshold { 281 + b.enqueueCurBundle() 282 + } 283 + 284 + // If we created a new bundle and it wasn't immediately handled, set a timer 285 + if b.curBundle != nil && b.flushTimer == nil { 286 + b.flushTimer = time.AfterFunc(b.DelayThreshold, b.tryHandleBundles) 287 + } 288 + 289 + return nil 290 + } 291 + 292 + // tryHandleBundles is the timer callback that handles or queues any current 293 + // bundle after DelayThreshold time, even if the bundle isn't completely full. 294 + func (b *Bundler[T]) tryHandleBundles() { 295 + b.mu.Lock() 296 + b.enqueueCurBundle() 297 + b.mu.Unlock() 298 + } 299 + 300 + // next returns the next bundle that is ready for handling and removes it from 301 + // the internal queue. It requires that b.mu is locked. 302 + func (b *Bundler[T]) next() *bundle[T] { 303 + if b.head == nil { 304 + return nil 305 + } 306 + out := b.head 307 + b.head = b.head.next 308 + if b.head == nil { 309 + b.tail = nil 310 + } 311 + out.next = nil 312 + return out 313 + } 314 + 315 + // handle calls the user-specified handler on the given bundle. handle is 316 + // intended to be run as a goroutine. After the handler returns, we update the 317 + // byte total. handle continues processing additional bundles that are ready. 318 + // If no more bundles are ready, the handler count is decremented and the 319 + // goroutine ends. 320 + func (b *Bundler[T]) handle(bu *bundle[T]) { 321 + for bu != nil { 322 + ctx, cancel := context.WithTimeout(context.Background(), b.ContextDeadline) 323 + b.handler(ctx, bu.items) 324 + cancel() 325 + bu = b.postHandle(bu) 326 + } 327 + } 328 + 329 + func (b *Bundler[T]) postHandle(bu *bundle[T]) *bundle[T] { 330 + b.mu.Lock() 331 + defer b.mu.Unlock() 332 + 333 + b.sem.Release(int64(bu.size)) 334 + bu.flush.Done() 335 + 336 + bu = b.next() 337 + if bu == nil { 338 + b.handlerCount-- 339 + } 340 + return bu 341 + } 342 + 343 + // AddWait adds item to the current bundle. It marks the bundle for handling and 344 + // starts a new one if any of the thresholds or limits are exceeded. 345 + // 346 + // If the item's size exceeds the maximum bundle size (Bundler.BundleByteLimit), then 347 + // the item can never be handled. AddWait returns ErrOversizedItem in this case. 348 + // 349 + // If adding the item would exceed the maximum memory allowed (Bundler.BufferedByteLimit), 350 + // AddWait blocks until space is available or ctx is done. 351 + // 352 + // Calls to Add and AddWait should not be mixed on the same Bundler. 353 + func (b *Bundler[T]) AddWait(ctx context.Context, item T, size int) error { 354 + if err := b.setMode(addWait); err != nil { 355 + return err 356 + } 357 + // If this item exceeds the maximum size of a bundle, 358 + // we can never send it. 359 + if b.BundleByteLimit > 0 && size > b.BundleByteLimit { 360 + return ErrOversizedItem 361 + } 362 + // If adding this item would exceed our allotted memory footprint, block 363 + // until space is available. The semaphore is FIFO, so there will be no 364 + // starvation. 365 + b.initSemaphores() 366 + if err := b.sem.Acquire(ctx, int64(size)); err != nil { 367 + return err 368 + } 369 + 370 + b.mu.Lock() 371 + defer b.mu.Unlock() 372 + return b.add(item, size) 373 + } 374 + 375 + // Flush invokes the handler for all remaining items in the Bundler and waits 376 + // for it to return. 377 + func (b *Bundler[T]) Flush() { 378 + b.mu.Lock() 379 + 380 + // If a curBundle is pending, move it to the queue. 381 + b.enqueueCurBundle() 382 + 383 + // Store a pointer to the WaitGroup that counts outstanding bundles 384 + // in the current flush and create a new one to track the next flush. 385 + wg := b.curFlush 386 + b.curFlush = &sync.WaitGroup{} 387 + 388 + // Flush must wait for all prior, outstanding flushes to complete. 389 + // We use a channel to communicate completion between each flush in 390 + // the sequence. 391 + prev := b.prevFlush 392 + next := make(chan bool) 393 + b.prevFlush = next 394 + 395 + b.mu.Unlock() 396 + 397 + // Wait until the previous flush is finished. 398 + if prev != nil { 399 + <-prev 400 + } 401 + 402 + // Wait until this flush is finished. 403 + wg.Wait() 404 + 405 + // Allow the next flush to finish. 406 + close(next) 407 + }
+37
bundler/bundler_test.go
··· 1 + package bundler 2 + 3 + import ( 4 + "context" 5 + "testing" 6 + ) 7 + 8 + func TestBundler(t *testing.T) { 9 + input := []int{1, 2, 3, 4} 10 + done := false 11 + b := New[int](func(_ context.Context, data []int) { 12 + if len(data) != 4 { 13 + t.Errorf("Wanted len(data) == %d, got: %d", len(input), len(data)) 14 + } 15 + 16 + sum := 0 17 + const wantSum = 10 18 + for _, i := range data { 19 + sum += i 20 + } 21 + 22 + if sum != wantSum { 23 + t.Errorf("wanted sum of inputs to be %d, got: %d", wantSum, sum) 24 + } 25 + done = true 26 + }) 27 + 28 + for _, i := range input { 29 + b.Add(i, 1) 30 + } 31 + 32 + b.Flush() 33 + 34 + if !done { 35 + t.Fatal("function wasn't called") 36 + } 37 + }
+45 -3
cmd/hythlodaeus/main.go
··· 6 6 "log" 7 7 "os" 8 8 "path/filepath" 9 + "time" 9 10 11 + islog "git.xeserv.us/Techaro/hythlodaeus/internal/slog" 10 12 "git.xeserv.us/Techaro/hythlodaeus/server" 13 + "git.xeserv.us/Techaro/hythlodaeus/telemetry" 11 14 "git.xeserv.us/Techaro/hythlodaeus/watcher" 12 15 "github.com/facebookgo/flagenv" 16 + _ "github.com/joho/godotenv/autoload" 13 17 "golang.org/x/sync/errgroup" 14 18 "k8s.io/client-go/kubernetes" 15 19 "k8s.io/client-go/rest" ··· 17 21 ) 18 22 19 23 var ( 20 - httpBind = flag.String("http-bind", ":80", "the host:port to bind plain HTTP") 21 - httpsBind = flag.String("https-bind", ":443", "the host:port to bind secure HTTPS") 24 + grpcBind = flag.String("grpc-bind", ":8393", "the host:port to bind the grpc API") 25 + grpcCert = flag.String("grpc-cert", "./var/tls.crt", "the TLS cert to serve grpc") 26 + grpcKey = flag.String("grpc-key", "./var/tls.key", "the TLS key to serve grpc") 27 + httpBind = flag.String("http-bind", ":80", "the host:port to bind plain HTTP") 28 + httpsBind = flag.String("https-bind", ":443", "the host:port to bind secure HTTPS") 29 + telemetryEnable = flag.Bool("telemetry-enable", false, "if true, enable request telemetry bundling to object storage") 30 + telemetryBucket = flag.String("telemetry-bucket", "relayd-logs", "object storage bucket to dump logs to") 31 + telemetryPathStyle = flag.Bool("telemetry-path-style", false, "if true, use s3 path style") 32 + telemetryHost = flag.String("telemetry-host", hostname(), "hostname to disambiguate telemetry") 33 + telemetryBundleCount = flag.Int("telemetry-bundle-count", 512, "maximum number of items per telemetry bundle") 34 + telemetryContextDeadline = flag.Duration("telemetry-context-deadline", time.Minute, "maximum time for the telemetry context deadline") 35 + telemetryDelayThreshold = flag.Duration("telemetry-delay-threshold", time.Minute, "how long log messages should live in memory before they are written to object storage") 22 36 ) 23 37 38 + func hostname() string { 39 + name, err := os.Hostname() 40 + if err != nil { 41 + return "hythlodaeus" 42 + } 43 + return name 44 + } 45 + 24 46 func main() { 25 47 flagenv.Parse() 26 48 flag.Parse() 27 49 50 + islog.Init() 51 + 28 52 client, err := kubernetes.NewForConfig(getKubernetesConfig()) 29 53 if err != nil { 30 54 log.Fatalf("can't make kubernetes client: %v", err) 31 55 } 32 56 33 - s := server.New(server.WithHTTPBind(*httpBind), server.WithHTTPSBind(*httpsBind)) 57 + s, err := server.New( 58 + server.WithGRPCBind(*grpcBind), 59 + server.WithGRPCCert(*grpcCert), 60 + server.WithGRPCKey(*grpcKey), 61 + server.WithHTTPBind(*httpBind), 62 + server.WithHTTPSBind(*httpsBind), 63 + server.WithTelemetryConfig(telemetry.Config{ 64 + Bucket: *telemetryBucket, 65 + PathStyle: *telemetryPathStyle, 66 + Host: *telemetryHost, 67 + BundleCount: *telemetryBundleCount, 68 + ContextDeadline: *telemetryContextDeadline, 69 + DelayThreshold: *telemetryDelayThreshold, 70 + }), 71 + ) 72 + if err != nil { 73 + log.Fatalf("can't make server: %v", err) 74 + } 75 + 34 76 w := watcher.New(client, func(payload *watcher.Payload) { 35 77 s.Update(payload) 36 78 })
+1 -1
docker/alpine.Dockerfile
··· 23 23 24 24 COPY --from=build /app/bin/${COMPONENT} /app/bin/${COMPONENT} 25 25 26 - CMD ["/app/bin/${COMPONENT}"] 26 + CMD ["/app/bin/hythlodaeus"] 27 27 28 28 LABEL org.opencontainers.image.source="https://git.xeserv.us/techaro/hivemind"
+30 -1
go.mod
··· 3 3 go 1.24.3 4 4 5 5 require ( 6 + github.com/aws/aws-sdk-go v1.55.7 7 + github.com/aws/aws-sdk-go-v2/config v1.29.14 8 + github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 6 9 github.com/bep/debounce v1.2.0 7 10 github.com/exaring/ja4plus v0.0.1 8 11 github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456 12 + github.com/felixge/httpsnoop v1.0.4 9 13 github.com/google/uuid v1.6.0 14 + github.com/joho/godotenv v1.5.1 15 + github.com/prometheus/client_golang v1.22.0 10 16 golang.org/x/net v0.40.0 11 17 golang.org/x/sync v0.14.0 18 + google.golang.org/grpc v1.72.1 19 + google.golang.org/protobuf v1.36.5 12 20 k8s.io/api v0.33.1 13 21 k8s.io/apimachinery v0.33.1 14 22 k8s.io/client-go v0.33.1 ··· 25 33 github.com/ProtonMail/go-crypto v1.1.6 // indirect 26 34 github.com/Songmu/gitconfig v0.2.0 // indirect 27 35 github.com/TecharoHQ/yeet v0.2.3 // indirect 36 + github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 37 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect 38 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 39 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 40 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 41 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 42 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 43 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect 44 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 45 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 // indirect 46 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 47 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect 48 + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 49 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 50 + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 51 + github.com/aws/smithy-go v1.22.2 // indirect 52 + github.com/beorn7/perks v1.0.1 // indirect 28 53 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect 29 54 github.com/cavaliergopher/cpio v1.0.1 // indirect 55 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 30 56 github.com/cli/go-gh v0.1.0 // indirect 31 57 github.com/cloudflare/circl v1.6.0 // indirect 32 58 github.com/cyphar/filepath-securejoin v0.4.1 // indirect ··· 76 102 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 77 103 github.com/pjbgf/sha1cd v0.3.2 // indirect 78 104 github.com/pkg/errors v0.9.1 // indirect 105 + github.com/prometheus/client_model v0.6.1 // indirect 106 + github.com/prometheus/common v0.62.0 // indirect 107 + github.com/prometheus/procfs v0.15.1 // indirect 79 108 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 80 109 github.com/shopspring/decimal v1.4.0 // indirect 81 110 github.com/skeema/knownhosts v1.3.1 // indirect ··· 95 124 golang.org/x/time v0.9.0 // indirect 96 125 golang.org/x/tools v0.33.0 // indirect 97 126 golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 98 - google.golang.org/protobuf v1.36.5 // indirect 127 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 99 128 gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 100 129 gopkg.in/inf.v0 v0.9.1 // indirect 101 130 gopkg.in/warnings.v0 v0.1.2 // indirect
+76
go.sum
··· 30 30 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 31 31 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 32 32 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 33 + github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= 34 + github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 35 + github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 36 + github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 37 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= 38 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= 39 + github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 40 + github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 41 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 42 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 43 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 44 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 45 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 46 + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 47 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 48 + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 49 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 50 + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 51 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= 52 + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= 53 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 54 + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 55 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 h1:4nm2G6A4pV9rdlWzGMPv4BNtQp22v1hg3yrtkYpeLl8= 56 + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= 57 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 58 + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 59 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= 60 + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= 61 + github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6URlm++yPUt9QPmQ= 62 + github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k= 63 + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 64 + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 65 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 66 + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 67 + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 68 + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 69 + github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 70 + github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 71 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 72 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 33 73 github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= 34 74 github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 35 75 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= ··· 40 80 github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= 41 81 github.com/cavaliergopher/rpm v1.3.0 h1:UHX46sasX8MesUXXQ+UbkFLUX4eUWTlEcX8jcnRBIgI= 42 82 github.com/cavaliergopher/rpm v1.3.0/go.mod h1:vEumo1vvtrHM1Ov86f6+k8j7zNKOxQfHDCAIcR/36ZI= 83 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 84 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 43 85 github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI= 44 86 github.com/cli/go-gh v0.1.0 h1:kMqFmC3ECBrV2UKzlOHjNOTTchExVc5tjNHtCqk/zYk= 45 87 github.com/cli/go-gh v0.1.0/go.mod h1:eTGWl99EMZ+3Iau5C6dHyGAJRRia65MtdBtuhWc+84o= ··· 78 120 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 79 121 github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 80 122 github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 123 + github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 124 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 81 125 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 82 126 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 83 127 github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= ··· 94 138 github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= 95 139 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 96 140 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 141 + github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 142 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 97 143 github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 98 144 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 99 145 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= ··· 123 169 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 124 170 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 125 171 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 172 + github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 173 + github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 126 174 github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 127 175 github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 128 176 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= ··· 151 199 github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 152 200 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 153 201 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 202 + github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 203 + github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 204 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 205 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 154 206 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 155 207 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 156 208 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= ··· 214 266 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 215 267 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 216 268 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 269 + github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 270 + github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 271 + github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 272 + github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 273 + github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 274 + github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 275 + github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 276 + github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 217 277 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 218 278 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 219 279 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= ··· 261 321 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 262 322 gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= 263 323 gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= 324 + go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 325 + go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 326 + go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 327 + go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 328 + go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 329 + go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 330 + go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 331 + go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 332 + go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 333 + go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 334 + go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 335 + go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 264 336 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 265 337 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 266 338 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= ··· 332 404 golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 333 405 golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= 334 406 golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 407 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= 408 + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= 409 + google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= 410 + google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 335 411 google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 336 412 google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 337 413 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+64
internal/slog/slog.go
··· 1 + // Package slog is my set of wrappers around package slog. 2 + package slog 3 + 4 + import ( 5 + "flag" 6 + "fmt" 7 + "io" 8 + "log/slog" 9 + "net/http" 10 + "os" 11 + ) 12 + 13 + var ( 14 + slogLevel = flag.String("slog-level", "INFO", "log level") 15 + 16 + // The current slog handler. 17 + Handler slog.Handler 18 + 19 + leveler *slog.LevelVar 20 + ) 21 + 22 + func Init() { 23 + var programLevel slog.Level 24 + if err := (&programLevel).UnmarshalText([]byte(*slogLevel)); err != nil { 25 + fmt.Fprintf(os.Stderr, "invalid log level %s: %v, using info\n", *slogLevel, err) 26 + programLevel = slog.LevelInfo 27 + } 28 + 29 + leveler = &slog.LevelVar{} 30 + leveler.Set(programLevel) 31 + 32 + h := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ 33 + AddSource: true, 34 + Level: leveler, 35 + }) 36 + slog.SetDefault(slog.New(h)) 37 + 38 + Handler = h 39 + 40 + http.HandleFunc("/.within/debug/slog-level", func(w http.ResponseWriter, r *http.Request) { 41 + var level, old slog.Level 42 + old = leveler.Level() 43 + 44 + if r.Method == http.MethodPost { 45 + data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 64)) 46 + defer r.Body.Close() 47 + if err != nil { 48 + http.Error(w, err.Error(), http.StatusBadRequest) 49 + return 50 + } 51 + 52 + if err := (&level).UnmarshalText(data); err != nil { 53 + http.Error(w, err.Error(), http.StatusBadRequest) 54 + return 55 + } 56 + 57 + leveler.Set(level) 58 + slog.Info("changed level", "from", old, "to", level) 59 + fmt.Fprintln(w, level) 60 + } else { 61 + fmt.Fprintln(w, old) 62 + } 63 + }) 64 + }
+253
proto/relayd/relayd.pb.go
··· 1 + // Code generated by protoc-gen-go. DO NOT EDIT. 2 + // versions: 3 + // protoc-gen-go v1.36.6 4 + // protoc (unknown) 5 + // source: proto/relayd/relayd.proto 6 + 7 + package relayd 8 + 9 + import ( 10 + protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 + protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 + durationpb "google.golang.org/protobuf/types/known/durationpb" 13 + timestamppb "google.golang.org/protobuf/types/known/timestamppb" 14 + reflect "reflect" 15 + sync "sync" 16 + unsafe "unsafe" 17 + ) 18 + 19 + const ( 20 + // Verify that this generated code is sufficiently up-to-date. 21 + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 22 + // Verify that runtime/protoimpl is sufficiently up-to-date. 23 + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 24 + ) 25 + 26 + type RequestLog struct { 27 + state protoimpl.MessageState `protogen:"open.v1"` 28 + RequestDate *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=request_date,json=requestDate,proto3" json:"request_date,omitempty"` 29 + ResponseTime *durationpb.Duration `protobuf:"bytes,2,opt,name=response_time,json=responseTime,proto3" json:"response_time,omitempty"` 30 + Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"` 31 + Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"` 32 + Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"` 33 + Query map[string]string `protobuf:"bytes,6,rep,name=query,proto3" json:"query,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` 34 + Headers map[string]string `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` 35 + RemoteIp string `protobuf:"bytes,8,opt,name=remote_ip,json=remoteIp,proto3" json:"remote_ip,omitempty"` 36 + // Deprecated: Marked as deprecated in proto/relayd/relayd.proto. 37 + Ja3N string `protobuf:"bytes,9,opt,name=ja3n,proto3" json:"ja3n,omitempty"` 38 + Ja4 string `protobuf:"bytes,10,opt,name=ja4,proto3" json:"ja4,omitempty"` 39 + RequestId string `protobuf:"bytes,11,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` 40 + StatusCode int32 `protobuf:"varint,12,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` 41 + BytesWritten int64 `protobuf:"varint,13,opt,name=bytes_written,json=bytesWritten,proto3" json:"bytes_written,omitempty"` 42 + unknownFields protoimpl.UnknownFields 43 + sizeCache protoimpl.SizeCache 44 + } 45 + 46 + func (x *RequestLog) Reset() { 47 + *x = RequestLog{} 48 + mi := &file_proto_relayd_relayd_proto_msgTypes[0] 49 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 + ms.StoreMessageInfo(mi) 51 + } 52 + 53 + func (x *RequestLog) String() string { 54 + return protoimpl.X.MessageStringOf(x) 55 + } 56 + 57 + func (*RequestLog) ProtoMessage() {} 58 + 59 + func (x *RequestLog) ProtoReflect() protoreflect.Message { 60 + mi := &file_proto_relayd_relayd_proto_msgTypes[0] 61 + if x != nil { 62 + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 63 + if ms.LoadMessageInfo() == nil { 64 + ms.StoreMessageInfo(mi) 65 + } 66 + return ms 67 + } 68 + return mi.MessageOf(x) 69 + } 70 + 71 + // Deprecated: Use RequestLog.ProtoReflect.Descriptor instead. 72 + func (*RequestLog) Descriptor() ([]byte, []int) { 73 + return file_proto_relayd_relayd_proto_rawDescGZIP(), []int{0} 74 + } 75 + 76 + func (x *RequestLog) GetRequestDate() *timestamppb.Timestamp { 77 + if x != nil { 78 + return x.RequestDate 79 + } 80 + return nil 81 + } 82 + 83 + func (x *RequestLog) GetResponseTime() *durationpb.Duration { 84 + if x != nil { 85 + return x.ResponseTime 86 + } 87 + return nil 88 + } 89 + 90 + func (x *RequestLog) GetHost() string { 91 + if x != nil { 92 + return x.Host 93 + } 94 + return "" 95 + } 96 + 97 + func (x *RequestLog) GetMethod() string { 98 + if x != nil { 99 + return x.Method 100 + } 101 + return "" 102 + } 103 + 104 + func (x *RequestLog) GetPath() string { 105 + if x != nil { 106 + return x.Path 107 + } 108 + return "" 109 + } 110 + 111 + func (x *RequestLog) GetQuery() map[string]string { 112 + if x != nil { 113 + return x.Query 114 + } 115 + return nil 116 + } 117 + 118 + func (x *RequestLog) GetHeaders() map[string]string { 119 + if x != nil { 120 + return x.Headers 121 + } 122 + return nil 123 + } 124 + 125 + func (x *RequestLog) GetRemoteIp() string { 126 + if x != nil { 127 + return x.RemoteIp 128 + } 129 + return "" 130 + } 131 + 132 + // Deprecated: Marked as deprecated in proto/relayd/relayd.proto. 133 + func (x *RequestLog) GetJa3N() string { 134 + if x != nil { 135 + return x.Ja3N 136 + } 137 + return "" 138 + } 139 + 140 + func (x *RequestLog) GetJa4() string { 141 + if x != nil { 142 + return x.Ja4 143 + } 144 + return "" 145 + } 146 + 147 + func (x *RequestLog) GetRequestId() string { 148 + if x != nil { 149 + return x.RequestId 150 + } 151 + return "" 152 + } 153 + 154 + func (x *RequestLog) GetStatusCode() int32 { 155 + if x != nil { 156 + return x.StatusCode 157 + } 158 + return 0 159 + } 160 + 161 + func (x *RequestLog) GetBytesWritten() int64 { 162 + if x != nil { 163 + return x.BytesWritten 164 + } 165 + return 0 166 + } 167 + 168 + var File_proto_relayd_relayd_proto protoreflect.FileDescriptor 169 + 170 + const file_proto_relayd_relayd_proto_rawDesc = "" + 171 + "\n" + 172 + "\x19proto/relayd/relayd.proto\x12\x17within.website.x.relayd\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xff\x04\n" + 173 + "\n" + 174 + "RequestLog\x12=\n" + 175 + "\frequest_date\x18\x01 \x01(\v2\x1a.google.protobuf.TimestampR\vrequestDate\x12>\n" + 176 + "\rresponse_time\x18\x02 \x01(\v2\x19.google.protobuf.DurationR\fresponseTime\x12\x12\n" + 177 + "\x04host\x18\x03 \x01(\tR\x04host\x12\x16\n" + 178 + "\x06method\x18\x04 \x01(\tR\x06method\x12\x12\n" + 179 + "\x04path\x18\x05 \x01(\tR\x04path\x12D\n" + 180 + "\x05query\x18\x06 \x03(\v2..within.website.x.relayd.RequestLog.QueryEntryR\x05query\x12J\n" + 181 + "\aheaders\x18\a \x03(\v20.within.website.x.relayd.RequestLog.HeadersEntryR\aheaders\x12\x1b\n" + 182 + "\tremote_ip\x18\b \x01(\tR\bremoteIp\x12\x16\n" + 183 + "\x04ja3n\x18\t \x01(\tB\x02\x18\x01R\x04ja3n\x12\x10\n" + 184 + "\x03ja4\x18\n" + 185 + " \x01(\tR\x03ja4\x12\x1d\n" + 186 + "\n" + 187 + "request_id\x18\v \x01(\tR\trequestId\x12\x1f\n" + 188 + "\vstatus_code\x18\f \x01(\x05R\n" + 189 + "statusCode\x12#\n" + 190 + "\rbytes_written\x18\r \x01(\x03R\fbytesWritten\x1a8\n" + 191 + "\n" + 192 + "QueryEntry\x12\x10\n" + 193 + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + 194 + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a:\n" + 195 + "\fHeadersEntry\x12\x10\n" + 196 + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + 197 + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B0Z.git.xeserv.us/Techaro/hythlodaeus/proto/relaydb\x06proto3" 198 + 199 + var ( 200 + file_proto_relayd_relayd_proto_rawDescOnce sync.Once 201 + file_proto_relayd_relayd_proto_rawDescData []byte 202 + ) 203 + 204 + func file_proto_relayd_relayd_proto_rawDescGZIP() []byte { 205 + file_proto_relayd_relayd_proto_rawDescOnce.Do(func() { 206 + file_proto_relayd_relayd_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_relayd_relayd_proto_rawDesc), len(file_proto_relayd_relayd_proto_rawDesc))) 207 + }) 208 + return file_proto_relayd_relayd_proto_rawDescData 209 + } 210 + 211 + var file_proto_relayd_relayd_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 212 + var file_proto_relayd_relayd_proto_goTypes = []any{ 213 + (*RequestLog)(nil), // 0: within.website.x.relayd.RequestLog 214 + nil, // 1: within.website.x.relayd.RequestLog.QueryEntry 215 + nil, // 2: within.website.x.relayd.RequestLog.HeadersEntry 216 + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp 217 + (*durationpb.Duration)(nil), // 4: google.protobuf.Duration 218 + } 219 + var file_proto_relayd_relayd_proto_depIdxs = []int32{ 220 + 3, // 0: within.website.x.relayd.RequestLog.request_date:type_name -> google.protobuf.Timestamp 221 + 4, // 1: within.website.x.relayd.RequestLog.response_time:type_name -> google.protobuf.Duration 222 + 1, // 2: within.website.x.relayd.RequestLog.query:type_name -> within.website.x.relayd.RequestLog.QueryEntry 223 + 2, // 3: within.website.x.relayd.RequestLog.headers:type_name -> within.website.x.relayd.RequestLog.HeadersEntry 224 + 4, // [4:4] is the sub-list for method output_type 225 + 4, // [4:4] is the sub-list for method input_type 226 + 4, // [4:4] is the sub-list for extension type_name 227 + 4, // [4:4] is the sub-list for extension extendee 228 + 0, // [0:4] is the sub-list for field type_name 229 + } 230 + 231 + func init() { file_proto_relayd_relayd_proto_init() } 232 + func file_proto_relayd_relayd_proto_init() { 233 + if File_proto_relayd_relayd_proto != nil { 234 + return 235 + } 236 + type x struct{} 237 + out := protoimpl.TypeBuilder{ 238 + File: protoimpl.DescBuilder{ 239 + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 240 + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_relayd_relayd_proto_rawDesc), len(file_proto_relayd_relayd_proto_rawDesc)), 241 + NumEnums: 0, 242 + NumMessages: 3, 243 + NumExtensions: 0, 244 + NumServices: 0, 245 + }, 246 + GoTypes: file_proto_relayd_relayd_proto_goTypes, 247 + DependencyIndexes: file_proto_relayd_relayd_proto_depIdxs, 248 + MessageInfos: file_proto_relayd_relayd_proto_msgTypes, 249 + }.Build() 250 + File_proto_relayd_relayd_proto = out.File 251 + file_proto_relayd_relayd_proto_goTypes = nil 252 + file_proto_relayd_relayd_proto_depIdxs = nil 253 + }
+22
proto/relayd/relayd.proto
··· 1 + syntax = "proto3"; 2 + package within.website.x.relayd; 3 + option go_package = "git.xeserv.us/Techaro/hythlodaeus/proto/relayd"; 4 + 5 + import "google/protobuf/duration.proto"; 6 + import "google/protobuf/timestamp.proto"; 7 + 8 + message RequestLog { 9 + google.protobuf.Timestamp request_date = 1; 10 + google.protobuf.Duration response_time = 2; 11 + string host = 3; 12 + string method = 4; 13 + string path = 5; 14 + map<string, string> query = 6; 15 + map<string, string> headers = 7; 16 + string remote_ip = 8; 17 + string ja3n = 9 [deprecated = true]; 18 + string ja4 = 10; 19 + string request_id = 11; 20 + int32 status_code = 12; 21 + int64 bytes_written = 13; 22 + }
+34
proto/relayd/requestlog.go
··· 1 + package relayd 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + 7 + "google.golang.org/protobuf/types/known/timestamppb" 8 + ) 9 + 10 + func RequestLogFromRequest(r *http.Request, ipAddress, requestID, ja4 string) *RequestLog { 11 + result := &RequestLog{ 12 + RequestDate: timestamppb.Now(), 13 + Host: r.Host, 14 + Method: r.Method, 15 + Path: r.URL.Path, 16 + Query: map[string]string{}, 17 + Headers: map[string]string{}, 18 + RemoteIp: ipAddress, 19 + Ja4: ja4, 20 + RequestId: requestID, 21 + } 22 + 23 + for k, v := range r.URL.Query() { 24 + result.Query[k] = strings.Join(v, ",") 25 + } 26 + 27 + for k, v := range r.Header { 28 + if !strings.HasPrefix(k, "X-") { 29 + result.Headers[k] = strings.Join(v, ",") 30 + } 31 + } 32 + 33 + return result 34 + }
+45 -4
server/config.go
··· 1 1 package server 2 2 3 + import "git.xeserv.us/Techaro/hythlodaeus/telemetry" 4 + 3 5 type config struct { 4 - httpBind string 5 - httpsBind string 6 + grpcBind string 7 + grpcCert string 8 + grpcKey string 9 + grpcInsecure bool 10 + httpBind string 11 + httpsBind string 12 + telemetryConfig telemetry.Config 6 13 } 7 14 8 15 func defaultConfig() *config { 9 16 return &config{ 10 - httpBind: ":80", 11 - httpsBind: ":443", 17 + grpcBind: ":8393", 18 + grpcInsecure: false, 19 + grpcCert: "", 20 + grpcKey: "", 21 + httpBind: ":80", 22 + httpsBind: ":443", 12 23 } 13 24 } 14 25 15 26 // An Option modifies the config. 16 27 type Option func(*config) 17 28 29 + func WithGRPCBind(grpcBind string) Option { 30 + return func(cfg *config) { 31 + cfg.grpcBind = grpcBind 32 + } 33 + } 34 + 35 + func WithGRPCCert(grpcCert string) Option { 36 + return func(cfg *config) { 37 + cfg.grpcCert = grpcCert 38 + } 39 + } 40 + 41 + func WithGRPCKey(grpcKey string) Option { 42 + return func(cfg *config) { 43 + cfg.grpcKey = grpcKey 44 + } 45 + } 46 + 47 + func WithGRPCInsecure(val bool) Option { 48 + return func(cfg *config) { 49 + cfg.grpcInsecure = val 50 + } 51 + } 52 + 18 53 func WithHTTPBind(httpBind string) Option { 19 54 return func(cfg *config) { 20 55 cfg.httpBind = httpBind ··· 26 61 cfg.httpsBind = httpsBind 27 62 } 28 63 } 64 + 65 + func WithTelemetryConfig(tcfg telemetry.Config) Option { 66 + return func(cfg *config) { 67 + cfg.telemetryConfig = tcfg 68 + } 69 + }
+1 -1
server/route.go
··· 14 14 "k8s.io/apimachinery/pkg/util/intstr" 15 15 ) 16 16 17 - const BackendProtocolAnnotation = "kubernetes-simple-ingress-controller/backend-protocol" 17 + const BackendProtocolAnnotation = "hythlodaeus.techaro.lol/backend-protocol" 18 18 19 19 // A RoutingTable contains the information needed to route a request. 20 20 type RoutingTable struct {
+63 -22
server/server.go
··· 5 5 "crypto/tls" 6 6 "fmt" 7 7 "log/slog" 8 + "net" 8 9 "net/http" 9 10 "net/http/httputil" 10 11 "sync/atomic" 11 12 13 + "git.xeserv.us/Techaro/hythlodaeus/telemetry" 12 14 "git.xeserv.us/Techaro/hythlodaeus/watcher" 13 15 "github.com/exaring/ja4plus" 14 - "github.com/google/uuid" 15 16 "golang.org/x/net/http2" 16 17 "golang.org/x/sync/errgroup" 18 + "google.golang.org/grpc" 19 + "google.golang.org/grpc/credentials" 20 + "google.golang.org/grpc/health" 21 + healthv1 "google.golang.org/grpc/health/grpc_health_v1" 22 + "google.golang.org/grpc/reflection" 23 + ) 24 + 25 + var ( 26 + healthSrv = health.NewServer() 17 27 ) 18 28 19 29 // A Server serves HTTP pages. ··· 21 31 cfg *config 22 32 routingTable atomic.Value 23 33 ja4m *ja4plus.JA4Middleware 34 + sink *telemetry.Sink 24 35 25 36 ready *Event 26 37 } 27 38 28 39 // New creates a new Server. 29 - func New(options ...Option) *Server { 40 + func New(options ...Option) (*Server, error) { 30 41 cfg := defaultConfig() 31 42 for _, o := range options { 32 43 o(cfg) ··· 36 47 ready: NewEvent(), 37 48 ja4m: &ja4plus.JA4Middleware{}, 38 49 } 50 + 51 + sink, err := telemetry.New(context.Background(), cfg.telemetryConfig) 52 + if err != nil { 53 + return nil, err 54 + } 55 + s.sink = sink 56 + 39 57 s.routingTable.Store(NewRoutingTable(nil)) 40 - return s 58 + return s, nil 41 59 } 42 60 43 61 // Run runs the server. ··· 46 64 s.ready.Wait(ctx) 47 65 48 66 var eg errgroup.Group 67 + 68 + eg.Go(func() error { 69 + serverCert, err := tls.LoadX509KeyPair(s.cfg.grpcCert, s.cfg.grpcKey) 70 + if err != nil { 71 + return err 72 + } 73 + srv := grpc.NewServer( 74 + grpc.Creds(credentials.NewTLS(&tls.Config{ 75 + Certificates: []tls.Certificate{serverCert}, 76 + ClientAuth: tls.NoClientCert, 77 + })), 78 + ) 79 + healthv1.RegisterHealthServer(srv, healthSrv) 80 + reflection.Register(srv) 81 + 82 + slog.Info("starting server", "protocol", "grpc", "bind", s.cfg.grpcBind) 83 + 84 + lis, err := net.Listen("tcp", s.cfg.grpcBind) 85 + if err != nil { 86 + return err 87 + } 88 + defer lis.Close() 89 + 90 + healthSrv.SetServingStatus("grpc", healthv1.HealthCheckResponse_SERVING) 91 + 92 + return srv.Serve(lis) 93 + }) 94 + 49 95 eg.Go(func() error { 50 96 srv := http.Server{ 51 97 Addr: s.cfg.httpsBind, 52 - Handler: s.ja4m.Wrap(s), 98 + Handler: s.ja4m.Wrap(s.sink.Middleware(s)), 53 99 ConnState: s.ja4m.ConnStateCallback, 54 100 TLSConfig: &tls.Config{ 55 101 GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { ··· 61 107 }, 62 108 }, 63 109 } 64 - slog.Info("starting server", "protocol", "http", "addr", srv.Addr) 110 + 111 + slog.Info("starting server", "protocol", "https", "addr", srv.Addr) 112 + 113 + healthSrv.SetServingStatus("https", healthv1.HealthCheckResponse_SERVING) 114 + 65 115 err := srv.ListenAndServeTLS("", "") 66 116 if err != nil { 67 117 return fmt.Errorf("error serving tls: %w", err) 68 118 } 69 119 return nil 70 120 }) 121 + 71 122 eg.Go(func() error { 72 123 srv := http.Server{ 73 124 Addr: s.cfg.httpBind, 74 - Handler: s, 125 + Handler: s.sink.Middleware(s), 75 126 } 76 - slog.Info("starting server", "protocol", "https", "addr", srv.Addr) 127 + 128 + slog.Info("starting server", "protocol", "http", "addr", srv.Addr) 129 + 130 + healthSrv.SetServingStatus("http", healthv1.HealthCheckResponse_SERVING) 131 + 77 132 err := srv.ListenAndServe() 78 133 if err != nil { 79 134 return fmt.Errorf("error serving non-tls: %w", err) ··· 85 140 86 141 // ServeHTTP serves an HTTP request. 87 142 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 88 - reqID := uuid.Must(uuid.NewV7()).String() 89 - 90 - ja4 := ja4plus.JA4FromContext(r.Context()) 91 - if ja4 != "" { 92 - r.Header.Set("X-Tls-Fingerprint-Ja4", ja4) 93 - } 94 - 95 - r.Header.Set("X-Forwarded-Host", r.Host) 96 - r.Header.Set("X-Forwarded-Proto", "https") 97 - r.Header.Set("X-Forwarded-Scheme", "https") 98 - r.Header.Set("X-Request-Id", reqID) 99 - r.Header.Set("X-Scheme", "https") 100 - r.Header.Set("X-HTTP-Protocol", r.Proto) 101 - 102 143 backendURL, err := s.routingTable.Load().(*RoutingTable).GetBackend(r.Host, r.URL.Path) 103 144 if err != nil { 104 145 http.Error(w, "upstream server not found", http.StatusNotFound) 105 146 return 106 147 } 107 - slog.Info("proxying request", "host", r.Host, "path", r.URL.Path, "backend", backendURL) 148 + slog.Debug("proxying request", "host", r.Host, "path", r.URL.Path, "backend", backendURL) 108 149 p := httputil.NewSingleHostReverseProxy(backendURL) 109 150 if backendURL.Scheme == "https" { 110 151 p.Transport = &http2.Transport{
+154
telemetry/telemetry.go
··· 1 + package telemetry 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "encoding/json" 7 + "fmt" 8 + "log/slog" 9 + "math/rand/v2" 10 + "net" 11 + "net/http" 12 + "time" 13 + 14 + "git.xeserv.us/Techaro/hythlodaeus/bundler" 15 + "git.xeserv.us/Techaro/hythlodaeus/proto/relayd" 16 + awsConfig "github.com/aws/aws-sdk-go-v2/config" 17 + "github.com/aws/aws-sdk-go-v2/service/s3" 18 + "github.com/aws/aws-sdk-go/aws" 19 + "github.com/exaring/ja4plus" 20 + "github.com/felixge/httpsnoop" 21 + "github.com/google/uuid" 22 + "github.com/prometheus/client_golang/prometheus" 23 + "github.com/prometheus/client_golang/prometheus/promauto" 24 + "google.golang.org/protobuf/types/known/durationpb" 25 + ) 26 + 27 + var ( 28 + responseTime = promauto.NewHistogramVec(prometheus.HistogramOpts{ 29 + Name: "hythlodaeus_response_time", 30 + Help: "response time per domain proxied", 31 + Buckets: prometheus.ExponentialBuckets(float64(time.Microsecond), 2, 24), 32 + }, []string{"host"}) 33 + responseCodes = promauto.NewCounterVec(prometheus.CounterOpts{ 34 + Name: "hythlodaeus_response_codes", 35 + Help: "response codes per domain proxied", 36 + }, []string{"host", "code"}) 37 + ) 38 + 39 + type Config struct { 40 + Bucket string 41 + PathStyle bool 42 + Host string 43 + BundleCount int 44 + ContextDeadline time.Duration 45 + DelayThreshold time.Duration 46 + } 47 + 48 + type Sink struct { 49 + sink *bundler.Bundler[*relayd.RequestLog] 50 + s3c *s3.Client 51 + cfg Config 52 + } 53 + 54 + func New(ctx context.Context, cfg Config) (*Sink, error) { 55 + slog.Info("telemetry enabled") 56 + 57 + acfg, err := awsConfig.LoadDefaultConfig(ctx) 58 + if err != nil { 59 + return nil, err 60 + } 61 + 62 + s3c := s3.NewFromConfig(acfg, func(o *s3.Options) { 63 + o.UsePathStyle = true 64 + }) 65 + 66 + result := &Sink{ 67 + s3c: s3c, 68 + cfg: cfg, 69 + } 70 + 71 + result.sink = bundler.New(result.WriteBundle) 72 + result.sink.DelayThreshold = cfg.DelayThreshold 73 + result.sink.BundleCountThreshold = cfg.BundleCount 74 + result.sink.ContextDeadline = cfg.ContextDeadline 75 + 76 + return result, nil 77 + } 78 + 79 + func (s *Sink) Add(item *relayd.RequestLog) { 80 + s.sink.Add(item, 1) 81 + } 82 + 83 + func (s *Sink) WriteBundle(ctx context.Context, items []*relayd.RequestLog) { 84 + if err := s.writeBundle(ctx, items); err != nil { 85 + slog.Error("failed writing", "itemCount", len(items), "err", err) 86 + for _, item := range items { 87 + item := item 88 + // 1 in 8 chance to drop 89 + if rand.IntN(8) != 4 /* chosen by fair dice roll */ { 90 + go func(rl *relayd.RequestLog) { 91 + s.sink.Add(rl, 1) 92 + }(item) 93 + } 94 + } 95 + } 96 + } 97 + 98 + func (s *Sink) writeBundle(ctx context.Context, items []*relayd.RequestLog) error { 99 + buf := bytes.NewBuffer(nil) 100 + enc := json.NewEncoder(buf) 101 + 102 + for _, item := range items { 103 + if err := enc.Encode(item); err != nil { 104 + return err 105 + } 106 + } 107 + 108 + id := uuid.Must(uuid.NewV7()).String() 109 + 110 + if _, err := s.s3c.PutObject(ctx, &s3.PutObjectInput{ 111 + Body: buf, 112 + Bucket: aws.String(s.cfg.Bucket), 113 + Key: aws.String(s.cfg.Host + "/" + id + ".jsonl"), 114 + ContentType: aws.String("application/jsonl"), 115 + }); err != nil { 116 + return err 117 + } 118 + 119 + return nil 120 + } 121 + 122 + func (s *Sink) Middleware(next http.Handler) http.Handler { 123 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 + host, _, _ := net.SplitHostPort(r.RemoteAddr) 125 + if host != "" { 126 + r.Header.Set("X-Real-Ip", host) 127 + } 128 + 129 + ja4 := ja4plus.JA4FromContext(r.Context()) 130 + if ja4 != "" { 131 + r.Header.Set("X-Tls-Fingerprint-Ja4", ja4) 132 + } 133 + 134 + reqID := uuid.Must(uuid.NewV7()).String() 135 + rl := relayd.RequestLogFromRequest(r, host, reqID, ja4) 136 + 137 + r.Header.Set("X-Forwarded-Host", r.Host) 138 + r.Header.Set("X-Forwarded-Proto", "https") 139 + r.Header.Set("X-Forwarded-Scheme", "https") 140 + r.Header.Set("X-Request-Id", reqID) 141 + r.Header.Set("X-Scheme", "https") 142 + r.Header.Set("X-HTTP-Protocol", r.Proto) 143 + 144 + m := httpsnoop.CaptureMetrics(next, w, r) 145 + rl.ResponseTime = durationpb.New(m.Duration) 146 + rl.StatusCode = int32(m.Code) 147 + rl.BytesWritten = m.Written 148 + 149 + responseTime.WithLabelValues(r.Host).Observe(float64(m.Duration)) 150 + responseCodes.WithLabelValues(r.Host, fmt.Sprint(m.Code)).Add(1) 151 + 152 + s.Add(rl) 153 + }) 154 + }
+13
test-app.yaml
··· 1 + apiVersion: x.within.website/v1 2 + kind: App 3 + metadata: 4 + name: httpdebug 5 + 6 + spec: 7 + image: ghcr.io/xe/x/httpdebug:latest 8 + autoUpdate: true 9 + 10 + ingress: 11 + enabled: true 12 + host: httpdebug.shiroko.within.lgbt 13 + className: traefik