├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── dependabot.yml │ └── ci.yml ├── .gitignore ├── internal ├── service │ ├── testdata │ │ └── sample.pdf │ ├── service.go │ ├── worker_test.go │ └── worker.go ├── internal.go ├── domain │ └── domain.go ├── repository │ ├── redis_test.go │ └── redis.go ├── transport │ ├── transport.go │ ├── handler_test.go │ ├── server.go │ ├── middleware.go │ └── handler.go └── client.go ├── README.md ├── misc └── golangci │ └── config.yml ├── LICENSE ├── cmd └── main.go ├── go.mod └── go.sum /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Nitro/sign-platform -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor. 2 | /.vscode 3 | 4 | # Temporary files. 5 | /cmd/main 6 | /cmd/cmd 7 | .DS_Store -------------------------------------------------------------------------------- /internal/service/testdata/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitro/lazyraster/main/internal/service/testdata/sample.pdf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: monday 8 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | workflow_run: 4 | types: 5 | - completed 6 | workflows: 7 | - 'CI' 8 | 9 | jobs: 10 | dependabot: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Merge the PR 15 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 16 | uses: ridedott/merge-me-action@v2 17 | with: 18 | GITHUB_TOKEN: ${{ secrets.NITRO_ROBOT_COMMIT_TOKEN }} 19 | PRESET: DEPENDABOT_MINOR 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lazyraster 2 | Lazyraster is a HTTP service to convert PDF pages into PNG built on top of lazypdf. 3 | 4 | ## Run 5 | Environment variables: 6 | | Options | Description | 7 | | ----------------------- | ------------------------------------------------------------------------------------- | 8 | | `URL_SIGNING_SECRET` | Secret used to check if the request is valid. | 9 | | `ENABLE_DATADOG` | Enable Datadog. | 10 | | `STORAGE_BUCKET_REGION` | Map of the region a bucket belongs to: `eu-west-1:bucket1,bucket2;us-west-1:bucket3`. | 11 | 12 | ```go 13 | go run cmd/main.go 14 | ``` 15 | 16 | ## Testing 17 | ```go 18 | go test -v -race -cover ./... 19 | ``` 20 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/rs/zerolog" 8 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 9 | ) 10 | 11 | type datadogLogger struct { 12 | logger zerolog.Logger 13 | } 14 | 15 | func (dl datadogLogger) Log(msg string) { 16 | dl.logger.Info().Msg(msg) 17 | } 18 | 19 | func traceLogger(enabled bool) func(context.Context, zerolog.Logger) (zerolog.Logger, error) { 20 | return func(ctx context.Context, logger zerolog.Logger) (zerolog.Logger, error) { 21 | if !enabled { 22 | return logger, nil 23 | } 24 | 25 | span, ok := tracer.SpanFromContext(ctx) 26 | if !ok { 27 | return logger, errors.New("could not found a span inside the context") 28 | } 29 | 30 | traceLogger := logger.With().Fields(map[string]interface{}{ 31 | "dd": map[string]uint64{ 32 | "trace_id": span.Context().TraceID(), 33 | "span_id": span.Context().SpanID(), 34 | }, 35 | }).Logger() 36 | return traceLogger, nil 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Sentinel errors. 8 | var ( 9 | ErrClient = ServiceError{origin: "client"} 10 | ErrNotFound = ServiceError{origin: "notFound"} 11 | ) 12 | 13 | // ServiceError has detailed information about errors from the service package. 14 | type ServiceError struct { 15 | base error 16 | origin string 17 | } 18 | 19 | // Is checks if the given error and the current ServiceError are the same. 20 | func (se ServiceError) Is(target error) bool { 21 | var err ServiceError 22 | if !errors.As(target, &err) { 23 | return false 24 | } 25 | return se.origin == err.origin 26 | } 27 | 28 | // Error is used to output the error message. 29 | func (se ServiceError) Error() string { 30 | return se.base.Error() 31 | } 32 | 33 | func newClientError(err error) error { 34 | return ServiceError{base: err, origin: "client"} 35 | } 36 | 37 | func newNotFoundError(err error) error { 38 | return ServiceError{base: err, origin: "notFound"} 39 | } 40 | -------------------------------------------------------------------------------- /misc/golangci/config.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - bodyclose 5 | - dupl 6 | - errcheck 7 | - gochecknoglobals 8 | - gochecknoinits 9 | - goconst 10 | - gocritic 11 | - gocyclo 12 | - gofmt 13 | - goimports 14 | - gosec 15 | - gosimple 16 | - govet 17 | - lll 18 | - ineffassign 19 | - misspell 20 | - nakedret 21 | - prealloc 22 | - staticcheck 23 | - stylecheck 24 | - typecheck 25 | - unconvert 26 | - unparam 27 | - unused 28 | - dogsled 29 | - godox 30 | - whitespace 31 | 32 | linter-settings: 33 | goimports: 34 | local-prefixes: github.com/nitro/lazyraster 35 | 36 | errcheck: 37 | check-type-assertions: true 38 | check-blank: true 39 | 40 | unused: 41 | check-exported: true 42 | 43 | unparam: 44 | check-exported: true 45 | 46 | prealloc: 47 | for-loops: true 48 | 49 | gocritic: 50 | enabled-tags: 51 | - diagnostic 52 | - style 53 | - performance 54 | - experimental 55 | - opinionated 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2021 Nitro Software 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /internal/domain/domain.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type AnnotationLocation struct { 4 | X float64 `json:"x"` 5 | Y float64 `json:"y"` 6 | } 7 | 8 | type AnnotationSize struct { 9 | Height float64 `json:"height"` 10 | Width float64 `json:"width"` 11 | } 12 | 13 | type AnnotationTextFont struct { 14 | Family string `json:"family"` 15 | Size float64 `json:"size"` 16 | } 17 | 18 | type AnnotationImage struct { 19 | Page int `json:"page"` 20 | Location AnnotationLocation `json:"location"` 21 | Size AnnotationSize `json:"size"` 22 | ImageLocation string `json:"imageLocation"` 23 | } 24 | 25 | type AnnotationText struct { 26 | Value string `json:"value"` 27 | Page int `json:"page"` 28 | Location AnnotationLocation `json:"location"` 29 | Font AnnotationTextFont `json:"font"` 30 | Size AnnotationSize `json:"size"` 31 | } 32 | 33 | type AnnotationCheckbox struct { 34 | Value bool `json:"value"` 35 | Page int `json:"page"` 36 | Location AnnotationLocation `json:"location"` 37 | Size AnnotationSize `json:"size"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/repository/redis_test.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nitro/lazyraster/v2/internal/domain" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRedisClientParseAnnotations(t *testing.T) { 11 | payload := ` 12 | [ 13 | {"type":"checkbox","location":{"x":1.0,"y":1.0},"page":1,"size":{"height":1.0,"width":1.0},"value":true}, 14 | { 15 | "type":"image", 16 | "imageLocation": "imageLocation", 17 | "location": {"x":2.0,"y":2.0}, 18 | "page": 2, 19 | "size": {"height":2.0,"width":2.0} 20 | }, 21 | {"type":"text","font":{"family":"font","size":3.0},"location":{"x":3.0,"y":3.0},"page":3,"value":"text"} 22 | ] 23 | ` 24 | expected := []any{ 25 | domain.AnnotationCheckbox{ 26 | Location: domain.AnnotationLocation{ 27 | X: 1.0, 28 | Y: 1.0, 29 | }, 30 | Page: 1, 31 | Size: domain.AnnotationSize{ 32 | Height: 1.0, 33 | Width: 1.0, 34 | }, 35 | Value: true, 36 | }, 37 | domain.AnnotationImage{ 38 | ImageLocation: "imageLocation", 39 | Page: 2, 40 | Location: domain.AnnotationLocation{ 41 | X: 2.0, 42 | Y: 2.0, 43 | }, 44 | Size: domain.AnnotationSize{ 45 | Height: 2.0, 46 | Width: 2.0, 47 | }, 48 | }, 49 | domain.AnnotationText{ 50 | Value: "text", 51 | Page: 3, 52 | Location: domain.AnnotationLocation{ 53 | X: 3.0, 54 | Y: 3.0, 55 | }, 56 | Font: domain.AnnotationTextFont{ 57 | Family: "font", 58 | Size: 3, 59 | }, 60 | }, 61 | } 62 | 63 | var c RedisClient 64 | result, err := c.parseAnnotations(payload) 65 | require.NoError(t, err) 66 | require.Equal(t, expected, result) 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "**" 5 | - "!main" 6 | 7 | name: CI 8 | jobs: 9 | quality: 10 | name: Quality 11 | timeout-minutes: 10 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest] 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Setup Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.25.3 22 | 23 | - name: Go cache 24 | id: cache 25 | uses: actions/cache@v3 26 | with: 27 | path: | 28 | ~/.cache/go-build 29 | ~/Library/Caches/go-build 30 | ~/go/pkg/mod 31 | ~/go/bin 32 | key: ${{ matrix.os }}-go-${{ hashFiles('**/go.sum') }} 33 | restore-keys: | 34 | ${{ matrix.os }}-go- 35 | 36 | - name: Install dependencies 37 | if: steps.cache.outputs.cache-hit != 'true' 38 | run: | 39 | go install github.com/mfridman/tparse@v0.16.0 40 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 41 | 42 | - name: Checkout code 43 | uses: actions/checkout@v3 44 | 45 | - name: Dependency linter 46 | if: matrix.os == 'ubuntu-latest' 47 | run: | 48 | go mod tidy 49 | git add . 50 | git diff --cached --exit-code 51 | 52 | - name: Build 53 | run: go build ./... 54 | 55 | - name: Test 56 | run: go test -race -cover -covermode=atomic -json ./... | tparse -all 57 | 58 | - name: Go golangci-lint 59 | if: matrix.os == 'ubuntu-latest' 60 | run: golangci-lint run -c misc/golangci/config.yml ./... 61 | -------------------------------------------------------------------------------- /internal/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | const ( 12 | maxBodySize = 100000 // 100kb. 13 | ) 14 | 15 | type traceExtractor func(context.Context, zerolog.Logger) (zerolog.Logger, error) 16 | 17 | type writer struct { 18 | logger zerolog.Logger 19 | traceExtractor traceExtractor 20 | } 21 | 22 | func (wrt writer) response(ctx context.Context, w http.ResponseWriter, r interface{}, status int, contentType string) { 23 | logger, err := wrt.traceExtractor(ctx, wrt.logger) 24 | if err != nil { 25 | logger.Err(err).Msg("Fail to extract the tracing ids") 26 | return 27 | } 28 | 29 | if r == nil { 30 | w.WriteHeader(status) 31 | return 32 | } 33 | w.Header().Set("Content-Type", contentType) 34 | w.WriteHeader(status) 35 | 36 | content, err := json.Marshal(r) 37 | if err != nil { 38 | logger.Err(err).Msg("Fail to marshal the response") 39 | return 40 | } 41 | 42 | writed, err := w.Write(content) 43 | if err != nil { 44 | logger.Err(err).Msg("Fail to write the payload") 45 | return 46 | } 47 | if writed != len(content) { 48 | logger.Error().Msgf("Invalid quantity of writed bytes, expected %d and got %d", len(content), writed) 49 | } 50 | } 51 | 52 | // Error is used to generate a proper error content to be sent to the client. 53 | func (wrt writer) error(ctx context.Context, w http.ResponseWriter, title string, err error, status int) { 54 | resp := struct { 55 | Error struct { 56 | Title string `json:"title"` 57 | Detail string `json:"detail,omitempty"` 58 | } `json:"error"` 59 | }{} 60 | resp.Error.Title = title 61 | if err != nil { 62 | resp.Error.Detail = err.Error() 63 | } 64 | wrt.response(ctx, w, &resp, status, "application/json") 65 | } 66 | -------------------------------------------------------------------------------- /internal/transport/handler_test.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "strconv" 9 | "testing" 10 | "time" 11 | 12 | "github.com/rs/zerolog" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestHandlerURLToVerify(t *testing.T) { 17 | tests := []struct { 18 | path string 19 | expected string 20 | }{ 21 | { 22 | path: "/path", 23 | expected: "/path", 24 | }, 25 | { 26 | path: "/path?page=1", 27 | expected: "/path?page=1", 28 | }, 29 | { 30 | path: "/path?token=2", 31 | expected: "/path?token=2", 32 | }, 33 | { 34 | path: "/path?page=1&token=2", 35 | expected: "/path?page=1&token=2", 36 | }, 37 | { 38 | path: "/path?page=1&token=2&scale=3&width=4", 39 | expected: "/path?page=1&token=2", 40 | }, 41 | { 42 | path: "/path?page=1&token=2&scale=3&width=4&token-ttl=5", 43 | expected: "/path?page=1&token=2&token-ttl=5", 44 | }, 45 | } 46 | for i, tt := range tests { 47 | t.Run(fmt.Sprintf("Scenario %d", i), func(t *testing.T) { 48 | t.Parallel() 49 | var h handler 50 | req, err := http.NewRequest(http.MethodGet, tt.path, nil) 51 | require.NoError(t, err) 52 | require.Equal(t, tt.expected, h.urlToVerify(req)) 53 | }) 54 | } 55 | } 56 | 57 | func TestHandlerDocumentTokenTTLExpired(t *testing.T) { 58 | ttl := time.Now().UTC().Add(-1 * time.Hour).Unix() 59 | strconv.FormatInt(ttl, 10) 60 | path := fmt.Sprintf("/documents?page=1&token-ttl=%s", strconv.FormatInt(ttl, 10)) 61 | req := httptest.NewRequest("GET", path, nil) 62 | rr := httptest.NewRecorder() 63 | traceExtractor := func(context.Context, zerolog.Logger) (zerolog.Logger, error) { 64 | return zerolog.Nop(), nil 65 | } 66 | h := handler{ 67 | traceExtractor: traceExtractor, 68 | writer: writer{ 69 | logger: zerolog.Nop(), 70 | traceExtractor: traceExtractor, 71 | }, 72 | } 73 | h.document(rr, req) 74 | require.Equal(t, http.StatusUnauthorized, rr.Code) 75 | } 76 | -------------------------------------------------------------------------------- /internal/repository/redis.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9" 11 | 12 | "github.com/nitro/lazyraster/v2/internal/domain" 13 | ) 14 | 15 | type RedisClient struct { 16 | baseClient *redis.Client 17 | } 18 | 19 | func (rc RedisClient) FetchAnnotation(ctx context.Context, key string) ([]any, error) { 20 | result, err := rc.baseClient.Get(ctx, key).Result() 21 | if err == redis.Nil { 22 | return nil, nil 23 | } else if err != nil { 24 | return nil, fmt.Errorf("failed to get the key '%s': %w", key, err) 25 | } 26 | 27 | annotations, err := rc.parseAnnotations(result) 28 | if err != nil { 29 | return nil, fmt.Errorf("failed to parse the annotations: %w", err) 30 | } 31 | 32 | return annotations, nil 33 | } 34 | 35 | func NewRedisClient(addr, username, password string) (RedisClient, error) { 36 | rdb := redis.NewClient(&redis.Options{ 37 | Addr: addr, 38 | Username: username, 39 | Password: password, 40 | TLSConfig: &tls.Config{ 41 | MinVersion: tls.VersionTLS12, 42 | }, 43 | }) 44 | ctx, ctxcancel := context.WithTimeout(context.Background(), time.Second) 45 | defer ctxcancel() 46 | if _, err := rdb.Ping(ctx).Result(); err != nil { 47 | return RedisClient{}, fmt.Errorf("failed to connect to redis: %w", err) 48 | } 49 | return RedisClient{ 50 | baseClient: rdb, 51 | }, nil 52 | } 53 | 54 | func (RedisClient) parseAnnotations(input string) ([]any, error) { 55 | var rawEntries []json.RawMessage 56 | if err := json.Unmarshal([]byte(input), &rawEntries); err != nil { 57 | return nil, err 58 | } 59 | 60 | result := make([]any, 0, len(rawEntries)) 61 | for _, rawEntry := range rawEntries { 62 | e := struct { 63 | Type string `json:"type"` 64 | }{} 65 | if err := json.Unmarshal(rawEntry, &e); err != nil { 66 | return nil, fmt.Errorf("failed to unmarshal message: %w", err) 67 | } 68 | switch e.Type { 69 | case "checkbox": 70 | value := domain.AnnotationCheckbox{} 71 | if err := json.Unmarshal(rawEntry, &value); err != nil { 72 | return nil, fmt.Errorf("failed to unmarshal message: %w", err) 73 | } 74 | result = append(result, value) 75 | case "image": 76 | value := domain.AnnotationImage{} 77 | if err := json.Unmarshal(rawEntry, &value); err != nil { 78 | return nil, fmt.Errorf("failed to unmarshal message: %w", err) 79 | } 80 | result = append(result, value) 81 | case "text": 82 | value := domain.AnnotationText{} 83 | if err := json.Unmarshal(rawEntry, &value); err != nil { 84 | return nil, fmt.Errorf("failed to unmarshal message: %w", err) 85 | } 86 | result = append(result, value) 87 | default: 88 | return nil, fmt.Errorf("unknow annotation type '%s'", e.Type) 89 | } 90 | } 91 | 92 | return result, nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/transport/server.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/go-chi/chi/v5" 11 | chiMiddleware "github.com/go-chi/chi/v5/middleware" 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | // Server is responsible for the transport layer of the API. 16 | type Server struct { 17 | Logger zerolog.Logger 18 | AsyncErrorHandler func(error) 19 | TraceExtractor traceExtractor 20 | DocumentService handlerDocumentService 21 | 22 | writer writer 23 | server http.Server 24 | router chi.Mux 25 | } 26 | 27 | // Init the server internal state. 28 | func (s *Server) Init() error { 29 | if s.AsyncErrorHandler == nil { 30 | return errors.New("internal/transport.Server.AsyncErrorHandler can't be nil") 31 | } 32 | if s.TraceExtractor == nil { 33 | return errors.New("internal/transport.Server.TraceExtractor can't be nil") 34 | } 35 | if s.DocumentService == nil { 36 | return errors.New("internal/transport.Server.DocumentService can't be nil") 37 | } 38 | return nil 39 | } 40 | 41 | // Start the server. 42 | func (s *Server) Start() { 43 | s.router = *chi.NewRouter() 44 | s.writer.logger = s.Logger 45 | s.writer.traceExtractor = s.TraceExtractor 46 | s.initMiddleware() 47 | s.initHandler() 48 | 49 | // The HTTP server uses a static configuration. In the case that we need to change this setting in the future, we 50 | // could consider moving it to a configuration file. 51 | s.server = http.Server{ 52 | ReadTimeout: 10 * time.Second, 53 | ReadHeaderTimeout: 20 * time.Second, 54 | WriteTimeout: 10 * time.Second, 55 | IdleTimeout: 30 * time.Second, 56 | MaxHeaderBytes: maxBodySize, 57 | Addr: ":8080", 58 | Handler: &s.router, 59 | } 60 | 61 | go func() { 62 | if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 63 | s.AsyncErrorHandler(fmt.Errorf("fail to start the http server: %w", err)) 64 | } 65 | }() 66 | } 67 | 68 | // Stop the server. 69 | func (s *Server) Stop(ctx context.Context) error { 70 | if err := s.server.Shutdown(ctx); err != nil { 71 | return fmt.Errorf("fail to close the http server: %w", err) 72 | } 73 | return nil 74 | } 75 | 76 | func (s *Server) initMiddleware() { 77 | m := middleware{log: s.Logger, writer: s.writer, traceExtractor: s.TraceExtractor} 78 | s.router.Use(m.recoverer) 79 | s.router.Use(m.timeout(5 * time.Second)) 80 | s.router.Use(m.datadogTracer) 81 | s.router.Use(chiMiddleware.NoCache) 82 | s.router.Use(chiMiddleware.RealIP) 83 | s.router.Use(chiMiddleware.RequestID) 84 | s.router.Use(chiMiddleware.StripSlashes) 85 | s.router.Use(chiMiddleware.NewCompressor(5).Handler) 86 | s.router.Use(m.logger) 87 | s.router.Use(m.limitReader(maxBodySize)) 88 | } 89 | 90 | func (s *Server) initHandler() { 91 | h := handler{ 92 | writer: s.writer, 93 | logger: s.Logger, 94 | traceExtractor: s.TraceExtractor, 95 | documentService: s.DocumentService, 96 | } 97 | 98 | s.router.MethodNotAllowed(h.methodNotAllowed) 99 | s.router.NotFound(h.notFound) 100 | s.router.Get("/health", h.health) 101 | s.router.Get("/documents/dropbox/*", h.document) 102 | s.router.Get("/documents/*", h.document) 103 | } 104 | -------------------------------------------------------------------------------- /internal/client.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/rs/zerolog" 11 | ddHTTP "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http" 12 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 13 | "gopkg.in/DataDog/dd-trace-go.v1/profiler" 14 | 15 | "github.com/nitro/lazyraster/v2/internal/repository" 16 | "github.com/nitro/lazyraster/v2/internal/service" 17 | "github.com/nitro/lazyraster/v2/internal/transport" 18 | ) 19 | 20 | // Client holds the logic to bootstrap the application. 21 | type Client struct { 22 | Logger zerolog.Logger 23 | AsyncErrorHandler func(error) 24 | URLSigningSecret string 25 | EnableDatadog bool 26 | StorageBucketRegion map[string]string 27 | RedisURL string 28 | RedisUsername string 29 | RedisPassword string 30 | redisDisabled bool 31 | 32 | server transport.Server 33 | serviceWorker service.Worker 34 | } 35 | 36 | // Init the client internal state. 37 | func (c *Client) Init() (err error) { 38 | httpClient := &http.Client{ 39 | Timeout: 10 * time.Second, 40 | Transport: &http.Transport{ 41 | Proxy: http.ProxyFromEnvironment, 42 | DialContext: (&net.Dialer{ 43 | Timeout: 30 * time.Second, 44 | KeepAlive: 30 * time.Second, 45 | }).DialContext, 46 | ForceAttemptHTTP2: true, 47 | MaxIdleConns: 100, 48 | IdleConnTimeout: 90 * time.Second, 49 | TLSHandshakeTimeout: 10 * time.Second, 50 | ExpectContinueTimeout: 1 * time.Second, 51 | }, 52 | } 53 | httpClient = ddHTTP.WrapClient(httpClient) 54 | 55 | if c.EnableDatadog { 56 | tracer.Start( 57 | tracer.WithHTTPClient(httpClient), 58 | tracer.WithLogger(datadogLogger{logger: c.Logger}), 59 | tracer.WithRuntimeMetrics(), 60 | ) 61 | defer func() { 62 | if err != nil { 63 | tracer.Stop() 64 | } 65 | }() 66 | 67 | err = profiler.Start( 68 | profiler.WithProfileTypes( 69 | profiler.CPUProfile, 70 | profiler.HeapProfile, 71 | ), 72 | ) 73 | if err != nil { 74 | return fmt.Errorf("failed to start datadog profiler: %w", err) 75 | } 76 | defer func() { 77 | if err != nil { 78 | profiler.Stop() 79 | } 80 | }() 81 | } 82 | 83 | if !c.redisDisabled { 84 | redisClient, err := repository.NewRedisClient(c.RedisURL, c.RedisUsername, c.RedisPassword) 85 | if err != nil { 86 | return fmt.Errorf("failed to create a redis client: %w", err) 87 | } 88 | c.serviceWorker.AnnotationStorage = redisClient 89 | } 90 | 91 | c.serviceWorker.URLSigningSecret = c.URLSigningSecret 92 | c.serviceWorker.HTTPClient = httpClient 93 | c.serviceWorker.Logger = c.Logger 94 | c.serviceWorker.TraceExtractor = traceLogger(c.EnableDatadog) 95 | c.serviceWorker.StorageBucketRegion = c.StorageBucketRegion 96 | if err := c.serviceWorker.Init(); err != nil { 97 | return fmt.Errorf("fail to initialize service worker: %w", err) 98 | } 99 | 100 | c.server.Logger = c.Logger 101 | c.server.AsyncErrorHandler = c.AsyncErrorHandler 102 | c.server.TraceExtractor = traceLogger(c.EnableDatadog) 103 | c.server.DocumentService = &c.serviceWorker 104 | if err := c.server.Init(); err != nil { 105 | return fmt.Errorf("fail to initialize the transport server: %w", err) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // Start the client. 112 | func (c *Client) Start() { 113 | c.server.Start() 114 | } 115 | 116 | // Stop the client. 117 | func (c *Client) Stop(ctx context.Context) error { 118 | defer tracer.Stop() 119 | if err := c.server.Stop(ctx); err != nil { 120 | return fmt.Errorf("fail to stop the server") 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/rs/zerolog" 14 | 15 | "github.com/nitro/lazyraster/v2/internal" 16 | ) 17 | 18 | func main() { 19 | var ( 20 | logger = configureLogger() 21 | urlSigningSecret = os.Getenv("URL_SIGNING_SECRET") 22 | enableDatadog = os.Getenv("ENABLE_DATADOG") 23 | rawStorageBucketRegion = os.Getenv("STORAGE_BUCKET_REGION") 24 | ) 25 | if urlSigningSecret == "" { 26 | logger.Fatal().Msg("Environment variable 'URL_SIGNING_SECRET' can't be empty") 27 | } 28 | if rawStorageBucketRegion == "" { 29 | logger.Fatal().Msg("Environment variable 'STORAGE_BUCKET_REGION' can't be empty") 30 | } 31 | 32 | storageBucketRegion, err := parseStorageBucketRegion(rawStorageBucketRegion) 33 | if err != nil { 34 | logger.Fatal().Msg("Fail to parse the environment variable 'STORAGE_BUCKET_REGION' payload") 35 | } 36 | 37 | waitHandlerAsyncError, waitHandler := wait(logger) 38 | client := internal.Client{ 39 | Logger: logger, 40 | AsyncErrorHandler: waitHandlerAsyncError, 41 | URLSigningSecret: urlSigningSecret, 42 | EnableDatadog: enableDatadog == "true", 43 | StorageBucketRegion: storageBucketRegion, 44 | RedisURL: os.Getenv("REDIS_URL"), 45 | RedisUsername: os.Getenv("REDIS_USERNAME"), 46 | RedisPassword: os.Getenv("REDIS_PASSWORD"), 47 | } 48 | if err := client.Init(); err != nil { 49 | logger.Fatal().Err(err).Msg("Fail to initialize the client") 50 | } 51 | client.Start() 52 | 53 | exitStatus := waitHandler() 54 | ctx, ctxCancel := context.WithTimeout(context.Background(), 10*time.Second) 55 | if err := client.Stop(ctx); err != nil { 56 | ctxCancel() 57 | logger.Fatal().Err(err).Msg("Fail to stop the client") 58 | } 59 | ctxCancel() 60 | os.Exit(exitStatus) 61 | } 62 | 63 | func wait(logger zerolog.Logger) (func(error), func() int) { 64 | signalChan := make(chan os.Signal, 2) 65 | var exitStatus int32 66 | asyncError := func(err error) { 67 | logger.Error().Err(err).Msg("Async error happened") 68 | signalChan <- os.Interrupt 69 | atomic.AddInt32(&exitStatus, 1) 70 | } 71 | handler := func() int { 72 | signal.Notify(signalChan, os.Interrupt) 73 | <-signalChan 74 | return (int)(exitStatus) 75 | } 76 | return asyncError, handler 77 | } 78 | 79 | func parseStorageBucketRegion(payload string) (map[string]string, error) { 80 | result := make(map[string]string) 81 | for _, segment := range strings.Split(payload, ";") { 82 | fragments := strings.Split(segment, ":") 83 | if len(fragments) != 2 { 84 | return nil, errors.New("invalid payload") 85 | } 86 | 87 | region := strings.TrimSpace(fragments[0]) 88 | buckets := strings.Split(fragments[1], ",") 89 | if len(buckets) == 0 { 90 | return nil, errors.New("expected at least one bucket") 91 | } 92 | for _, bucket := range buckets { 93 | result[strings.TrimSpace(bucket)] = region 94 | } 95 | } 96 | if len(result) == 0 { 97 | return nil, fmt.Errorf("fail to parse the storage bucket region") 98 | } 99 | return result, nil 100 | } 101 | 102 | func configureLogger() zerolog.Logger { 103 | var logLevel zerolog.Level 104 | switch os.Getenv("LOG_LEVEL") { 105 | case "debug": 106 | logLevel = zerolog.DebugLevel 107 | case "info": 108 | logLevel = zerolog.InfoLevel 109 | case "warn": 110 | logLevel = zerolog.WarnLevel 111 | case "error": 112 | logLevel = zerolog.ErrorLevel 113 | default: 114 | logLevel = zerolog.InfoLevel 115 | } 116 | 117 | return zerolog.New(os.Stdout).With().Timestamp().Caller().Logger().Level(logLevel) 118 | } 119 | -------------------------------------------------------------------------------- /internal/transport/middleware.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "runtime/debug" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/go-chi/chi/v5" 14 | chiMiddleware "github.com/go-chi/chi/v5/middleware" 15 | "github.com/rs/zerolog" 16 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" 17 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" 18 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 19 | ) 20 | 21 | type middleware struct { 22 | log zerolog.Logger 23 | writer writer 24 | traceExtractor traceExtractor 25 | } 26 | 27 | func (m middleware) recoverer(next http.Handler) http.Handler { 28 | fn := func(w http.ResponseWriter, r *http.Request) { 29 | defer func() { 30 | if rvr := recover(); rvr != nil && rvr != http.ErrAbortHandler { 31 | m.writer.error(r.Context(), w, "Internal server error", nil, http.StatusInternalServerError) 32 | } 33 | }() 34 | next.ServeHTTP(w, r) 35 | } 36 | return http.HandlerFunc(fn) 37 | } 38 | 39 | // limitReader don't prevent all cases of a way to big payload. The way this function works is by looking to the 40 | // content-length header. This header may not be available or even wrong, this function it's just a initial layer of 41 | // protection. 42 | func (m middleware) limitReader(limit int64) func(http.Handler) http.Handler { 43 | return func(next http.Handler) http.Handler { 44 | fn := func(w http.ResponseWriter, r *http.Request) { 45 | rawContentLength := r.Header.Get("Content-Length") 46 | if rawContentLength != "" { 47 | contentLength, err := strconv.ParseInt(rawContentLength, 10, 64) 48 | if err != nil { 49 | m.writer.error(r.Context(), w, "Fail to parse the header content-length", err, http.StatusBadRequest) 50 | return 51 | } 52 | if contentLength > limit { 53 | m.writer.error(r.Context(), w, "Request payload too large", nil, http.StatusRequestEntityTooLarge) 54 | return 55 | } 56 | } 57 | next.ServeHTTP(w, r) 58 | } 59 | return http.HandlerFunc(fn) 60 | } 61 | } 62 | 63 | func (m middleware) logger(next http.Handler) http.Handler { 64 | fn := func(w http.ResponseWriter, r *http.Request) { 65 | if r.RequestURI == "/health" { 66 | next.ServeHTTP(w, r) 67 | return 68 | } 69 | 70 | requestURI := r.RequestURI 71 | if token := r.URL.Query().Get("token"); token != "" { 72 | requestURI = strings.ReplaceAll(requestURI, token, "[REDACTED]") 73 | } 74 | if strings.HasPrefix(requestURI, "/documents/dropbox/") { 75 | requestURI = "/documents/dropbox/[REDACTED]" 76 | } 77 | 78 | log, err := m.traceExtractor(r.Context(), m.log) 79 | if err != nil { 80 | m.writer.error(r.Context(), w, "Could not extract tracing id", nil, http.StatusInternalServerError) 81 | return 82 | } 83 | 84 | t1 := time.Now() 85 | reqID := chiMiddleware.GetReqID(r.Context()) 86 | entry := log.Debug(). 87 | Str("requestID", reqID). 88 | Str("method", r.Method). 89 | Str("endpoint", requestURI). 90 | Str("protocol", r.Proto) 91 | if r.RemoteAddr != "" { 92 | entry = entry.Str("ip", r.RemoteAddr) 93 | } 94 | entry.Msg("Request started") 95 | 96 | defer func() { 97 | if err := recover(); err != nil { 98 | log.Error(). 99 | Str("requestID", reqID). 100 | Dur("duration", time.Since(t1)). 101 | Int("status", 500). 102 | Str("stacktrace", string(debug.Stack())). 103 | Msg("Request finished with panic") 104 | panic(err) 105 | } 106 | }() 107 | 108 | ww := chiMiddleware.NewWrapResponseWriter(w, r.ProtoMajor) 109 | responseBody := bytes.NewBuffer([]byte{}) 110 | ww.Tee(responseBody) 111 | next.ServeHTTP(ww, r) 112 | 113 | status := ww.Status() 114 | entry = log.Debug(). 115 | Err(r.Context().Err()). 116 | Str("requestID", reqID). 117 | Dur("duration", time.Since(t1)). 118 | Int("contentLength", ww.BytesWritten()). 119 | Int("status", status) 120 | 121 | if status < 200 || status >= 300 { 122 | entry = entry.Str("body", responseBody.String()) 123 | } 124 | 125 | if status == http.StatusInternalServerError { 126 | entry.Str("stacktrace", string(debug.Stack())).Msg("Internal error during request") 127 | } else { 128 | entry.Msg("Request finished") 129 | } 130 | } 131 | 132 | return http.HandlerFunc(fn) 133 | } 134 | 135 | func (m middleware) datadogTracer(next http.Handler) http.Handler { 136 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 137 | path := r.URL.Path 138 | if strings.HasPrefix(path, "/documents/dropbox/") { 139 | path = "/documents/dropbox/[REDACTED]" 140 | } 141 | 142 | opts := []ddtrace.StartSpanOption{ 143 | tracer.SpanType(ext.SpanTypeWeb), 144 | tracer.Tag(ext.HTTPMethod, r.Method), 145 | tracer.Tag(ext.HTTPURL, path), 146 | tracer.Measured(), 147 | } 148 | if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil { 149 | opts = append(opts, tracer.ChildOf(spanctx)) 150 | } 151 | span, ctx := tracer.StartSpanFromContext(r.Context(), "http.request", opts...) 152 | defer span.Finish() 153 | 154 | ww := chiMiddleware.NewWrapResponseWriter(w, r.ProtoMajor) 155 | next.ServeHTTP(ww, r.WithContext(ctx)) 156 | 157 | resourceName := chi.RouteContext(r.Context()).RoutePattern() 158 | if resourceName == "" { 159 | resourceName = "unknown" 160 | } 161 | resourceName = r.Method + " " + resourceName 162 | 163 | // If the header 'datadog-resource-prefix' is present the value will be prefixed at the resource name 164 | // that is set at the span sent to Datadog. 165 | resourcePrefix := r.Header.Get("datadog-resource-prefix") 166 | if resourcePrefix != "" { 167 | resourceName = fmt.Sprintf("%s - %s", resourcePrefix, resourceName) 168 | } 169 | span.SetTag(ext.ResourceName, resourceName) 170 | 171 | status := ww.Status() 172 | if ww.Status() == 0 { 173 | status = http.StatusOK 174 | } 175 | span.SetTag(ext.HTTPCode, strconv.Itoa(status)) 176 | 177 | if status >= 500 { 178 | span.SetTag(ext.Error, true) 179 | span.SetTag(ext.ErrorMsg, fmt.Errorf("%d: %s", status, http.StatusText(status))) 180 | span.SetTag(ext.ErrorType, "internal/service/transport") 181 | span.SetTag(ext.ErrorStack, string(debug.Stack())) 182 | } 183 | }) 184 | } 185 | 186 | func (m middleware) timeout(duration time.Duration) func(http.Handler) http.Handler { 187 | return func(next http.Handler) http.Handler { 188 | fn := func(w http.ResponseWriter, r *http.Request) { 189 | ctx, ctxCancel := context.WithTimeout(r.Context(), duration) 190 | next.ServeHTTP(w, r.WithContext(ctx)) 191 | ctxCancel() 192 | } 193 | return http.HandlerFunc(fn) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nitro/lazyraster/v2 2 | 3 | go 1.25.3 4 | 5 | require ( 6 | github.com/Nitro/urlsign v0.0.0-20181015102600-5c9420004fa4 7 | github.com/aws/aws-sdk-go-v2 v1.39.5 8 | github.com/aws/aws-sdk-go-v2/config v1.31.16 9 | github.com/aws/aws-sdk-go-v2/service/s3 v1.89.1 10 | github.com/go-chi/chi/v5 v5.2.3 11 | github.com/google/uuid v1.6.0 12 | github.com/nitro/lazypdf/v2 v2.0.0-20251222085501-6157acf24854 13 | github.com/rs/zerolog v1.34.0 14 | github.com/sirupsen/logrus v1.9.3 // indirect 15 | github.com/smartystreets/goconvey v1.6.4 // indirect 16 | github.com/stretchr/testify v1.11.1 17 | github.com/tinylib/msgp v1.5.0 // indirect 18 | golang.org/x/sys v0.37.0 // indirect 19 | golang.org/x/time v0.14.0 // indirect 20 | gopkg.in/DataDog/dd-trace-go.v1 v1.74.8 21 | ) 22 | 23 | require ( 24 | github.com/redis/go-redis/v9 v9.16.0 25 | golang.org/x/sync v0.17.0 26 | ) 27 | 28 | require ( 29 | github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.71.2 // indirect 30 | github.com/DataDog/datadog-agent/pkg/obfuscate v0.71.2 // indirect 31 | github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/attributes v0.71.2 // indirect 32 | github.com/DataDog/datadog-agent/pkg/proto v0.71.2 // indirect 33 | github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.71.2 // indirect 34 | github.com/DataDog/datadog-agent/pkg/trace v0.71.2 // indirect 35 | github.com/DataDog/datadog-agent/pkg/util/log v0.71.2 // indirect 36 | github.com/DataDog/datadog-agent/pkg/util/scrubber v0.71.2 // indirect 37 | github.com/DataDog/datadog-agent/pkg/version v0.71.2 // indirect 38 | github.com/DataDog/datadog-go/v5 v5.8.1 // indirect 39 | github.com/DataDog/dd-trace-go/contrib/aws/aws-sdk-go-v2/v2 v2.3.1 // indirect 40 | github.com/DataDog/dd-trace-go/contrib/net/http/v2 v2.3.1 // indirect 41 | github.com/DataDog/dd-trace-go/v2 v2.3.1 // indirect 42 | github.com/DataDog/go-libddwaf/v4 v4.6.1 // indirect 43 | github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20250721125240-fdf1ef85b633 // indirect 44 | github.com/DataDog/go-sqllexer v0.1.9 // indirect 45 | github.com/DataDog/go-tuf v1.1.1-0.5.2 // indirect 46 | github.com/DataDog/gostackparse v0.7.0 // indirect 47 | github.com/DataDog/sketches-go v1.4.7 // indirect 48 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 49 | github.com/Microsoft/go-winio v0.6.2 // indirect 50 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 // indirect 51 | github.com/aws/aws-sdk-go-v2/credentials v1.18.20 // indirect 52 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 // indirect 53 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 // indirect 54 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 // indirect 55 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect 56 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.12 // indirect 57 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.3 // indirect 58 | github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.9 // indirect 59 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect 60 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.3 // indirect 61 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.12 // indirect 62 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 // indirect 63 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.12 // indirect 64 | github.com/aws/aws-sdk-go-v2/service/kinesis v1.41.1 // indirect 65 | github.com/aws/aws-sdk-go-v2/service/sfn v1.39.10 // indirect 66 | github.com/aws/aws-sdk-go-v2/service/sns v1.39.2 // indirect 67 | github.com/aws/aws-sdk-go-v2/service/sqs v1.42.12 // indirect 68 | github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 // indirect 69 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 // indirect 70 | github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 // indirect 71 | github.com/aws/smithy-go v1.23.1 // indirect 72 | github.com/cenkalti/backoff/v5 v5.0.3 // indirect 73 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 74 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect 75 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 76 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 77 | github.com/dustin/go-humanize v1.0.1 // indirect 78 | github.com/ebitengine/purego v0.9.0 // indirect 79 | github.com/go-logr/logr v1.4.3 // indirect 80 | github.com/go-logr/stdr v1.2.2 // indirect 81 | github.com/go-ole/go-ole v1.3.0 // indirect 82 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 83 | github.com/gogo/protobuf v1.3.2 // indirect 84 | github.com/golang/protobuf v1.5.4 // indirect 85 | github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect 86 | github.com/hashicorp/go-version v1.7.0 // indirect 87 | github.com/json-iterator/go v1.1.12 // indirect 88 | github.com/klauspost/compress v1.18.1 // indirect 89 | github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect 90 | github.com/mattn/go-colorable v0.1.14 // indirect 91 | github.com/mattn/go-isatty v0.0.20 // indirect 92 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 93 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 94 | github.com/outcaste-io/ristretto v0.2.3 // indirect 95 | github.com/philhofer/fwd v1.2.0 // indirect 96 | github.com/pkg/errors v0.9.1 // indirect 97 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 98 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 99 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 100 | github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect 101 | github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 // indirect 102 | github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect 103 | github.com/shirou/gopsutil/v4 v4.25.10 // indirect 104 | github.com/spaolacci/murmur3 v1.1.0 // indirect 105 | github.com/stretchr/objx v0.5.3 // indirect 106 | github.com/theckman/httpforwarded v0.4.0 // indirect 107 | github.com/tklauser/go-sysconf v0.3.15 // indirect 108 | github.com/tklauser/numcpus v0.10.0 // indirect 109 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 110 | go.opentelemetry.io/auto/sdk v1.2.1 // indirect 111 | go.opentelemetry.io/collector/component v1.44.0 // indirect 112 | go.opentelemetry.io/collector/featuregate v1.44.0 // indirect 113 | go.opentelemetry.io/collector/internal/telemetry v0.138.0 // indirect 114 | go.opentelemetry.io/collector/pdata v1.44.0 // indirect 115 | go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 // indirect 116 | go.opentelemetry.io/otel v1.38.0 // indirect 117 | go.opentelemetry.io/otel/log v0.14.0 // indirect 118 | go.opentelemetry.io/otel/metric v1.38.0 // indirect 119 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 120 | go.uber.org/atomic v1.11.0 // indirect 121 | go.uber.org/multierr v1.11.0 // indirect 122 | go.uber.org/zap v1.27.0 // indirect 123 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect 124 | golang.org/x/image v0.32.0 // indirect 125 | golang.org/x/mod v0.29.0 // indirect 126 | golang.org/x/net v0.46.0 // indirect 127 | golang.org/x/text v0.30.0 // indirect 128 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect 129 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect 130 | google.golang.org/grpc v1.76.0 // indirect 131 | google.golang.org/protobuf v1.36.10 // indirect 132 | gopkg.in/ini.v1 v1.67.0 // indirect 133 | gopkg.in/yaml.v3 v3.0.1 // indirect 134 | ) 135 | -------------------------------------------------------------------------------- /internal/transport/handler.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "slices" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | chiMiddleware "github.com/go-chi/chi/v5/middleware" 16 | "github.com/rs/zerolog" 17 | 18 | "github.com/nitro/lazyraster/v2/internal/service" 19 | ) 20 | 21 | type handlerDocumentService interface { 22 | Process(context.Context, string, string, int, int, float32, int, io.Writer, string) error 23 | Metadata(context.Context, string, string) (string, int, error) 24 | } 25 | 26 | type handler struct { 27 | writer writer 28 | logger zerolog.Logger 29 | traceExtractor traceExtractor 30 | documentService handlerDocumentService 31 | } 32 | 33 | func (h handler) notFound(w http.ResponseWriter, r *http.Request) { 34 | h.writer.error(r.Context(), w, "Endpoint not found", nil, http.StatusNotFound) 35 | } 36 | 37 | func (h handler) methodNotAllowed(w http.ResponseWriter, r *http.Request) { 38 | h.writer.error(r.Context(), w, "Method not allowed", nil, http.StatusMethodNotAllowed) 39 | } 40 | 41 | func (h handler) health(w http.ResponseWriter, r *http.Request) { 42 | h.writer.response(r.Context(), w, map[string]interface{}{"status": "healthy"}, http.StatusOK, "application/json") 43 | } 44 | 45 | func (h handler) document(w http.ResponseWriter, r *http.Request) { 46 | reqID := chiMiddleware.GetReqID(r.Context()) 47 | logger, err := h.traceExtractor(r.Context(), h.logger) 48 | if err != nil { 49 | logger.Err(err).Str("requestID", reqID).Msg("Could not extract tracing id") 50 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusInternalServerError) 51 | return 52 | } 53 | 54 | rawPage := r.URL.Query().Get("page") 55 | if rawPage == "" { 56 | h.metadata(w, r) 57 | return 58 | } 59 | 60 | page, err := strconv.Atoi(rawPage) 61 | if err != nil { 62 | logger.Err(err).Str("requestID", reqID).Msg("Invalid 'page' parameter") 63 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusBadRequest) 64 | return 65 | } 66 | 67 | var width int 68 | rawWidth := r.URL.Query().Get("width") 69 | if rawWidth != "" { 70 | width, err = strconv.Atoi(rawWidth) 71 | if err != nil { 72 | logger.Err(err).Str("requestID", reqID).Msg("Invalid 'width' parameter") 73 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusBadRequest) 74 | return 75 | } 76 | } 77 | 78 | var dpi int 79 | rawDPI := r.URL.Query().Get("dpi") 80 | if rawDPI != "" { 81 | dpi, err = strconv.Atoi(rawDPI) 82 | if err != nil { 83 | logger.Err(err).Str("requestID", reqID).Msg("Invalid 'dpi' parameter") 84 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusBadRequest) 85 | return 86 | } 87 | } 88 | 89 | var scale float64 90 | rawScale := r.URL.Query().Get("scale") 91 | if rawScale != "" { 92 | scale, err = strconv.ParseFloat(rawScale, 32) 93 | if err != nil { 94 | logger.Err(err).Str("requestID", reqID).Msg("Invalid 'scale' parameter") 95 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusBadRequest) 96 | return 97 | } 98 | } 99 | 100 | if rawTokenTTL := r.URL.Query().Get("token-ttl"); rawTokenTTL != "" { 101 | parsedTokenTTL, err := strconv.ParseInt(rawTokenTTL, 10, 64) 102 | if err != nil { 103 | logger.Err(err).Str("requestID", reqID).Msg("Invalid 'token-ttl' parameter") 104 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusBadRequest) 105 | return 106 | } 107 | if time.Now().After(time.Unix(parsedTokenTTL, 0)) { 108 | logger.Debug().Str("requestID", reqID).Msg("Token has expired") 109 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusUnauthorized) 110 | return 111 | } 112 | } 113 | 114 | var contentType string 115 | format := r.URL.Query().Get("format") 116 | switch format { 117 | case "png": 118 | contentType = "image/png" 119 | case "html": 120 | contentType = "text/html" 121 | case "": 122 | contentType = "image/png" 123 | format = "png" 124 | default: 125 | logger.Err(err).Str("requestID", reqID).Msg("Invalid 'format' parameter") 126 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusBadRequest) 127 | return 128 | } 129 | path := strings.TrimPrefix(r.URL.Path, "/documents/") 130 | buf := bytes.NewBuffer([]byte{}) 131 | err = h.documentService.Process(r.Context(), h.urlToVerify(r), path, page, width, float32(scale), dpi, buf, format) 132 | if ctxErr := r.Context().Err(); ctxErr != nil { 133 | logger.Err(ctxErr).Str("requestID", reqID).Msg("Context error") 134 | if ctxErr == context.Canceled { 135 | return 136 | } 137 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusRequestTimeout) 138 | return 139 | } 140 | if err != nil { 141 | status := http.StatusInternalServerError 142 | if errors.Is(err, service.ErrClient) { 143 | status = http.StatusBadRequest 144 | } else if errors.Is(err, service.ErrNotFound) { 145 | status = http.StatusNotFound 146 | } 147 | logger.Err(err).Str("requestID", reqID).Msg("Error") 148 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, status) 149 | return 150 | } 151 | 152 | w.Header().Set("content-length", strconv.Itoa(len(buf.Bytes()))) 153 | w.Header().Set("content-type", contentType) 154 | w.WriteHeader(http.StatusOK) 155 | if _, err := w.Write(buf.Bytes()); err != nil { 156 | logger.Err(err).Str("requestID", reqID).Msg("Fail to write the response back to the client") 157 | } 158 | } 159 | 160 | func (h handler) metadata(w http.ResponseWriter, r *http.Request) { 161 | reqID := chiMiddleware.GetReqID(r.Context()) 162 | logger, err := h.traceExtractor(r.Context(), h.logger) 163 | if err != nil { 164 | logger.Err(err).Str("requestID", reqID).Msg("Could not extract tracing id") 165 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusInternalServerError) 166 | return 167 | } 168 | 169 | path := strings.TrimPrefix(r.URL.Path, "/documents/") 170 | fileName, pageCount, err := h.documentService.Metadata(r.Context(), h.urlToVerify(r), path) 171 | if ctxErr := r.Context().Err(); ctxErr != nil { 172 | logger.Err(ctxErr).Str("requestID", reqID).Msg("Context error") 173 | if ctxErr == context.Canceled { 174 | return 175 | } 176 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, http.StatusRequestTimeout) 177 | return 178 | } 179 | if err != nil { 180 | status := http.StatusInternalServerError 181 | if errors.Is(err, service.ErrClient) { 182 | status = http.StatusBadRequest 183 | } else if errors.Is(err, service.ErrNotFound) { 184 | status = http.StatusNotFound 185 | } 186 | logger.Err(err).Str("requestID", reqID).Msg("Error") 187 | h.writer.error(r.Context(), w, fmt.Sprintf("Request ID '%s'", reqID), nil, status) 188 | return 189 | } 190 | result := map[string]interface{}{ 191 | "Filename": fileName, 192 | "PageCount": pageCount, 193 | } 194 | h.writer.response(r.Context(), w, result, http.StatusOK, "application/json") 195 | } 196 | 197 | // Remove all the parameters, but the token and page, from the path. Other parameters can then be passed to the service 198 | // without making the url signature invalid. 199 | func (handler) urlToVerify(r *http.Request) string { 200 | q := r.URL.Query() 201 | for key := range q { 202 | if slices.Contains([]string{"page", "token", "token-ttl"}, key) { 203 | continue 204 | } 205 | q.Del(key) 206 | } 207 | r.URL.RawQuery = q.Encode() 208 | return r.URL.String() 209 | } 210 | -------------------------------------------------------------------------------- /internal/service/worker_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "testing" 12 | "time" 13 | 14 | "github.com/Nitro/urlsign" 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | "github.com/aws/aws-sdk-go-v2/service/s3" 17 | "github.com/nitro/lazyraster/v2/internal/domain" 18 | "github.com/rs/zerolog" 19 | "github.com/stretchr/testify/mock" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | // nolint: goconst 24 | func TestWorkerProcess(t *testing.T) { 25 | t.Parallel() 26 | 27 | urlSecret := "secret" 28 | expiredToken := urlsign.GenerateToken("secret", 8*time.Hour, time.Now().Add(-24*time.Hour), "documents") 29 | validToken := urlsign.GenerateToken(urlSecret, 8*time.Hour, time.Now().Add(time.Hour), "documents") 30 | 31 | tests := []struct { 32 | message string 33 | url string 34 | path string 35 | page int 36 | width int 37 | scale float32 38 | s3Client func(*testing.T) *mockS3 39 | expectedError string 40 | annotationStorage func(t *testing.T) *mockWorkerAnnotationStorage 41 | }{ 42 | { 43 | message: "have an invalid page #1", 44 | page: -1, 45 | expectedError: "invalid page", 46 | }, 47 | { 48 | message: "have an invalid page #2", 49 | page: 0, 50 | expectedError: "invalid page", 51 | }, 52 | { 53 | message: "have an invalid width #1", 54 | page: 1, 55 | width: -1, 56 | expectedError: "invalid width", 57 | }, 58 | { 59 | message: "have an invalid width #2", 60 | page: 1, 61 | width: 4097, 62 | expectedError: "invalid width, can't be bigger than 4096", 63 | }, 64 | { 65 | message: "have an invalid scale #1", 66 | page: 1, 67 | scale: -1, 68 | expectedError: "invalid scale", 69 | }, 70 | { 71 | message: "have an invalid scale #2", 72 | page: 1, 73 | scale: 4, 74 | expectedError: "invalid scale, can't be bigger than 3", 75 | }, 76 | { 77 | message: "have an invalid token #1", 78 | page: 1, 79 | expectedError: "invalid token", 80 | }, 81 | { 82 | message: "have an invalid token #2", 83 | page: 1, 84 | url: fmt.Sprintf("/another-endpoint?token=%s", validToken), 85 | expectedError: "invalid token", 86 | }, 87 | { 88 | message: "have an invalid token #3", 89 | page: 1, 90 | url: fmt.Sprintf("documents?token=%s", expiredToken), 91 | expectedError: "invalid token", 92 | }, 93 | { 94 | message: "have an error fetching the file #1", 95 | page: 1, 96 | url: fmt.Sprintf("documents?token=%s", validToken), 97 | expectedError: "fail to fetch the file: invalid path", 98 | }, 99 | { 100 | message: "have an error fetching the file #2", 101 | page: 1, 102 | url: fmt.Sprintf("documents?token=%s", validToken), 103 | path: "documents", 104 | expectedError: "fail to fetch the file: invalid path", 105 | }, 106 | { 107 | message: "have an error fetching the file #3", 108 | page: 1, 109 | url: fmt.Sprintf("documents?token=%s", validToken), 110 | path: "random-bucket/file.pdf", 111 | expectedError: "fail to fetch the file: fail to get the s3 bucket client: can't find the bucket 'random-bucket' region", // nolint: lll 112 | }, 113 | { 114 | message: "have an error fetching the file #4", 115 | page: 1, 116 | url: fmt.Sprintf("documents?token=%s", validToken), 117 | path: "bucket-1/file.pdf", 118 | s3Client: func(*testing.T) *mockS3 { 119 | var client mockS3 120 | input := s3.GetObjectInput{ 121 | Bucket: aws.String("bucket-1"), 122 | Key: aws.String("file.pdf"), 123 | } 124 | client.On("GetObject", mock.Anything, &input).Return((*s3.GetObjectOutput)(nil), errors.New("s3 error")) 125 | return &client 126 | }, 127 | expectedError: "fail to fetch the file: fail to get object: s3 error", 128 | }, 129 | { 130 | message: "have an error processing the file", 131 | page: 1, 132 | url: fmt.Sprintf("documents?token=%s", validToken), 133 | path: "bucket-1/file.pdf", 134 | s3Client: func(*testing.T) *mockS3 { 135 | var client mockS3 136 | input := s3.GetObjectInput{ 137 | Bucket: aws.String("bucket-1"), 138 | Key: aws.String("file.pdf"), 139 | } 140 | output := s3.GetObjectOutput{Body: io.NopCloser(bytes.NewBuffer([]byte{}))} 141 | client.On("GetObject", mock.Anything, &input).Return(&output, nil) 142 | return &client 143 | }, 144 | expectedError: "empty payload", 145 | }, 146 | { 147 | message: "process and return a page", 148 | page: 1, 149 | url: fmt.Sprintf("documents?token=%s", validToken), 150 | path: "bucket-1/file.pdf", 151 | s3Client: func(t *testing.T) *mockS3 { 152 | var client mockS3 153 | input := s3.GetObjectInput{ 154 | Bucket: aws.String("bucket-1"), 155 | Key: aws.String("file.pdf"), 156 | } 157 | payload, err := os.ReadFile("testdata/sample.pdf") 158 | require.NoError(t, err) 159 | output := s3.GetObjectOutput{Body: io.NopCloser(bytes.NewBuffer(payload))} 160 | client.On("GetObject", mock.Anything, &input).Return(&output, nil) 161 | return &client 162 | }, 163 | }, 164 | { 165 | message: "process and return a page with annotations", 166 | page: 1, 167 | url: fmt.Sprintf("documents?token=%s", validToken), 168 | path: "bucket-1/file.pdf", 169 | annotationStorage: func(t *testing.T) *mockWorkerAnnotationStorage { 170 | var client mockWorkerAnnotationStorage 171 | client.On("FetchAnnotation", mock.Anything, mock.Anything).Return([]any{ 172 | domain.AnnotationCheckbox{ 173 | Value: true, 174 | Page: 0, 175 | Location: domain.AnnotationLocation{ 176 | X: 0.5, 177 | Y: 0.5, 178 | }, 179 | Size: domain.AnnotationSize{ 180 | Height: 0.1, 181 | Width: 0.1, 182 | }, 183 | }, 184 | domain.AnnotationText{ 185 | Value: "hey annotation from lazyraster!", 186 | Page: 0, 187 | Location: domain.AnnotationLocation{ 188 | X: 0.4, 189 | Y: 0.4, 190 | }, 191 | Font: domain.AnnotationTextFont{ 192 | Family: "Courier", 193 | Size: 12, 194 | }, 195 | Size: domain.AnnotationSize{ 196 | Height: 0.1, 197 | Width: 0.1, 198 | }, 199 | }, 200 | }, nil) 201 | return &client 202 | }, 203 | s3Client: func(t *testing.T) *mockS3 { 204 | var client mockS3 205 | input := s3.GetObjectInput{ 206 | Bucket: aws.String("bucket-1"), 207 | Key: aws.String("file.pdf"), 208 | } 209 | payload, err := os.ReadFile("testdata/sample.pdf") 210 | require.NoError(t, err) 211 | output := s3.GetObjectOutput{Body: io.NopCloser(bytes.NewBuffer(payload))} 212 | client.On("GetObject", mock.Anything, &input).Return(&output, nil) 213 | return &client 214 | }, 215 | }, 216 | } 217 | for _, format := range []string{"png", "html"} { 218 | for _, tt := range tests { 219 | t.Run(fmt.Sprintf("Should %s (%s)", tt.message, format), func(t *testing.T) { 220 | t.Parallel() 221 | 222 | var ( 223 | s3Client *mockS3 224 | getS3Client func(string) (workerS3API, error) 225 | ) 226 | if tt.s3Client != nil { 227 | s3Client = tt.s3Client(t) 228 | defer s3Client.AssertExpectations(t) 229 | getS3Client = func(string) (workerS3API, error) { 230 | return s3Client, nil 231 | } 232 | } 233 | 234 | w := Worker{ 235 | HTTPClient: http.DefaultClient, 236 | URLSigningSecret: urlSecret, 237 | TraceExtractor: traceExtractor, 238 | StorageBucketRegion: map[string]string{"bucket-1": "eu-central-1"}, 239 | getS3Client: getS3Client, 240 | } 241 | if tt.annotationStorage == nil { 242 | var client mockWorkerAnnotationStorage 243 | client.On("FetchAnnotation", mock.Anything, mock.Anything).Return([]any{}, nil) 244 | w.AnnotationStorage = &client 245 | } else { 246 | w.AnnotationStorage = tt.annotationStorage(t) 247 | } 248 | require.NoError(t, w.Init()) 249 | 250 | err := w.Process( 251 | context.Background(), tt.url, tt.path, tt.page, tt.width, tt.scale, 72, bytes.NewBuffer([]byte{}), format, 252 | ) 253 | require.Equal(t, tt.expectedError == "", err == nil) 254 | if tt.expectedError != "" { 255 | require.Equal(t, tt.expectedError, err.Error()) 256 | } 257 | }) 258 | } 259 | } 260 | } 261 | 262 | type mockS3 struct { 263 | mock.Mock 264 | } 265 | 266 | func (m *mockS3) GetObject( 267 | ctx context.Context, 268 | input *s3.GetObjectInput, 269 | _ ...func(*s3.Options), 270 | ) (*s3.GetObjectOutput, error) { 271 | args := m.Called(ctx, input) 272 | var output *s3.GetObjectOutput 273 | if value := args.Get(0); value != nil { 274 | output = value.(*s3.GetObjectOutput) 275 | } 276 | return output, args.Error(1) 277 | } 278 | 279 | func traceExtractor(context.Context, zerolog.Logger) (zerolog.Logger, error) { 280 | return zerolog.Nop(), nil 281 | } 282 | 283 | type mockWorkerAnnotationStorage struct { 284 | mock.Mock 285 | } 286 | 287 | func (m *mockWorkerAnnotationStorage) FetchAnnotation(ctx context.Context, token string) ([]any, error) { 288 | args := m.Called(ctx, token) 289 | return args.Get(0).([]any), args.Error(1) 290 | } 291 | -------------------------------------------------------------------------------- /internal/service/worker.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/Nitro/urlsign" 18 | "github.com/aws/aws-sdk-go-v2/aws" 19 | "github.com/aws/aws-sdk-go-v2/config" 20 | "github.com/aws/aws-sdk-go-v2/service/s3" 21 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 22 | "github.com/google/uuid" 23 | "github.com/nitro/lazypdf/v2" 24 | "github.com/rs/zerolog" 25 | "golang.org/x/sync/errgroup" 26 | awsv2trace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go-v2/aws" 27 | "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" 28 | ddTracer "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" 29 | 30 | "github.com/nitro/lazyraster/v2/internal/domain" 31 | ) 32 | 33 | type workerS3API interface { 34 | GetObject(context.Context, *s3.GetObjectInput, ...func(*s3.Options)) (*s3.GetObjectOutput, error) 35 | } 36 | 37 | type workerAnnotationStorage interface { 38 | FetchAnnotation(context.Context, string) ([]any, error) 39 | } 40 | 41 | // Worker used to fetch and process PDF files. 42 | type Worker struct { 43 | HTTPClient *http.Client 44 | URLSigningSecret string 45 | Logger zerolog.Logger 46 | TraceExtractor func(context.Context, zerolog.Logger) (zerolog.Logger, error) 47 | StorageBucketRegion map[string]string 48 | AnnotationStorage workerAnnotationStorage 49 | 50 | getS3Client func(string) (workerS3API, error) 51 | s3Clients map[string]workerS3API 52 | mutex sync.Mutex 53 | } 54 | 55 | // Init worker internal state. 56 | func (w *Worker) Init() error { 57 | if w.HTTPClient == nil { 58 | return errors.New("internal/service/Worker.HTTPClient can't be nil") 59 | } 60 | if w.URLSigningSecret == "" { 61 | return errors.New("internal/service/Worker.URLSigningSecret can't be empty") 62 | } 63 | if w.TraceExtractor == nil { 64 | return errors.New("internal/service/Worker.TraceExtractor can't be nil") 65 | } 66 | if len(w.StorageBucketRegion) == 0 { 67 | return errors.New("internal/service/Worker.StorageBucketRegion can't be empty") 68 | } 69 | if w.getS3Client == nil { 70 | w.getS3Client = w.getBucketS3Client 71 | } 72 | w.s3Clients = make(map[string]workerS3API) 73 | return nil 74 | } 75 | 76 | func (w *Worker) Process( 77 | ctx context.Context, url, path string, page int, width int, scale float32, dpi int, output io.Writer, format string, 78 | ) (err error) { 79 | span, ctx := w.startSpan(ctx, "Worker.Process") 80 | defer func() { span.Finish(ddTracer.WithError(err)) }() 81 | 82 | // This change is required because of historical reasons. The first page for the frontend is 1 and not zero. 83 | page-- 84 | 85 | if page < 0 { 86 | return newClientError(errors.New("invalid page")) 87 | } 88 | 89 | if width < 0 { 90 | return newClientError(errors.New("invalid width")) 91 | } else if width > 4096 { 92 | return newClientError(errors.New("invalid width, can't be bigger than 4096")) 93 | } 94 | 95 | if scale < 0 { 96 | return newClientError(errors.New("invalid scale")) 97 | } else if scale > 3 { 98 | return newClientError(errors.New("invalid scale, can't be bigger than 3")) 99 | } 100 | 101 | if dpi > 600 { 102 | return newClientError(errors.New("invalid dpi, can't be bigger than 600")) 103 | } 104 | 105 | if !urlsign.IsValidSignature(w.URLSigningSecret, 8*time.Hour, time.Now(), url) { 106 | return newClientError(errors.New("invalid token")) 107 | } 108 | 109 | // Fetch the file in a goroutine to allow the annotations to be processed while the payload is being fetch. 110 | chanPayload := make(chan []byte) 111 | chanError := make(chan error) 112 | go func() { 113 | payload, err := w.fetchFile(ctx, path) 114 | if err != nil { 115 | chanError <- fmt.Errorf("fail to fetch the file: %w", err) 116 | return 117 | } 118 | 119 | if len(payload) == 0 { 120 | chanError <- fmt.Errorf("empty payload") 121 | return 122 | } 123 | 124 | chanPayload <- payload 125 | }() 126 | 127 | storage := bytes.NewBuffer([]byte{}) 128 | switch format { 129 | case "png": 130 | token, err := w.extractToken(url) 131 | if err != nil { 132 | return fmt.Errorf("failed to extract the token: %w", err) 133 | } 134 | 135 | annotations, annotationsCleanup, err := w.fetchAnnotations(ctx, token, page) 136 | if err != nil { 137 | return fmt.Errorf("failed to fetch the annotations: %w", err) 138 | } 139 | defer annotationsCleanup() 140 | 141 | var rawPayload []byte 142 | select { 143 | case err := <-chanError: 144 | return err 145 | case rawPayload = <-chanPayload: 146 | } 147 | 148 | if len(annotations) > 0 { 149 | //nolint:gosec,G115 150 | err := w.SaveToPNGWithAnnotations( 151 | ctx, uint16(page), uint16(width), scale, dpi, 152 | bytes.NewBuffer(rawPayload), storage, annotations, 153 | ) 154 | if err != nil { 155 | return fmt.Errorf("failed to process annotations and generate PNG: %w", err) 156 | } 157 | } else { 158 | //nolint:gosec,G115 159 | err = lazypdf.SaveToPNG(ctx, uint16(page), uint16(width), scale, dpi, bytes.NewBuffer(rawPayload), storage) 160 | if err != nil { 161 | return fmt.Errorf("fail to extract the PNG from the PDF: %w", err) 162 | } 163 | } 164 | case "html": 165 | var rawPayload []byte 166 | select { 167 | case err := <-chanError: 168 | return err 169 | case rawPayload = <-chanPayload: 170 | } 171 | //nolint:gosec,G115 172 | err = lazypdf.SaveToHTML(ctx, uint16(page), uint16(width), scale, dpi, bytes.NewBuffer(rawPayload), storage) 173 | if err != nil { 174 | return fmt.Errorf("fail to render the PDF page to HTML: %w", err) 175 | } 176 | default: 177 | return fmt.Errorf("unknown format '%s'", format) 178 | } 179 | result := io.NopCloser(storage) 180 | defer result.Close() 181 | 182 | if _, err := io.Copy(output, result); err != nil { 183 | return fmt.Errorf("fail write the result to the output: %w", err) 184 | } 185 | return nil 186 | } 187 | 188 | // Metadata is used to fetch the document metadata. 189 | func (w *Worker) Metadata(ctx context.Context, url, path string) (_ string, _ int, err error) { 190 | span, ctx := w.startSpan(ctx, "Worker.Metadata") 191 | defer func() { span.Finish(ddTracer.WithError(err)) }() 192 | 193 | if !urlsign.IsValidSignature(w.URLSigningSecret, 8*time.Hour, time.Now(), url) { 194 | return "", 0, newClientError(errors.New("invalid token")) 195 | } 196 | 197 | payload, err := w.fetchFile(ctx, path) 198 | if err != nil { 199 | return "", 0, fmt.Errorf("fail to fetch the file: %w", err) 200 | } 201 | 202 | pageCount, err := lazypdf.PageCount(ctx, bytes.NewReader(payload)) 203 | if err != nil { 204 | return "", 0, fmt.Errorf("fail to count the file pages: %w", err) 205 | } 206 | 207 | return w.generateFilename(), pageCount, nil 208 | } 209 | 210 | func (w *Worker) fetchFile(ctx context.Context, path string) (_ []byte, err error) { 211 | span, ctx := ddTracer.StartSpanFromContext(ctx, "Worker.fetchFile") 212 | defer func() { span.Finish(ddTracer.WithError(err)) }() 213 | 214 | if strings.HasPrefix(path, "dropbox/") { 215 | return w.fetchFileFromDropbox(ctx, path) 216 | } 217 | 218 | var bucket, filePath string 219 | switch { 220 | case strings.HasPrefix(path, "s3://"): 221 | path = strings.TrimPrefix(path, "s3://") 222 | parts := strings.SplitN(path, "/", 2) 223 | if len(parts) != 2 { 224 | return nil, fmt.Errorf("invalid S3 path '%s'", path) 225 | } 226 | bucket = parts[0] 227 | filePath = parts[1] 228 | case strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://"): 229 | return w.fetchFileFromInternet(ctx, path) 230 | default: 231 | fragments := strings.Split(path, "/") 232 | if len(fragments) < 2 { 233 | return nil, newClientError(errors.New("invalid path")) 234 | } 235 | bucket = fragments[0] 236 | filePath = strings.Join(fragments[1:], "/") 237 | } 238 | 239 | s3Client, err := w.getS3Client(bucket) 240 | if err != nil { 241 | return nil, fmt.Errorf("fail to get the s3 bucket client: %w", err) 242 | } 243 | 244 | output, err := s3Client.GetObject(ctx, &s3.GetObjectInput{ 245 | Bucket: aws.String(bucket), 246 | Key: aws.String(filePath), 247 | }) 248 | if err != nil { 249 | var notFound *types.NoSuchKey 250 | if errors.As(err, ¬Found) { 251 | return nil, newNotFoundError(err) 252 | } 253 | return nil, fmt.Errorf("fail to get object: %w", err) 254 | } 255 | defer output.Body.Close() 256 | 257 | payload, err := io.ReadAll(output.Body) 258 | if err != nil { 259 | return nil, fmt.Errorf("fail to read the reader: %w", err) 260 | } 261 | span.SetTag("fileSize", len(payload)) 262 | 263 | return payload, nil 264 | } 265 | 266 | func (w *Worker) fetchFileFromDropbox(ctx context.Context, path string) (_ []byte, err error) { 267 | span, ctx := ddTracer.StartSpanFromContext(ctx, "Worker.fetchFileFromDropbox") 268 | defer func() { span.Finish(ddTracer.WithError(err)) }() 269 | 270 | fileURL, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(path, "dropbox/")) 271 | if err != nil { 272 | return nil, newClientError(fmt.Errorf("fail to decode base64 path: %w", err)) 273 | } 274 | 275 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(fileURL), nil) 276 | if err != nil { 277 | return nil, fmt.Errorf("fail to create the HTTP request: %w", err) 278 | } 279 | 280 | resp, err := w.HTTPClient.Do(req) 281 | if err != nil { 282 | return nil, fmt.Errorf("fail to download file: %w", err) 283 | } 284 | defer resp.Body.Close() 285 | 286 | if resp.StatusCode == http.StatusNotFound { 287 | return nil, newNotFoundError(errors.New("dropbox returned 404")) 288 | } else if resp.StatusCode < 200 || resp.StatusCode >= 300 { 289 | return nil, fmt.Errorf("invalid status code '%d'", resp.StatusCode) 290 | } 291 | 292 | payload, err := io.ReadAll(resp.Body) 293 | if err != nil { 294 | return nil, fmt.Errorf("fail to read the body response: %w", err) 295 | } 296 | 297 | return payload, nil 298 | } 299 | 300 | func (w *Worker) fetchFileFromInternet(ctx context.Context, uri string) (_ []byte, err error) { 301 | span, ctx := ddTracer.StartSpanFromContext(ctx, "Worker.fetchFileFromInternet") 302 | defer func() { span.Finish(ddTracer.WithError(err)) }() 303 | 304 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) 305 | if err != nil { 306 | return nil, fmt.Errorf("failed to create a HTTP request: %w", err) 307 | } 308 | 309 | resp, err := w.HTTPClient.Do(req) 310 | if err != nil { 311 | return nil, fmt.Errorf("fail to download file: %w", err) 312 | } 313 | defer resp.Body.Close() 314 | 315 | if resp.StatusCode == http.StatusNotFound { 316 | return nil, newNotFoundError(errors.New("server returned 404")) 317 | } else if resp.StatusCode < 200 || resp.StatusCode >= 300 { 318 | return nil, fmt.Errorf("invalid status code '%d'", resp.StatusCode) 319 | } 320 | 321 | payload, err := io.ReadAll(resp.Body) 322 | if err != nil { 323 | return nil, fmt.Errorf("fail to read the body response: %w", err) 324 | } 325 | 326 | return payload, nil 327 | } 328 | 329 | func (*Worker) generateFilename() string { 330 | id := uuid.New() 331 | return id.String() + "/document.pdf" 332 | } 333 | 334 | func (*Worker) startSpan(ctx context.Context, operation string) (ddtrace.Span, context.Context) { 335 | return ddTracer.StartSpanFromContext(ctx, "internal/service/"+operation) 336 | } 337 | 338 | func (w *Worker) getBucketS3Client(bucket string) (workerS3API, error) { 339 | region, ok := w.StorageBucketRegion[bucket] 340 | if !ok { 341 | return nil, fmt.Errorf("can't find the bucket '%s' region", bucket) 342 | } 343 | 344 | w.mutex.Lock() 345 | defer w.mutex.Unlock() 346 | 347 | client, ok := w.s3Clients[region] 348 | if ok { 349 | return client, nil 350 | } 351 | 352 | cfg, err := config.LoadDefaultConfig( 353 | context.Background(), 354 | config.WithRegion(region), 355 | config.WithHTTPClient(w.HTTPClient), 356 | ) 357 | if err != nil { 358 | return nil, fmt.Errorf("fail to load configuration for region '%s': %w", region, err) 359 | } 360 | awsv2trace.AppendMiddleware(&cfg) 361 | 362 | client = s3.NewFromConfig(cfg, func(o *s3.Options) { 363 | o.HTTPClient = w.HTTPClient 364 | }) 365 | w.s3Clients[region] = client 366 | return client, nil 367 | } 368 | 369 | func (w *Worker) extractToken(endpoint string) (string, error) { 370 | u, err := url.Parse(endpoint) 371 | if err != nil { 372 | return "", fmt.Errorf("failed to parse the endpoint: %w", err) 373 | } 374 | 375 | token := u.Query().Get("token") 376 | if token == "" { 377 | return "", errors.New("token not found") 378 | } 379 | 380 | return token, nil 381 | } 382 | 383 | // fetchAnnotations is used to get the annotations based on a token and preprocess them. The second return parameter is 384 | // a cleanup function that always need to be executed once the information is no longer needed. The cleanup function is 385 | // only available in case there is no errors. 386 | func (w *Worker) fetchAnnotations( 387 | ctx context.Context, token string, page int, 388 | ) (annotations []any, cleanup func(), err error) { 389 | span, ctx := ddTracer.StartSpanFromContext(ctx, "Worker.fetchAnnotations") 390 | defer func() { span.Finish(ddTracer.WithError(err)) }() 391 | 392 | annotations = make([]any, 0) 393 | originalAnnotations, err := w.AnnotationStorage.FetchAnnotation(ctx, token) 394 | if err != nil { 395 | return nil, nil, fmt.Errorf("failed to fetch the annotations: %w", err) 396 | } 397 | 398 | var temporaryAnnotationFilesMutex sync.Mutex 399 | temporaryAnnotationFiles := make([]string, 0) 400 | g, gctx := errgroup.WithContext(ctx) 401 | for _, annotation := range originalAnnotations { 402 | //nolint:gocritic 403 | switch v := annotation.(type) { 404 | case domain.AnnotationText: 405 | if v.Page != page+1 { 406 | continue 407 | } 408 | annotations = append(annotations, v) 409 | case domain.AnnotationCheckbox: 410 | if v.Page != page+1 { 411 | continue 412 | } 413 | annotations = append(annotations, v) 414 | case domain.AnnotationImage: 415 | if v.Page != page+1 { 416 | continue 417 | } 418 | imgIdx := len(annotations) 419 | annotations = append(annotations, v) 420 | g.Go(func() error { 421 | // Fetch the file from the internet. 422 | payload, err := w.fetchFile(gctx, v.ImageLocation) 423 | if err != nil { 424 | return fmt.Errorf("failed to fetch the image: %w", err) 425 | } 426 | 427 | // Once we have the image in memory it needs to be dumped into a file because this is how the C layer at lazypdf 428 | // can consume it. 429 | tmpFile, err := os.CreateTemp("", uuid.New().String()) 430 | if err != nil { 431 | return fmt.Errorf("failed to create a temporary file: %w", err) 432 | } 433 | defer tmpFile.Close() 434 | 435 | // Save the temporary file on an array to cleanup later. 436 | temporaryAnnotationFilesMutex.Lock() 437 | temporaryAnnotationFiles = append(temporaryAnnotationFiles, tmpFile.Name()) 438 | temporaryAnnotationFilesMutex.Unlock() 439 | 440 | // Get the payload from S3 and send it to the temporary file. 441 | if _, err := tmpFile.Write(payload); err != nil { 442 | return fmt.Errorf("failed to write to the temporary file: %w", err) 443 | } 444 | 445 | // Update the image location to the disk copy. 446 | v.ImageLocation = tmpFile.Name() 447 | annotations[imgIdx] = v 448 | return nil 449 | }) 450 | } 451 | } 452 | 453 | cleanup = func() { 454 | for _, entry := range temporaryAnnotationFiles { 455 | go func() { 456 | os.Remove(entry) 457 | }() 458 | } 459 | } 460 | 461 | if err := g.Wait(); err != nil { 462 | cleanup() 463 | return nil, nil, fmt.Errorf("failed to preprocess the annotations: %w", err) 464 | } 465 | 466 | return annotations, cleanup, nil 467 | } 468 | 469 | func (w *Worker) SaveToPNGWithAnnotations( 470 | ctx context.Context, page uint16, width uint16, scale float32, dpi int, 471 | payload io.Reader, storage io.Writer, annotations []any, 472 | ) (err error) { 473 | span, ctx := ddTracer.StartSpanFromContext(ctx, "Worker.SaveToPNGWithAnnotations") 474 | defer func() { span.Finish(ddTracer.WithError(err)) }() 475 | 476 | ph := lazypdf.NewPdfHandler(ctx, nil) 477 | 478 | doc, err := ph.OpenPDF(payload) 479 | if err != nil { 480 | return fmt.Errorf("failed to open the PDF: %w", err) 481 | } 482 | defer func() { 483 | if err := ph.ClosePDF(doc); err != nil { 484 | w.Logger.Err(err).Msg("Failed to close the PDF") 485 | } 486 | }() 487 | 488 | deadline, hasDeadline := ctx.Deadline() 489 | 490 | for _, annotation := range annotations { 491 | if hasDeadline && time.Now().After(deadline) { 492 | return context.DeadlineExceeded 493 | } 494 | 495 | var err error 496 | switch v := annotation.(type) { 497 | case domain.AnnotationCheckbox: 498 | params := lazypdf.CheckboxParams{ 499 | Value: v.Value, 500 | Page: v.Page - 1, 501 | Location: lazypdf.Location{ 502 | X: v.Location.X, 503 | Y: v.Location.Y, 504 | }, 505 | Size: lazypdf.Size{ 506 | Width: v.Size.Width, 507 | Height: v.Size.Height, 508 | }, 509 | } 510 | err = ph.AddCheckboxToPage(doc, params) 511 | case domain.AnnotationImage: 512 | params := lazypdf.ImageParams{ 513 | Page: v.Page - 1, 514 | Location: lazypdf.Location{ 515 | X: v.Location.X, 516 | Y: v.Location.Y, 517 | }, 518 | Size: lazypdf.Size{ 519 | Width: v.Size.Width, 520 | Height: v.Size.Height, 521 | }, 522 | ImagePath: v.ImageLocation, 523 | } 524 | err = ph.AddImageToPage(doc, params) 525 | case domain.AnnotationText: 526 | params := lazypdf.TextParams{ 527 | Value: v.Value, 528 | Page: v.Page - 1, 529 | Location: lazypdf.Location{ 530 | X: v.Location.X, 531 | Y: v.Location.Y, 532 | }, 533 | Font: struct { 534 | Family string 535 | Size float64 536 | }{ 537 | Family: v.Font.Family, 538 | Size: v.Font.Size, 539 | }, 540 | Size: lazypdf.Size{ 541 | Width: v.Size.Width, 542 | Height: v.Size.Height, 543 | }, 544 | } 545 | err = ph.AddTextBoxToPage(doc, params) 546 | default: 547 | return fmt.Errorf("annotation type '%T' not supported", annotation) 548 | } 549 | if err != nil { 550 | return fmt.Errorf("failed to add an annotation to the PDF: %w", err) 551 | } 552 | } 553 | 554 | err = ph.SaveToPNG(doc, page, width, scale, dpi, storage) 555 | if err != nil { 556 | return fmt.Errorf("failed to add an annotation to the PDF: %w", err) 557 | } 558 | return nil 559 | } 560 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.71.2 h1:C4huKojabL8u+MknxnBYUk2Dudkii5kRH5PhD6gp2MA= 2 | github.com/DataDog/datadog-agent/comp/core/tagger/origindetection v0.71.2/go.mod h1:y05SPqKEtrigKul+JBVM69ehv3lOgyKwrUIwLugoaSI= 3 | github.com/DataDog/datadog-agent/pkg/obfuscate v0.71.2 h1:SS3xTi1zlyhslE7kJsrMErKAA56rdAP1Ll4ZWCRkq/o= 4 | github.com/DataDog/datadog-agent/pkg/obfuscate v0.71.2/go.mod h1:B3T0If+WdWAwPMpawjm1lieJyqSI0v04dQZHq15WGxY= 5 | github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/attributes v0.71.2 h1:v9PTAUhEQhHh+AZIU1OgzpJdSB76pwPI9+erztcdsJU= 6 | github.com/DataDog/datadog-agent/pkg/opentelemetry-mapping-go/otlp/attributes v0.71.2/go.mod h1:XeZj0IgsiL3vgeEGTucf61JvJRh1LxWMUbZA/XJsPD0= 7 | github.com/DataDog/datadog-agent/pkg/proto v0.71.2 h1:WC69FCbHoYQEneHtp8cv4A71GpT/WNjv5EiYkuopvFo= 8 | github.com/DataDog/datadog-agent/pkg/proto v0.71.2/go.mod h1:KSn4jt3CykV6CT1C8Rknn/Nj3E+VYHK/UDWolg/+kzw= 9 | github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.71.2 h1:z3P/8Znwo/cT3EgxNRa+UJqPHT0JPDIaAbOxPTgef68= 10 | github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.71.2/go.mod h1:cAUt6KWsedHR2k4agAvEfiK8tGxFJDIrCvrWMIGwe/o= 11 | github.com/DataDog/datadog-agent/pkg/trace v0.71.2 h1:F3Zk3JxkSnQ0rs4kifZ1y94alDqo/SAT9rwQlT0Nx7c= 12 | github.com/DataDog/datadog-agent/pkg/trace v0.71.2/go.mod h1:cCkrxJC4m2KSDdfYlKb60W4yEguO5nBpmGquVX8Lb1w= 13 | github.com/DataDog/datadog-agent/pkg/util/log v0.71.2 h1:GaOMKewaJnnbaOX1cdsZbsQCmKxCNamPyxI7e7kSL6c= 14 | github.com/DataDog/datadog-agent/pkg/util/log v0.71.2/go.mod h1:lsew565lFp63tFjppWCKpZ1qVJrLhjFNGyTa/cwqZDY= 15 | github.com/DataDog/datadog-agent/pkg/util/scrubber v0.71.2 h1:0QkToZ7R5bpiHcaa9pBOVXaMODxh9pUvMb3kpfT/nik= 16 | github.com/DataDog/datadog-agent/pkg/util/scrubber v0.71.2/go.mod h1:0xxMqmIVxjAAXBUk2ntnvPuj0UjGDAEXZqLPLHF4eYg= 17 | github.com/DataDog/datadog-agent/pkg/version v0.71.2 h1:5wVVZrOCzvH6ka+J/3iKQH3rMJPIW1OaLOkOO/DRX8U= 18 | github.com/DataDog/datadog-agent/pkg/version v0.71.2/go.mod h1:FYj51C1ib86rpr5tlLEep9jitqvljIJ5Uz2rrimGTeY= 19 | github.com/DataDog/datadog-go/v5 v5.8.1 h1:+GOES5W9zpKlhwHptZVW2C0NLVf7ilr7pHkDcbNvpIc= 20 | github.com/DataDog/datadog-go/v5 v5.8.1/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= 21 | github.com/DataDog/dd-trace-go/contrib/aws/aws-sdk-go-v2/v2 v2.3.1 h1:vdkOoLp1mHhtkW5paGl+7N2EKtP9tnXBPgsiC75oceQ= 22 | github.com/DataDog/dd-trace-go/contrib/aws/aws-sdk-go-v2/v2 v2.3.1/go.mod h1:V92qa78iIgq3kY52MYu1QJBIzSTC2i7WXIEODhlg9Io= 23 | github.com/DataDog/dd-trace-go/contrib/net/http/v2 v2.3.1 h1:qi0gqNIjMgoxqTQ1rwx3IY07PLPyQ5ROh8HzrGM5qQY= 24 | github.com/DataDog/dd-trace-go/contrib/net/http/v2 v2.3.1/go.mod h1:/3bm09bPcIGsZfkE4+U084IyviAcHgpiRs/vTUo5qyM= 25 | github.com/DataDog/dd-trace-go/v2 v2.3.1 h1:DPtpoUOri4ZuTXnIBNUPu41S7iEf1uBoZBHisSFWxtg= 26 | github.com/DataDog/dd-trace-go/v2 v2.3.1/go.mod h1:yFomJ/rqKNLDbS9ohIDibdz8q9GK0MUSSkBdVDCibGA= 27 | github.com/DataDog/go-libddwaf/v4 v4.6.1 h1:wGUioRkQ2a5MYr2wTn5uZfMENbLV4uKXrkr6zCVInCs= 28 | github.com/DataDog/go-libddwaf/v4 v4.6.1/go.mod h1:/AZqP6zw3qGJK5mLrA0PkfK3UQDk1zCI2fUNCt4xftE= 29 | github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20250721125240-fdf1ef85b633 h1:ZRLR9Lbym748e8RznWzmSoK+OfV+8qW6SdNYA4/IqdA= 30 | github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20250721125240-fdf1ef85b633/go.mod h1:YFoTl1xsMzdSRFIu33oCSPS/3+HZAPGpO3oOM96wXCM= 31 | github.com/DataDog/go-sqllexer v0.1.9 h1:0R8FnSHGXRtZo70UxgCneL7Yu4PurswUAMb5N3kOIrI= 32 | github.com/DataDog/go-sqllexer v0.1.9/go.mod h1:vOw7Ia7z+z6nl3zGZlLIZe0vQlPtCPR906WIPBJadxc= 33 | github.com/DataDog/go-tuf v1.1.1-0.5.2 h1:YWvghV4ZvrQsPcUw8IOUMSDpqc3W5ruOIC+KJxPknv0= 34 | github.com/DataDog/go-tuf v1.1.1-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0= 35 | github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= 36 | github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= 37 | github.com/DataDog/sketches-go v1.4.7 h1:eHs5/0i2Sdf20Zkj0udVFWuCrXGRFig2Dcfm5rtcTxc= 38 | github.com/DataDog/sketches-go v1.4.7/go.mod h1:eAmQ/EBmtSO+nQp7IZMZVRPT4BQTmIc5RZQ+deGlTPM= 39 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 40 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 41 | github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= 42 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 43 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 44 | github.com/Nitro/urlsign v0.0.0-20181015102600-5c9420004fa4 h1:PzkFPpKVlnBHKKOrB4hIz/imgFE48mYoQR6t16UVZ78= 45 | github.com/Nitro/urlsign v0.0.0-20181015102600-5c9420004fa4/go.mod h1:YYI6psmVqfFYrABuvsEk9dXmhd4Sfea17A8I31ipqTM= 46 | github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w= 47 | github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= 48 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2 h1:t9yYsydLYNBk9cJ73rgPhPWqOh/52fcWDQB5b1JsKSY= 49 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.2/go.mod h1:IusfVNTmiSN3t4rhxWFaBAqn+mcNdwKtPcV16eYdgko= 50 | github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc= 51 | github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34= 52 | github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q= 53 | github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0= 54 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE= 55 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE= 56 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.14 h1:Nhcq+ODoD9FRQYI3lATy6iADS5maER3ZXSfE8v3FMh8= 57 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.14/go.mod h1:VlBbwTpgCj3rKWMVkEAYiAR3FKs7Mi3jALTMGfbfuns= 58 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 h1:p/9flfXdoAnwJnuW9xHEAFY22R3A6skYkW19JFF9F+8= 59 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12/go.mod h1:ZTLHakoVCTtW8AaLGSwJ3LXqHD9uQKnOcv1TrpO6u2k= 60 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 h1:2lTWFvRcnWFFLzHWmtddu5MTchc5Oj2OOey++99tPZ0= 61 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12/go.mod h1:hI92pK+ho8HVcWMHKHrK3Uml4pfG7wvL86FzO0LVtQQ= 62 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= 63 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= 64 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.12 h1:itu4KHu8JK/N6NcLIISlf3LL1LccMqruLUXZ9y7yBZw= 65 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.12/go.mod h1:i+6vTU3xziikTY3vcox23X8pPGW5X3wVgd1VZ7ha+x8= 66 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.3 h1:28+obyib2FhFKASJ6qSPbuteiy0nvvcvfItdAAYure0= 67 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.52.3/go.mod h1:7EyplKXfbtwOuOShW70orLOWaYPdRKdDiKyACL6+kgk= 68 | github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.9 h1:FF8FL70LlMdxjaz4/JpBmOD8278gqPNcnkeDiLWRHpU= 69 | github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.9/go.mod h1:lFu9i1q0upcFg+pOvKZI894+wobxBp1QNrTbL9K0ATM= 70 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= 71 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= 72 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.3 h1:NEe7FaViguRQEm8zl8Ay/kC/QRsMtWUiCGZajQIsLdc= 73 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.3/go.mod h1:JLuCKu5VfiLBBBl/5IzZILU7rxS0koQpHzMOCzycOJU= 74 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.12 h1:1W0j7DSEnEKnBF4Sxm/fNEzPBtE9/62GbVN4/H2a9LI= 75 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.12/go.mod h1:/kejjnGxwnSc0MHYNScIX/cXpo43xpL3hBRZLVmDSxE= 76 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 h1:MM8imH7NZ0ovIVX7D2RxfMDv7Jt9OiUXkcQ+GqywA7M= 77 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12/go.mod h1:gf4OGwdNkbEsb7elw2Sy76odfhwNktWII3WgvQgQQ6w= 78 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.12 h1:R3uW0iKl8rgNEXNjVGliW/oMEh9fO/LlUEV8RvIFr1I= 79 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.12/go.mod h1:XEttbEr5yqsw8ebi7vlDoGJJjMXRez4/s9pibpJyL5s= 80 | github.com/aws/aws-sdk-go-v2/service/kinesis v1.41.1 h1:6qOdmkKKf5YzxHbdpqsGttLoTEIYLNSlyXy5v106eUc= 81 | github.com/aws/aws-sdk-go-v2/service/kinesis v1.41.1/go.mod h1:r+EHvZe9yNk9rrnW5wpF5Ps6IjkEstus/u8UTZFVbKw= 82 | github.com/aws/aws-sdk-go-v2/service/s3 v1.89.1 h1:Dq82AV+Qxpno/fG162eAhnD8d48t9S+GZCfz7yv1VeA= 83 | github.com/aws/aws-sdk-go-v2/service/s3 v1.89.1/go.mod h1:MbKLznDKpf7PnSonNRUVYZzfP0CeLkRIUexeblgKcU4= 84 | github.com/aws/aws-sdk-go-v2/service/sfn v1.39.10 h1:ggXvgyAQ4ow+sQQbZGaiX1mspa/0u+q2NEZLrA6HQWs= 85 | github.com/aws/aws-sdk-go-v2/service/sfn v1.39.10/go.mod h1:v5vsb7rtT3os/XJHk6+eJRm94wW91mvUnphUChy1wEk= 86 | github.com/aws/aws-sdk-go-v2/service/sns v1.39.2 h1:7nFu56/9bT2FvVt6IWDG9FXBwLmAUBsm9ddIg8bcp+E= 87 | github.com/aws/aws-sdk-go-v2/service/sns v1.39.2/go.mod h1:/MkhVPJvg4zY6owmU1+swTqB76qvhm+jqOS4j1z3xVw= 88 | github.com/aws/aws-sdk-go-v2/service/sqs v1.42.12 h1:gKm7A7ShrL5Pn53ec5GqzQB2tWvk978bbasFEZfwu2U= 89 | github.com/aws/aws-sdk-go-v2/service/sqs v1.42.12/go.mod h1:tQRO8Q9JzfImAG5sG3TUyeF/EqCXwvZ7TA8gz5Whpec= 90 | github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw= 91 | github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I= 92 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M= 93 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA= 94 | github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY= 95 | github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs= 96 | github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= 97 | github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 98 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 99 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 100 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 101 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 102 | github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 103 | github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 104 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 105 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 106 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 107 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= 108 | github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= 109 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 110 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 111 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 112 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 113 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 114 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 115 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= 116 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 117 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 118 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 119 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 120 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 121 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 122 | github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= 123 | github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 124 | github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= 125 | github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 126 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 127 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 128 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 129 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 130 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 131 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 132 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 133 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 134 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 135 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 136 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 137 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 138 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 139 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 140 | github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= 141 | github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= 142 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 143 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 144 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 145 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 146 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 147 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 148 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 149 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 150 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 151 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 152 | github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= 153 | github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= 154 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 155 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 156 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 157 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 158 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 159 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 160 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 161 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 162 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 163 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 164 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 165 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 166 | github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= 167 | github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= 168 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 169 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 170 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 171 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 172 | github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= 173 | github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= 174 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 175 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 176 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 177 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 178 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 179 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 180 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 181 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 182 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 183 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 184 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 185 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 186 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 187 | github.com/nitro/lazypdf/v2 v2.0.0-20251222085501-6157acf24854 h1:sr7laRsZSbLr0eoUf3hiYRyn7tTwy3Rx6+JjmkHsmfI= 188 | github.com/nitro/lazypdf/v2 v2.0.0-20251222085501-6157acf24854/go.mod h1:hIq0I7f/dyOQlD8RP0wFK57BCgKiFZxl9uO9jeso2y8= 189 | github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.133.0 h1:iPei+89a2EK4LuN4HeIRzZNE6XxCyrKfBKG3BkK/ViU= 190 | github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.133.0/go.mod h1:asV77TgnGfc7A+a9jggdsnlLlW5dnJT8RroVuf5slko= 191 | github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.133.0 h1:4ca2pM3+xDMB9H3UnhjAiNg7EpIydZ7HdohOexU8xb8= 192 | github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.133.0/go.mod h1:3N2Saf55l9vrxjbf3KCEcBjbLHDZtbN4nPcxREztpPU= 193 | github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 194 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 195 | github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= 196 | github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= 197 | github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= 198 | github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 199 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 200 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 201 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 202 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 203 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 204 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 205 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 206 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 207 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 208 | github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= 209 | github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 210 | github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= 211 | github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 212 | github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY= 213 | github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3/go.mod h1:vl5+MqJ1nBINuSsUI2mGgH79UweUT/B5Fy8857PqyyI= 214 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 215 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 216 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 217 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 218 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 219 | github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g= 220 | github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU= 221 | github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= 222 | github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= 223 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 224 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 225 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 226 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 227 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 228 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 229 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 230 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 231 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 232 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 233 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 234 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 235 | github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= 236 | github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= 237 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 238 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 239 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 240 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 241 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 242 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 243 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 244 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 245 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 246 | github.com/theckman/httpforwarded v0.4.0 h1:N55vGJT+6ojTnLY3LQCNliJC4TW0P0Pkeys1G1WpX2w= 247 | github.com/theckman/httpforwarded v0.4.0/go.mod h1:GVkFynv6FJreNbgH/bpOU9ITDZ7a5WuzdNCtIMI1pVI= 248 | github.com/tinylib/msgp v1.5.0 h1:GWnqAE54wmnlFazjq2+vgr736Akg58iiHImh+kPY2pc= 249 | github.com/tinylib/msgp v1.5.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= 250 | github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= 251 | github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= 252 | github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= 253 | github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= 254 | github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI= 255 | github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 256 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 257 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 258 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 259 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 260 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 261 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 262 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 263 | go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 264 | go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 265 | go.opentelemetry.io/collector/component v1.44.0 h1:SX5UO/gSDm+1zyvHVRFgpf8J1WP6U3y/SLUXiVEghbE= 266 | go.opentelemetry.io/collector/component v1.44.0/go.mod h1:geKbCTNoQfu55tOPiDuxLzNZsoO9//HRRg10/8WusWk= 267 | go.opentelemetry.io/collector/component/componentstatus v0.133.0 h1:fIcFKg+yPhpvOJKeMph9TtSC4DIGdIuNmxvUB0UGcoc= 268 | go.opentelemetry.io/collector/component/componentstatus v0.133.0/go.mod h1:biQWms9cgXSZu3nb92Z0bA9uHh9lEhgmQ8CF4HLmu8Y= 269 | go.opentelemetry.io/collector/component/componenttest v0.133.0 h1:mg54QqXC+GNqLHa9y6Efh3X5Di4XivjgJr6mzvfVQR8= 270 | go.opentelemetry.io/collector/component/componenttest v0.133.0/go.mod h1:E+oqRK03WjG/b1aX1pd0CfTKh12MPTKbEBaBROp4w0M= 271 | go.opentelemetry.io/collector/consumer v1.39.0 h1:Jc6la3uacHbznX5ORmh16Nddh23ZxBzoiNF2L0wD2Ks= 272 | go.opentelemetry.io/collector/consumer v1.39.0/go.mod h1:tW2BXyntjvlKrRc+mwistt1KuC/b4mTfTkc8zWjeeRY= 273 | go.opentelemetry.io/collector/consumer/consumertest v0.133.0 h1:MteqaGpgmHVHFqnB7A2voGleA2j51qJyVfX5x/wm+8I= 274 | go.opentelemetry.io/collector/consumer/consumertest v0.133.0/go.mod h1:vHGknLn/RRUcMQuuBDt+SgrpDN46DBJyqRnWXm3gLwY= 275 | go.opentelemetry.io/collector/consumer/xconsumer v0.133.0 h1:Xx4Yna/We4qDlbAla1nfxgkvujzWRuR8bqqwsLLvYSg= 276 | go.opentelemetry.io/collector/consumer/xconsumer v0.133.0/go.mod h1:he874Md/0uAS2Fs+TDHAy10OBLRSw8233LdREizVvG4= 277 | go.opentelemetry.io/collector/featuregate v1.44.0 h1:/GeGhTD8f+FNWS7C4w1Dj0Ui9Jp4v2WAdlXyW1p3uG8= 278 | go.opentelemetry.io/collector/featuregate v1.44.0/go.mod h1:d0tiRzVYrytB6LkcYgz2ESFTv7OktRPQe0QEQcPt1L4= 279 | go.opentelemetry.io/collector/internal/telemetry v0.138.0 h1:xHHYlPh1vVvr+ip0ct288l1joc4bsEeHh0rcY3WVXJo= 280 | go.opentelemetry.io/collector/internal/telemetry v0.138.0/go.mod h1:evqf71fdIMXdQEofbs1bVnBUzfF6zysLMLR9bEAS9Xw= 281 | go.opentelemetry.io/collector/pdata v1.44.0 h1:q/EfWDDKrSaf4hjTIzyPeg1ZcCRg1Uj7VTFnGfNVdk8= 282 | go.opentelemetry.io/collector/pdata v1.44.0/go.mod h1:LnsjYysFc3AwMVh6KGNlkGKJUF2ReuWxtD9Hb3lSMZk= 283 | go.opentelemetry.io/collector/pdata/pprofile v0.133.0 h1:ewFYqV2FU4D0ixTdkJueaI2JGCoeiIJisX8EdHejDi8= 284 | go.opentelemetry.io/collector/pdata/pprofile v0.133.0/go.mod h1:5l4/B0iCxzoVkA7eOLzIHV0AUEO2IKypTHTLq9JKsHs= 285 | go.opentelemetry.io/collector/pdata/testdata v0.133.0 h1:K0q47qecWVJf0sWbeWfifbJ72TiqR+A2PCsMkCEKvus= 286 | go.opentelemetry.io/collector/pdata/testdata v0.133.0/go.mod h1:/emFpIox/mi7FucvsSn54KsiMh/iy7BUviqgURNVT6U= 287 | go.opentelemetry.io/collector/pipeline v1.44.0 h1:EFdFBg3Wm2BlMtQbUeork5a4KFpS6haInSr+u/dk8rg= 288 | go.opentelemetry.io/collector/pipeline v1.44.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= 289 | go.opentelemetry.io/collector/processor v1.39.0 h1:QwPJxJnFZwojo09Vfnvph7A27TauxxvA1koO6nr87O8= 290 | go.opentelemetry.io/collector/processor v1.39.0/go.mod h1:WQWZqKmrlJcLjirnQOULxYgWV6h5oxK6FQNiFgw53i8= 291 | go.opentelemetry.io/collector/processor/processorhelper v0.133.0 h1:3w/wvSmzyCvyNXjUQihH/VLQ+Tnzn3MlQNbv1AEoXiU= 292 | go.opentelemetry.io/collector/processor/processorhelper v0.133.0/go.mod h1:lTlC8tGOBqkpdwGXCmaDnWXc2jqIrRUKvV7eK26Thc4= 293 | go.opentelemetry.io/collector/processor/processortest v0.133.0 h1:PAuOr8Pwj/LAuey2LW1fix0vvnE+WwGpSF7bghaxjEE= 294 | go.opentelemetry.io/collector/processor/processortest v0.133.0/go.mod h1:fEhWs9DCe431+iFke1WmlxqjcRDN25GLRXdktKAPyw8= 295 | go.opentelemetry.io/collector/processor/xprocessor v0.133.0 h1:V5YMrXUgClh3awWOdigGXHxvq/Ira2wLDj4DJLqB+Eo= 296 | go.opentelemetry.io/collector/processor/xprocessor v0.133.0/go.mod h1:5gDFI+pGIzoFQeBUM4QZ4E0B+SaU0e+2V7Td+ONoU4M= 297 | go.opentelemetry.io/contrib/bridges/otelzap v0.13.0 h1:aBKdhLVieqvwWe9A79UHI/0vgp2t/s2euY8X59pGRlw= 298 | go.opentelemetry.io/contrib/bridges/otelzap v0.13.0/go.mod h1:SYqtxLQE7iINgh6WFuVi2AI70148B8EI35DSk0Wr8m4= 299 | go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= 300 | go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 301 | go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= 302 | go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= 303 | go.opentelemetry.io/otel/log/logtest v0.14.0 h1:BGTqNeluJDK2uIHAY8lRqxjVAYfqgcaTbVk1n3MWe5A= 304 | go.opentelemetry.io/otel/log/logtest v0.14.0/go.mod h1:IuguGt8XVP4XA4d2oEEDMVDBBCesMg8/tSGWDjuKfoA= 305 | go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= 306 | go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 307 | go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= 308 | go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 309 | go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 310 | go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 311 | go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 312 | go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 313 | go.opentelemetry.io/proto/slim/otlp v1.8.0 h1:afcLwp2XOeCbGrjufT1qWyruFt+6C9g5SOuymrSPUXQ= 314 | go.opentelemetry.io/proto/slim/otlp v1.8.0/go.mod h1:Yaa5fjYm1SMCq0hG0x/87wV1MP9H5xDuG/1+AhvBcsI= 315 | go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0 h1:Uc+elixz922LHx5colXGi1ORbsW8DTIGM+gg+D9V7HE= 316 | go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.1.0/go.mod h1:VyU6dTWBWv6h9w/+DYgSZAPMabWbPTFTuxp25sM8+s0= 317 | go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0 h1:i8YpvWGm/Uq1koL//bnbJ/26eV3OrKWm09+rDYo7keU= 318 | go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.1.0/go.mod h1:pQ70xHY/ZVxNUBPn+qUWPl8nwai87eWdqL3M37lNi9A= 319 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 320 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 321 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 322 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 323 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 324 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 325 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 326 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 327 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 328 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 329 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 330 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 331 | golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= 332 | golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 333 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= 334 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 335 | golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= 336 | golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= 337 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 338 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 339 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 340 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 341 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 342 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 343 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 344 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 345 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 346 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 347 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 348 | golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= 349 | golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 350 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 351 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 352 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 353 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 354 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 355 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 356 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 357 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 366 | golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 367 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 368 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 369 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 370 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 371 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 372 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 373 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 374 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 375 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 376 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 377 | golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= 378 | golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 379 | golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 380 | golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 381 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 382 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 383 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 384 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 385 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 386 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 387 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 388 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 389 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 390 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 391 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 392 | golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 393 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 394 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 395 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 396 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 397 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= 398 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 399 | google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= 400 | google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= 401 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 402 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 403 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 404 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 405 | gopkg.in/DataDog/dd-trace-go.v1 v1.74.8 h1:h96ji92t9eXbPvSWhJ+lrPWetHiQNYlt48JKRO09NFA= 406 | gopkg.in/DataDog/dd-trace-go.v1 v1.74.8/go.mod h1:LpHbtHsCZBlm1HWrlVOUQcEXwMWZnU6yMvmtd1GvSDI= 407 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 408 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 409 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 410 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 411 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 412 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 413 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 414 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 415 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 416 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 417 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 418 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 419 | --------------------------------------------------------------------------------