From 56d1aaf6c63fecef87709ad998611d57095a8fc9 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Mon, 22 Sep 2025 14:24:50 +0300 Subject: [PATCH 01/47] chore!: added otlp tracing supporting --- configs/config.toml | 4 + configs/testing.toml | 4 + internal/infrastructure/config/config.go | 8 ++ internal/infrastructure/httpserver/config.go | 6 ++ .../infrastructure/httpserver/httpserver.go | 35 +++++++- .../httpserver/routes_bucket.go | 5 ++ .../infrastructure/httpserver/routes_task.go | 7 +- internal/infrastructure/httpserver/tracer.go | 88 +++++++++++++++++++ 8 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 internal/infrastructure/httpserver/tracer.go diff --git a/configs/config.toml b/configs/config.toml index c8c46f8..d223953 100644 --- a/configs/config.toml +++ b/configs/config.toml @@ -10,6 +10,10 @@ level = "info" address = "loki:3100" enable_loki = true +[server.http.tracer] +address = "jaeger:4317" +enable_jaeger = false + [ocr.dedoc] address = "http://localhost:8004" timeout = 300 diff --git a/configs/testing.toml b/configs/testing.toml index 35cd2aa..95c5a94 100644 --- a/configs/testing.toml +++ b/configs/testing.toml @@ -10,6 +10,10 @@ level = "info" enable_loki = false address = "localhost:3100" +[server.http.tracer] +address = "localhost:4317" +enable_jaeger = false + [ocr.dedoc] address = "http://localhost:8004" timeout = 300 diff --git a/internal/infrastructure/config/config.go b/internal/infrastructure/config/config.go index c8bab4f..b484c9f 100644 --- a/internal/infrastructure/config/config.go +++ b/internal/infrastructure/config/config.go @@ -93,6 +93,14 @@ func FromFile(filePath string) (*Config, error) { if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } + bindErr = viperInstance.BindEnv("server.http.tracer.address", "WATCHTOWER__SERVER__HTTP__TRACER__ADDRESS") + if bindErr != nil { + return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) + } + bindErr = viperInstance.BindEnv("server.http.tracer.enable_jaeger", "WATCHTOWER__SERVER__HTTP__TRACER__ENABLE_JAEGER") + if bindErr != nil { + return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) + } // OCR config bindErr = viperInstance.BindEnv("ocr.dedoc.address", "WATCHTOWER__OCR__DEDOC__ADDRESS") diff --git a/internal/infrastructure/httpserver/config.go b/internal/infrastructure/httpserver/config.go index a8cf9e1..9f22038 100644 --- a/internal/infrastructure/httpserver/config.go +++ b/internal/infrastructure/httpserver/config.go @@ -3,6 +3,7 @@ package httpserver type Config struct { Address string `mapstructure:"address"` Logger LoggerConfig `mapstructure:"logger"` + Tracer TracerConfig `mapstructure:"tracer"` } type LoggerConfig struct { @@ -10,3 +11,8 @@ type LoggerConfig struct { Address string `mapstructure:"address"` EnableLoki bool `mapstructure:"enable_loki"` } + +type TracerConfig struct { + Address string `mapstructure:"address"` + EnableJaeger bool `mapstructure:"enable_jaeger"` +} diff --git a/internal/infrastructure/httpserver/httpserver.go b/internal/infrastructure/httpserver/httpserver.go index 3f3d0fc..f18b4ac 100644 --- a/internal/infrastructure/httpserver/httpserver.go +++ b/internal/infrastructure/httpserver/httpserver.go @@ -3,10 +3,12 @@ package httpserver import ( "context" "fmt" + "log/slog" "github.com/labstack/echo-contrib/echoprometheus" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "go.opentelemetry.io/otel/trace" "watchtower/internal/application/usecase" echoSwagger "github.com/swaggo/echo-swagger" @@ -15,6 +17,7 @@ import ( type Server struct { server *echo.Echo + tracer trace.Tracer config *Config uc *usecase.UseCase @@ -36,8 +39,38 @@ func (s *Server) setupServer() { if s.config.Logger.EnableLoki { lokiLog := InitLokiLogger(s.config.Logger) s.server.Use(lokiLog.LokiLoggerMW()) + } else { + s.server.Use(InitLocalLogger(s.config.Logger)) } - s.server.Use(InitLocalLogger(s.config.Logger)) + + if s.config.Tracer.EnableJaeger { + tp, err := InitTracer(s.config) + if err != nil { + slog.Error("failed to initialize tracer", slog.String("err", err.Error())) + } else { + s.tracer = tp + defer func() { + //if err := s.tracer.Cleanup(context.Background()); err != nil { + // slog.Error("error shutting down tracer provider", slog.String("err", err.Error())) + //} + //s.tracer.Shutdown(context.Background()) + }() + + //traceFilterMW := otelecho.Middleware( + // AppName, + // otelecho.WithTracerProvider(otel.GetTracerProvider()), + // otelecho.WithPropagators(otel.GetTextMapPropagator()), + // otelecho.WithSkipper(func(c echo.Context) bool { + // return shouldSkipTrace(c.Path()) + // }), + //) + // + //s.server.Use(tracingFilter()) + } + } + + _, span := s.tracer.Start(context.Background(), "init-watchtower") + span.End() s.server.Use(middleware.CORS()) s.server.Use(middleware.Recover()) diff --git a/internal/infrastructure/httpserver/routes_bucket.go b/internal/infrastructure/httpserver/routes_bucket.go index 05cc165..8525127 100644 --- a/internal/infrastructure/httpserver/routes_bucket.go +++ b/internal/infrastructure/httpserver/routes_bucket.go @@ -28,8 +28,13 @@ func (s *Server) CreateStorageBucketsGroup() error { // @Router /cloud/buckets [get] func (s *Server) GetBuckets(eCtx echo.Context) error { ctx := eCtx.Request().Context() + + _, span := GetTracer().Start(ctx, "get-buckets") + defer span.End() + watcherDirs, err := s.uc.GetObjectStorage().GetBuckets(ctx) if err != nil { + span.RecordError(err) return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } diff --git a/internal/infrastructure/httpserver/routes_task.go b/internal/infrastructure/httpserver/routes_task.go index 518ed9a..4b4d8e3 100644 --- a/internal/infrastructure/httpserver/routes_task.go +++ b/internal/infrastructure/httpserver/routes_task.go @@ -38,8 +38,12 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { } ctx := eCtx.Request().Context() - tasks, err := s.uc.GetTaskManager().GetAll(ctx, bucket) + tCtx, span := GetTracer().Start(ctx, "load-buckets") + defer span.End() + + tasks, err := s.uc.GetTaskManager().GetAll(tCtx, bucket) if err != nil { + span.RecordError(err) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -50,6 +54,7 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { inputTaskStatus, err := mapping.TaskStatusFromString(status) if err != nil { + span.RecordError(err) return echo.NewHTTPError(http.StatusUnprocessableEntity, "unknown status") } diff --git a/internal/infrastructure/httpserver/tracer.go b/internal/infrastructure/httpserver/tracer.go new file mode 100644 index 0000000..10eeb41 --- /dev/null +++ b/internal/infrastructure/httpserver/tracer.go @@ -0,0 +1,88 @@ +package httpserver + +import ( + "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer + +func InitTracer(config *Config) (trace.Tracer, error) { + tracerConfig := config.Tracer + + res := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.TelemetrySDKLanguageGo, + semconv.ServiceNameKey.String(AppName), + ) + + client := otlptracegrpc.NewClient( + otlptracegrpc.WithEndpoint(tracerConfig.Address), + otlptracegrpc.WithInsecure(), + ) + + ctx := context.Background() + traceExporter, err := otlptrace.New(ctx, client) + if err != nil { + return nil, err + } + + bsp := sdktrace.NewBatchSpanProcessor(traceExporter) + sampler := sdktrace.AlwaysSample() + tp := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sampler), + sdktrace.WithResource(res), + sdktrace.WithSpanProcessor(bsp), + ) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + otel.SetTracerProvider(tp) + + tracer = tp.Tracer(AppName) + + return tracer, nil +} + +// GetTracer gets global Tracer +// func GetTracer() trace.Tracer { +func GetTracer() trace.Tracer { + return tracer +} + +//func tracingFilter() echo.MiddlewareFunc { +// return otelecho.Middleware(AppName, +// otelecho.WithTracerProvider(otel.GetTracerProvider()), +// otelecho.WithPropagators(otel.GetTextMapPropagator()), +// otelecho.WithSkipper(func(c echo.Context) bool { +// return shouldSkipTrace(c.Path()) +// }), +// ) +//} +// +//func shouldSkipTrace(path string) bool { +// // List of paths to exclude from tracing +// excludedPaths := []string{ +// "/health", +// "/metrics", +// "/favicon.ico", +// "/static/", +// } +// +// for _, excluded := range excludedPaths { +// if strings.HasPrefix(path, excluded) { +// return true +// } +// } +// return false +//} From d20bdf022a8f8f5ff6f3ac683fdbe9c460a8bd7f Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Mon, 22 Sep 2025 14:25:06 +0300 Subject: [PATCH 02/47] chore: go mod tody --- go.mod | 39 ++++++++++++++++++-------- go.sum | 86 ++++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 84 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index 816d356..47dd72a 100644 --- a/go.mod +++ b/go.mod @@ -5,24 +5,32 @@ go 1.24.0 require ( github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 + github.com/jonathanhecl/chunker v0.0.1 github.com/labstack/echo-contrib v0.17.4 - github.com/labstack/echo/v4 v4.13.3 + github.com/labstack/echo/v4 v4.13.4 github.com/minio/minio-go/v7 v7.0.94 github.com/rabbitmq/amqp091-go v1.10.0 github.com/redis/go-redis/v9 v9.11.0 github.com/samber/slog-loki/v2 v2.2.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.4 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 + golang.org/x/sync v0.17.0 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -30,16 +38,18 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jonathanhecl/chunker v0.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect @@ -74,15 +84,20 @@ require ( github.com/tinylib/msgp v1.3.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.33.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 4bc39fa..42f10fb 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -27,6 +29,11 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= @@ -37,16 +44,16 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -70,8 +77,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= -github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= -github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -146,8 +153,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= @@ -162,36 +169,57 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 3f6a021226ec11e5fa99d3249d7d43b49de8c94f Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Mon, 22 Sep 2025 14:26:20 +0300 Subject: [PATCH 03/47] chore: added new env variables --- .env | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env b/.env index 1201546..8d04e40 100644 --- a/.env +++ b/.env @@ -5,6 +5,8 @@ WATCHTOWER__SERVER__HTTP__ADDRESS=0.0.0.0:2893 WATCHTOWER__SERVER__HTTP__LOGGER__LEVEL=DEBUG WATCHTOWER__SERVER__HTTP__LOGGER__ADDRESS=localhost:3100 WATCHTOWER__SERVER__HTTP__LOGGER__ENABLE_LOKI=false +WATCHTOWER__SERVER__HTTP__TRACER__ADDRESS=localhost:4317 +WATCHTOWER__SERVER__HTTP__TRACER__ENABLE_JAEGER=true WATCHTOWER__OCR__DEDOC__ADDRESS=localhost:8004 WATCHTOWER__OCR__DEDOC__TIMEOUT=100s From b1e2b3f8892370f683d48e1e2933b2967582d780 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 23 Sep 2025 10:00:58 +0300 Subject: [PATCH 04/47] chore: impled tracing chain for interfaces --- go.mod | 1 + go.sum | 2 + internal/application/dto/message.go | 6 +- .../services/doc-storage/public.go | 6 - internal/application/usecase/usecase.go | 38 +++--- internal/infrastructure/dedoc/dedoc.go | 27 ++++- .../infrastructure/doc-storage/doc-storage.go | 93 +++++---------- .../infrastructure/httpserver/httpserver.go | 28 ++--- .../httpserver/routes_bucket.go | 21 +++- .../httpserver/routes_object.go | 109 +++++++++++++++--- .../infrastructure/httpserver/routes_task.go | 32 +++-- internal/infrastructure/httpserver/tracer.go | 93 +++++++-------- internal/infrastructure/rmq/rmq.go | 81 ++++++++++++- internal/infrastructure/s3/s3.go | 9 ++ 14 files changed, 352 insertions(+), 194 deletions(-) diff --git a/go.mod b/go.mod index 47dd72a..7663e40 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 42f10fb..82dbda2 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 h1:6YeICKmGrvgJ5th4+OMNpcuoB6q/Xs8gt0YCO7MUv1k= +go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0/go.mod h1:ZEA7j2B35siNV0T00aapacNzjz4tvOlNoHp0ncCfwNQ= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= diff --git a/internal/application/dto/message.go b/internal/application/dto/message.go index 8f3b1b8..28cf15c 100644 --- a/internal/application/dto/message.go +++ b/internal/application/dto/message.go @@ -1,8 +1,12 @@ package dto -import "github.com/google/uuid" +import ( + "context" + "github.com/google/uuid" +) type Message struct { + Ctx context.Context EventId uuid.UUID `json:"event_id"` Body TaskEvent `json:"body"` } diff --git a/internal/application/services/doc-storage/public.go b/internal/application/services/doc-storage/public.go index dd8794b..8a5fe5d 100644 --- a/internal/application/services/doc-storage/public.go +++ b/internal/application/services/doc-storage/public.go @@ -7,7 +7,6 @@ import ( ) type IDocumentStorage interface { - IIndexManager IDocumentManager } @@ -16,8 +15,3 @@ type IDocumentManager interface { UpdateDocument(ctx context.Context, folder string, document *dto.DocumentObject) error StoreDocument(ctx context.Context, folder string, document *dto.DocumentObject) (string, error) } - -type IIndexManager interface { - CreateIndex(ctx context.Context, folder string) error - DeleteIndex(ctx context.Context, folder string) error -} diff --git a/internal/application/usecase/usecase.go b/internal/application/usecase/usecase.go index 5b50210..a8a66d8 100644 --- a/internal/application/usecase/usecase.go +++ b/internal/application/usecase/usecase.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "log/slog" "path" "sync" "time" @@ -61,25 +62,11 @@ func (uc *UseCase) LaunchWatcherListener(ctx context.Context) { go func() { for { select { - case taskEvent := <-uc.processorCh: - // TODO: Disabled for TechDebt - _ = uc.isTaskAlreadyProcessed(ctx, &taskEvent) - // if uc.isTaskAlreadyProcessed(ctx, &taskEvent) { - // log.Printf("task has been already processed: %s", taskEvent.ID) - // continue - // } - - status, msg := dto.Pending, EmptyMessage - if err := uc.publishToQueue(ctx, taskEvent); err != nil { - status, msg = dto.Failed, err.Error() - log.Printf("failed to pulish task to queue: %v", err) - } - - uc.updateTaskStatus(ctx, &taskEvent, status, msg) case cMsg := <-uc.consumerCh: + ctx = cMsg.Ctx uc.Processing(ctx, cMsg) case <-ctx.Done(): - log.Println("terminated processing") + slog.Info("terminating processing") return } } @@ -240,7 +227,10 @@ func (uc *UseCase) StoreFileToStorage(ctx context.Context, fileForm dto.FileToUp // TODO: Disabled for TechDebt // id := utils.GenerateUniqID(fileForm.Bucket, fileForm.FilePath) id := utils.GenerateTaskID() - log.Printf("[%s]: publish task: %s", fileForm.Bucket, id) + slog.Info("publish task to queue", + slog.String("bucket", fileForm.Bucket), + slog.String("task-id", id), + ) task := dto.TaskEvent{ ID: id, @@ -261,7 +251,19 @@ func (uc *UseCase) StoreFileToStorage(ctx context.Context, fileForm dto.FileToUp return nil, fmt.Errorf("failed to upload file: %w", err) } - uc.processorCh <- task + // TODO: Disabled for TechDebt + _ = uc.isTaskAlreadyProcessed(ctx, &task) + // if uc.isTaskAlreadyProcessed(ctx, &taskEvent) { + // log.Printf("task has been already processed: %s", taskEvent.ID) + // continue + // } + + status, msg := dto.Pending, EmptyMessage + if err = uc.publishToQueue(ctx, task); err != nil { + status, msg = dto.Failed, err.Error() + log.Printf("failed to pulish task to queue: %v", err) + } + uc.updateTaskStatus(ctx, &task, status, msg) return &task, nil } diff --git a/internal/infrastructure/dedoc/dedoc.go b/internal/infrastructure/dedoc/dedoc.go index f73c52b..eb6962b 100644 --- a/internal/infrastructure/dedoc/dedoc.go +++ b/internal/infrastructure/dedoc/dedoc.go @@ -8,8 +8,10 @@ import ( "mime/multipart" "time" + "go.opentelemetry.io/otel/codes" "watchtower/internal/application/dto" "watchtower/internal/application/utils" + "watchtower/internal/infrastructure/httpserver" ) const RecognitionURL = "/ocr_extract_text" @@ -25,19 +27,30 @@ func New(config *Config) *DedocClient { } func (dc *DedocClient) Recognize(ctx context.Context, inputFile dto.InputFile) (*dto.Recognized, error) { + ctx, span := httpserver.GlobalTracer.Start(ctx, "recognize-file") + defer span.End() + var buf bytes.Buffer mpw := multipart.NewWriter(&buf) fileForm, err := mpw.CreateFormFile("file", inputFile.Name) if err != nil { - return nil, fmt.Errorf("failed to create form file for dedoc: %w", err) + err = fmt.Errorf("failed to create form file for dedoc: %w", err) + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err } if _, err = fileForm.Write(inputFile.Data.Bytes()); err != nil { - return nil, fmt.Errorf("failed to write form file for dedoc: %w", err) + err = fmt.Errorf("failed to write form file for dedoc: %w", err) + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err } if err = mpw.Close(); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return nil, err } @@ -47,13 +60,19 @@ func (dc *DedocClient) Recognize(ctx context.Context, inputFile dto.InputFile) ( respData, err := utils.POST(ctx, &buf, targetURL, mimeType, timeoutReq) if err != nil { - return nil, fmt.Errorf("failed send request: %w", err) + err = fmt.Errorf("failed send request: %w", err) + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err } var recData dto.Recognized _ = json.Unmarshal(respData, &recData) if len(recData.Text) == 0 { - return nil, fmt.Errorf("returned empty content data") + err = fmt.Errorf("returned empty content data") + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err } return &recData, nil diff --git a/internal/infrastructure/doc-storage/doc-storage.go b/internal/infrastructure/doc-storage/doc-storage.go index a69d936..4e57a02 100644 --- a/internal/infrastructure/doc-storage/doc-storage.go +++ b/internal/infrastructure/doc-storage/doc-storage.go @@ -5,13 +5,16 @@ import ( "context" "encoding/json" "fmt" - "log" + "log/slog" "net/http" "strings" "time" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "watchtower/internal/application/dto" "watchtower/internal/application/utils" + "watchtower/internal/infrastructure/httpserver" ) const DocumentJsonMime = "application/json" @@ -32,6 +35,9 @@ func (dsc *DocSearcherClient) StoreDocument( folder string, doc *dto.DocumentObject, ) (string, error) { + ctx, span := httpserver.GlobalTracer.Start(ctx, "store-document") + defer span.End() + storeDoc := StoreDocumentForm{ FileName: doc.FileName, FilePath: doc.FilePath, @@ -43,7 +49,10 @@ func (dsc *DocSearcherClient) StoreDocument( jsonData, err := json.Marshal(storeDoc) if err != nil { - return "", fmt.Errorf("failed while marshaling doc: %w", err) + err = fmt.Errorf("failed while marshaling doc: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return "", err } buildURL := strings.Builder{} @@ -51,21 +60,35 @@ func (dsc *DocSearcherClient) StoreDocument( buildURL.WriteString(fmt.Sprintf("%s/storage/%s/create?force=true", ApiVersionPrefix, folder)) targetURL := buildURL.String() - log.Printf("storing document to index %s", folder) + slog.Debug("storing document to index", + slog.String("index", folder), + slog.String("file-path", doc.FilePath), + ) reqBody := bytes.NewBuffer(jsonData) timeoutReq := time.Duration(300) * time.Second respData, err := utils.PUT(ctx, reqBody, targetURL, DocumentJsonMime, timeoutReq) if err != nil { - return "", fmt.Errorf("failed to store document to storage: %w", err) + err = fmt.Errorf("failed to store document to storage: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return "", err } status := &StoreDocumentResult{} err = json.Unmarshal(respData, status) if err != nil { - return "", fmt.Errorf("failed to unmarshal response body: %w", err) + err = fmt.Errorf("failed to unmarshal response body: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return "", err } + span.SetAttributes( + attribute.String("index-id", folder), + attribute.String("file-path", doc.FilePath), + ) + return status.Message, nil } @@ -96,63 +119,3 @@ func (dsc *DocSearcherClient) DeleteDocument(ctx context.Context, folder, id str return nil } - -func (dsc *DocSearcherClient) CreateIndex(ctx context.Context, folder string) error { - form := &CreateIndexForm{ - folder, - folder, - "./", - } - - data, err := json.Marshal(form) - if err != nil { - return fmt.Errorf("failed while marshaling form: %w", err) - } - - buildURL := strings.Builder{} - buildURL.WriteString(dsc.config.Address) - buildURL.WriteString(fmt.Sprintf("%s/storage/%s", ApiVersionPrefix, folder)) - targetURL := buildURL.String() - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("failed while creating new request: %w", err) - } - - req.Header.Set("Content-Type", DocumentJsonMime) - client := &http.Client{Timeout: time.Duration(100) * time.Second} - response, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed while sending request: %w", err) - } - - if response.StatusCode/100 > 2 { - return fmt.Errorf("bad response error status: %s", response.Status) - } - - return nil -} - -func (dsc *DocSearcherClient) DeleteIndex(ctx context.Context, folder string) error { - buildURL := strings.Builder{} - buildURL.WriteString(dsc.config.Address) - buildURL.WriteString(fmt.Sprintf("%s/storage/%s", ApiVersionPrefix, folder)) - targetURL := buildURL.String() - - req, err := http.NewRequestWithContext(ctx, http.MethodDelete, targetURL, bytes.NewReader([]byte{})) - if err != nil { - return fmt.Errorf("failed while creating new request: %w", err) - } - - client := &http.Client{Timeout: time.Duration(100) * time.Second} - response, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed while sending request: %w", err) - } - - if response.StatusCode/100 > 2 { - return fmt.Errorf("bad response error status %s", response.Status) - } - - return nil -} diff --git a/internal/infrastructure/httpserver/httpserver.go b/internal/infrastructure/httpserver/httpserver.go index f18b4ac..d00a775 100644 --- a/internal/infrastructure/httpserver/httpserver.go +++ b/internal/infrastructure/httpserver/httpserver.go @@ -8,6 +8,7 @@ import ( "github.com/labstack/echo-contrib/echoprometheus" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" "go.opentelemetry.io/otel/trace" "watchtower/internal/application/usecase" @@ -16,9 +17,9 @@ import ( ) type Server struct { + config *Config server *echo.Echo tracer trace.Tracer - config *Config uc *usecase.UseCase } @@ -49,29 +50,14 @@ func (s *Server) setupServer() { slog.Error("failed to initialize tracer", slog.String("err", err.Error())) } else { s.tracer = tp - defer func() { - //if err := s.tracer.Cleanup(context.Background()); err != nil { - // slog.Error("error shutting down tracer provider", slog.String("err", err.Error())) - //} - //s.tracer.Shutdown(context.Background()) - }() - - //traceFilterMW := otelecho.Middleware( - // AppName, - // otelecho.WithTracerProvider(otel.GetTracerProvider()), - // otelecho.WithPropagators(otel.GetTextMapPropagator()), - // otelecho.WithSkipper(func(c echo.Context) bool { - // return shouldSkipTrace(c.Path()) - // }), - //) - // - //s.server.Use(tracingFilter()) + s.server.Use(otelecho.Middleware( + AppName, + otelecho.WithPropagators(propagator), + otelecho.WithSkipper(TracerSkipper), + )) } } - _, span := s.tracer.Start(context.Background(), "init-watchtower") - span.End() - s.server.Use(middleware.CORS()) s.server.Use(middleware.Recover()) diff --git a/internal/infrastructure/httpserver/routes_bucket.go b/internal/infrastructure/httpserver/routes_bucket.go index 8525127..f2210dc 100644 --- a/internal/infrastructure/httpserver/routes_bucket.go +++ b/internal/infrastructure/httpserver/routes_bucket.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/labstack/echo/v4" + "go.opentelemetry.io/otel/codes" ) func (s *Server) CreateStorageBucketsGroup() error { @@ -28,13 +29,13 @@ func (s *Server) CreateStorageBucketsGroup() error { // @Router /cloud/buckets [get] func (s *Server) GetBuckets(eCtx echo.Context) error { ctx := eCtx.Request().Context() - - _, span := GetTracer().Start(ctx, "get-buckets") + ctx, span := s.tracer.Start(ctx, "get-buckets") defer span.End() watcherDirs, err := s.uc.GetObjectStorage().GetBuckets(ctx) if err != nil { span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } @@ -54,16 +55,23 @@ func (s *Server) GetBuckets(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/bucket [put] func (s *Server) CreateBucket(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "create-bucket") + defer span.End() + jsonForm := &CreateBucketForm{} decoder := json.NewDecoder(eCtx.Request().Body) err := decoder.Decode(jsonForm) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - ctx := eCtx.Request().Context() err = s.uc.GetObjectStorage().CreateBucket(ctx, jsonForm.BucketName) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -82,10 +90,15 @@ func (s *Server) CreateBucket(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket} [delete] func (s *Server) RemoveBucket(eCtx echo.Context) error { - bucket := eCtx.Param("bucket") ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "remove-bucket") + defer span.End() + + bucket := eCtx.Param("bucket") err := s.uc.GetObjectStorage().RemoveBucket(ctx, bucket) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } diff --git a/internal/infrastructure/httpserver/routes_object.go b/internal/infrastructure/httpserver/routes_object.go index e2f4505..6e87f40 100644 --- a/internal/infrastructure/httpserver/routes_object.go +++ b/internal/infrastructure/httpserver/routes_object.go @@ -4,11 +4,12 @@ import ( "bytes" "encoding/json" "fmt" - "log" + "log/slog" "net/http" "time" "github.com/labstack/echo/v4" + "go.opentelemetry.io/otel/codes" "watchtower/internal/application/dto" ) @@ -43,18 +44,25 @@ func (s *Server) CreateStorageObjectsGroup() error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file/copy [post] func (s *Server) CopyFile(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "copy-file") + defer span.End() + bucket := eCtx.Param("bucket") jsonForm := &CopyFileForm{} decoder := json.NewDecoder(eCtx.Request().Body) err := decoder.Decode(jsonForm) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - ctx := eCtx.Request().Context() err = s.uc.GetObjectStorage().CopyFile(ctx, bucket, jsonForm.SrcPath, jsonForm.DstPath) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -75,18 +83,25 @@ func (s *Server) CopyFile(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file/move [post] func (s *Server) MoveFile(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "move-file") + defer span.End() + bucket := eCtx.Param("bucket") jsonForm := &CopyFileForm{} decoder := json.NewDecoder(eCtx.Request().Body) err := decoder.Decode(jsonForm) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - ctx := eCtx.Request().Context() err = s.uc.GetObjectStorage().CopyFile(ctx, bucket, jsonForm.SrcPath, jsonForm.DstPath) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -108,42 +123,58 @@ func (s *Server) MoveFile(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file/upload [put] func (s *Server) UploadFile(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "upload-file") + defer span.End() + var fileData bytes.Buffer multipartForm, err := eCtx.MultipartForm() if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } bucket := eCtx.Param("bucket") - if exist, err := s.uc.GetObjectStorage().IsBucketExist(eCtx.Request().Context(), bucket); err != nil || !exist { + exist, err := s.uc.GetObjectStorage().IsBucketExist(eCtx.Request().Context(), bucket) + if err != nil || !exist { retErr := fmt.Errorf("specified bucket %s does not exist", bucket) + span.RecordError(retErr) + span.SetStatus(codes.Error, retErr.Error()) return echo.NewHTTPError(http.StatusBadRequest, retErr.Error()) } if multipartForm.File["files"] == nil { err = fmt.Errorf("there are no files into multipart form") + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } expired := eCtx.QueryParam("expired") timeVal, timeParseErr := time.Parse(time.RFC3339, expired) if timeParseErr != nil { - log.Println("failed to parse expired time param: ", expired, timeParseErr) + slog.Warn("failed to parse expired time param", + slog.String("expired", expired), + slog.String("err", timeParseErr.Error()), + ) } uploadedFiles := make([]*dto.TaskEvent, len(multipartForm.File["files"])) - ctx := eCtx.Request().Context() for index, fileForm := range multipartForm.File["files"] { fileName := fileForm.Filename fileHandler, err := fileForm.Open() if err != nil { - log.Println("failed to open file form", err) + slog.Error("failed to open file form", slog.String("err", err.Error())) continue } defer func() { if err := fileHandler.Close(); err != nil { - log.Println("failed to close file handler: ", fileName, err) + slog.Error("failed to close file handler", + slog.String("file", fileName), + slog.String("err", err.Error()), + ) return } }() @@ -151,7 +182,10 @@ func (s *Server) UploadFile(eCtx echo.Context) error { fileData.Reset() _, err = fileData.ReadFrom(fileHandler) if err != nil { - log.Println("failed to read file form", fileName, err) + slog.Error("failed to read file form", + slog.String("file", fileName), + slog.String("err", err.Error()), + ) continue } @@ -164,7 +198,10 @@ func (s *Server) UploadFile(eCtx echo.Context) error { task, err := s.uc.StoreFileToStorage(ctx, uploadItem) if err != nil { - log.Println("failed to upload file to cloud: ", fileName, err) + slog.Error("failed to upload file to cloud", + slog.String("file", fileName), + slog.String("err", err.Error()), + ) continue } @@ -188,17 +225,24 @@ func (s *Server) UploadFile(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file/download [post] func (s *Server) DownloadFile(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "download-file") + defer span.End() + bucket := eCtx.Param("bucket") jsonForm := &DownloadFileForm{} decoder := json.NewDecoder(eCtx.Request().Body) if err := decoder.Decode(jsonForm); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - ctx := eCtx.Request().Context() fileData, err := s.uc.GetObjectStorage().DownloadFile(ctx, bucket, jsonForm.FileName) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } defer fileData.Reset() @@ -219,16 +263,23 @@ func (s *Server) DownloadFile(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file/remove [delete] func (s *Server) RemoveFile(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "remove-file") + defer span.End() + bucket := eCtx.Param("bucket") jsonForm := &RemoveFileForm{} decoder := json.NewDecoder(eCtx.Request().Body) if err := decoder.Decode(jsonForm); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - ctx := eCtx.Request().Context() if err := s.uc.GetObjectStorage().DeleteFile(ctx, bucket, jsonForm.FileName); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -248,10 +299,15 @@ func (s *Server) RemoveFile(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file [delete] func (s *Server) RemoveFile2(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "remove-file-2") + defer span.End() + bucket := eCtx.Param("bucket") fileName := eCtx.QueryParam("file_name") - ctx := eCtx.Request().Context() if err := s.uc.GetObjectStorage().DeleteFile(ctx, bucket, fileName); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -272,18 +328,25 @@ func (s *Server) RemoveFile2(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/files [post] func (s *Server) GetFiles(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "get-files") + defer span.End() + bucket := eCtx.Param("bucket") jsonForm := &GetFilesForm{} decoder := json.NewDecoder(eCtx.Request().Body) err := decoder.Decode(jsonForm) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - ctx := eCtx.Request().Context() listObjects, err := s.uc.GetObjectStorage().GetBucketFiles(ctx, bucket, jsonForm.DirectoryName) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -304,18 +367,25 @@ func (s *Server) GetFiles(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file/attributes [post] func (s *Server) GetFileAttributes(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "get-file-attributes") + defer span.End() + bucket := eCtx.Param("bucket") jsonForm := &GetFileAttributesForm{} decoder := json.NewDecoder(eCtx.Request().Body) err := decoder.Decode(jsonForm) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } - ctx := eCtx.Request().Context() listObjects, err := s.uc.GetObjectStorage().GetFileMetadata(ctx, bucket, jsonForm.FilePath) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -336,21 +406,28 @@ func (s *Server) GetFileAttributes(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /cloud/{bucket}/file/share [post] func (s *Server) ShareFile(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "share-file") + defer span.End() + bucket := eCtx.Param("bucket") form := &ShareFileForm{} decoder := json.NewDecoder(eCtx.Request().Body) err := decoder.Decode(form) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } expired := time.Second * time.Duration(form.ExpiredSecs) - ctx := eCtx.Request().Context() storage := s.uc.GetObjectStorage() url, err := storage.GenSharedURL(ctx, expired, bucket, form.FilePath) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } diff --git a/internal/infrastructure/httpserver/routes_task.go b/internal/infrastructure/httpserver/routes_task.go index 4b4d8e3..92cd4a2 100644 --- a/internal/infrastructure/httpserver/routes_task.go +++ b/internal/infrastructure/httpserver/routes_task.go @@ -1,10 +1,12 @@ package httpserver import ( - "golang.org/x/exp/slices" + "fmt" "net/http" "github.com/labstack/echo/v4" + "go.opentelemetry.io/otel/codes" + "golang.org/x/exp/slices" "watchtower/internal/application/dto" "watchtower/internal/application/mapping" ) @@ -32,18 +34,22 @@ func (s *Server) CreateTasksGroup() error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /tasks/{bucket} [get] func (s *Server) LoadTasks(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "load-tasks") + defer span.End() + bucket := eCtx.Param("bucket") if bucket == "" { + err := fmt.Errorf("bucket parameter is required") + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, "bucket is required") } - ctx := eCtx.Request().Context() - tCtx, span := GetTracer().Start(ctx, "load-buckets") - defer span.End() - - tasks, err := s.uc.GetTaskManager().GetAll(tCtx, bucket) + tasks, err := s.uc.GetTaskManager().GetAll(ctx, bucket) if err != nil { span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -55,6 +61,7 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { inputTaskStatus, err := mapping.TaskStatusFromString(status) if err != nil { span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusUnprocessableEntity, "unknown status") } @@ -79,19 +86,30 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { // @Failure 503 {object} ServerErrorForm "Server does not available" // @Router /tasks/{bucket}/{task_id} [get] func (s *Server) LoadTaskByID(eCtx echo.Context) error { + ctx := eCtx.Request().Context() + ctx, span := s.tracer.Start(ctx, "load-task-by-id") + defer span.End() + bucket := eCtx.Param("bucket") if bucket == "" { + err := fmt.Errorf("bucket parameter is required") + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, "bucket is required") } taskID := eCtx.Param("task_id") if taskID == "" { + err := fmt.Errorf("task-id parameter is required") + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, "task_id is required") } - ctx := eCtx.Request().Context() task, err := s.uc.GetTaskManager().Get(ctx, bucket, taskID) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } diff --git a/internal/infrastructure/httpserver/tracer.go b/internal/infrastructure/httpserver/tracer.go index 10eeb41..7a95eda 100644 --- a/internal/infrastructure/httpserver/tracer.go +++ b/internal/infrastructure/httpserver/tracer.go @@ -2,27 +2,37 @@ package httpserver import ( "context" + "fmt" + "strings" + + "github.com/labstack/echo/v4" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/trace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" - "go.opentelemetry.io/otel/trace" ) -var tracer trace.Tracer +var ( + excludedPaths = []string{ + "/health", + "/metrics", + "/favicon.ico", + "/static/", + } + propagator = propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + ) +) + +var GlobalTracer trace.Tracer func InitTracer(config *Config) (trace.Tracer, error) { tracerConfig := config.Tracer - - res := resource.NewWithAttributes( - semconv.SchemaURL, - semconv.TelemetrySDKLanguageGo, - semconv.ServiceNameKey.String(AppName), - ) - client := otlptracegrpc.NewClient( otlptracegrpc.WithEndpoint(tracerConfig.Address), otlptracegrpc.WithInsecure(), @@ -31,58 +41,41 @@ func InitTracer(config *Config) (trace.Tracer, error) { ctx := context.Background() traceExporter, err := otlptrace.New(ctx, client) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to connect to otlp trace server: %w", err) } - bsp := sdktrace.NewBatchSpanProcessor(traceExporter) + res, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.TelemetrySDKLanguageGo, + semconv.ServiceNameKey.String(AppName), + ), + ) sampler := sdktrace.AlwaysSample() + bsp := sdktrace.NewBatchSpanProcessor(traceExporter) tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sampler), sdktrace.WithResource(res), sdktrace.WithSpanProcessor(bsp), ) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( - propagation.TraceContext{}, - propagation.Baggage{}, - )) - + otel.SetTextMapPropagator(propagator) + GlobalTracer = tp.Tracer(AppName) otel.SetTracerProvider(tp) + return GlobalTracer, nil +} - tracer = tp.Tracer(AppName) +func TracerSkipper(c echo.Context) bool { + for _, excluded := range excludedPaths { + if strings.HasPrefix(c.Path(), excluded) { + return true + } + } - return tracer, nil -} + if c.Request().Method == "OPTIONS" { + return true + } -// GetTracer gets global Tracer -// func GetTracer() trace.Tracer { -func GetTracer() trace.Tracer { - return tracer + return false } - -//func tracingFilter() echo.MiddlewareFunc { -// return otelecho.Middleware(AppName, -// otelecho.WithTracerProvider(otel.GetTracerProvider()), -// otelecho.WithPropagators(otel.GetTextMapPropagator()), -// otelecho.WithSkipper(func(c echo.Context) bool { -// return shouldSkipTrace(c.Path()) -// }), -// ) -//} -// -//func shouldSkipTrace(path string) bool { -// // List of paths to exclude from tracing -// excludedPaths := []string{ -// "/health", -// "/metrics", -// "/favicon.ico", -// "/static/", -// } -// -// for _, excluded := range excludedPaths { -// if strings.HasPrefix(path, excluded) { -// return true -// } -// } -// return false -//} diff --git a/internal/infrastructure/rmq/rmq.go b/internal/infrastructure/rmq/rmq.go index bd8a5ae..7bbc095 100644 --- a/internal/infrastructure/rmq/rmq.go +++ b/internal/infrastructure/rmq/rmq.go @@ -5,8 +5,16 @@ import ( "encoding/json" "fmt" "log" + "log/slog" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + "watchtower/internal/infrastructure/httpserver" + amqp "github.com/rabbitmq/amqp091-go" "watchtower/internal/application/dto" ) @@ -53,9 +61,15 @@ func (r *RmqClient) GetConsumerChannel() chan dto.Message { return r.redirect } -func (r *RmqClient) Publish(_ context.Context, msg dto.Message) error { +func (r *RmqClient) Publish(ctx context.Context, msg dto.Message) error { + ctx, span := httpserver.GlobalTracer.Start(ctx, "rmq-publish") + defer span.End() + + headers := injectSpanContextToHeaders(ctx) body, err := json.Marshal(msg) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return fmt.Errorf("failed while marshalling rmq body: %w", err) } @@ -66,14 +80,26 @@ func (r *RmqClient) Publish(_ context.Context, msg dto.Message) error { false, amqp.Publishing{ ContentType: "application/json", + Headers: headers, Body: body, + Timestamp: time.Now(), }, ) + span.SetAttributes( + attribute.Int("message.size", len(body)), + attribute.String("messaging.system", "rabbitmq"), + attribute.String("messaging.destination", r.config.Exchange), + attribute.String("messaging.rabbitmq.routing_key", r.config.RoutingKey), + ) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return fmt.Errorf("failed while publishing rmq message: %w", err) } + span.SetStatus(codes.Ok, "success") return nil } @@ -121,8 +147,23 @@ func (r *RmqClient) handle(deliveries <-chan amqp.Delivery, done chan error) { defer cleanup() for delMsg := range deliveries { + ctx := extractSpanContextFromHeaders(delMsg.Headers) + span := trace.SpanFromContext(ctx) + defer span.End() + msg := &dto.Message{} - _ = json.Unmarshal(delMsg.Body, msg) + err := json.Unmarshal(delMsg.Body, msg) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + slog.Error("failed while read rmq message", slog.String("err", err.Error())) + continue + } + + span.SetName("rmq-consume") + span.SetAttributes(attribute.String("task-id", msg.EventId.String())) + + msg.Ctx = ctx r.redirect <- *msg } } @@ -191,3 +232,39 @@ func (r *RmqClient) CreateQueue(exchange, queue, routingKey string) error { return nil } + +func injectSpanContextToHeaders(ctx context.Context) amqp.Table { + carrier := propagation.HeaderCarrier{} + propagator := otel.GetTextMapPropagator() + propagator.Inject(ctx, carrier) + + span := trace.SpanFromContext(ctx) + sCtx := span.SpanContext() + + headers := amqp.Table{} + headers["trace-id"] = sCtx.TraceID().String() + headers["span-id"] = sCtx.SpanID().String() + headers["trace-flags"] = sCtx.TraceFlags().String() + headers["trace-state"] = sCtx.TraceState().String() + + return headers +} + +func extractSpanContextFromHeaders(headers amqp.Table) context.Context { + ctx := context.Background() + if headers == nil { + return ctx + } + + traceID, _ := trace.TraceIDFromHex(headers["trace-id"].(string)) + spanID, _ := trace.SpanIDFromHex(headers["span-id"].(string)) + sCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + TraceState: trace.TraceState{}, + Remote: true, + }) + + return trace.ContextWithSpanContext(ctx, sCtx) +} diff --git a/internal/infrastructure/s3/s3.go b/internal/infrastructure/s3/s3.go index 694da35..4b9507c 100644 --- a/internal/infrastructure/s3/s3.go +++ b/internal/infrastructure/s3/s3.go @@ -9,7 +9,9 @@ import ( "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" + "go.opentelemetry.io/otel/codes" "watchtower/internal/application/dto" + "watchtower/internal/infrastructure/httpserver" ) type S3Client struct { @@ -147,16 +149,23 @@ func (s *S3Client) MoveFile(ctx context.Context, bucket, srcPath, dstPath string } func (s *S3Client) DownloadFile(ctx context.Context, bucket, filePath string) (bytes.Buffer, error) { + ctx, span := httpserver.GlobalTracer.Start(ctx, "s3-download-file") + defer span.End() + var objBody bytes.Buffer opts := minio.GetObjectOptions{} obj, err := s.mc.GetObject(ctx, bucket, filePath, opts) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return objBody, fmt.Errorf("failed to get object from s3: %w", err) } _, err = objBody.ReadFrom(obj) if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) return objBody, fmt.Errorf("failed to read loaded object from s3: %w", err) } From 2e63a1a99fdbd074e772483587d61bb9fd3883c6 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 23 Sep 2025 11:23:40 +0300 Subject: [PATCH 05/47] chore: moved tracing to application layer --- .env | 16 +++-- cmd/watchtower/watchtower.go | 2 +- configs/config.toml | 6 +- configs/testing.toml | 6 +- .../application/services/server/public.go | 2 + internal/application/utils/sender.go | 49 ++++++++++++-- .../utils/telemetry}/config.go | 8 +-- .../application/utils/telemetry/logger.go | 38 +++++++++++ .../utils/telemetry}/tracer.go | 39 +++-------- internal/infrastructure/config/config.go | 26 +++++--- internal/infrastructure/dedoc/dedoc.go | 4 +- .../infrastructure/doc-storage/doc-storage.go | 4 +- .../infrastructure/httpserver/httpserver.go | 66 +++++++++++-------- .../httpserver/{ => mw}/logger.go | 41 ++---------- .../infrastructure/httpserver/mw/tracer.go | 30 +++++++++ internal/infrastructure/rmq/rmq.go | 6 +- internal/infrastructure/s3/s3.go | 4 +- 17 files changed, 211 insertions(+), 136 deletions(-) rename internal/{infrastructure/httpserver => application/utils/telemetry}/config.go (61%) create mode 100644 internal/application/utils/telemetry/logger.go rename internal/{infrastructure/httpserver => application/utils/telemetry}/tracer.go (65%) rename internal/infrastructure/httpserver/{ => mw}/logger.go (61%) create mode 100644 internal/infrastructure/httpserver/mw/tracer.go diff --git a/.env b/.env index 8d04e40..a5f2f17 100644 --- a/.env +++ b/.env @@ -2,16 +2,18 @@ WATCHTOWER__SETTINGS__CHUNK_SIZE=10000 WATCHTOWER__SETTINGS__CHUNK_OVERlAP=1000 WATCHTOWER__SERVER__HTTP__ADDRESS=0.0.0.0:2893 -WATCHTOWER__SERVER__HTTP__LOGGER__LEVEL=DEBUG -WATCHTOWER__SERVER__HTTP__LOGGER__ADDRESS=localhost:3100 -WATCHTOWER__SERVER__HTTP__LOGGER__ENABLE_LOKI=false -WATCHTOWER__SERVER__HTTP__TRACER__ADDRESS=localhost:4317 -WATCHTOWER__SERVER__HTTP__TRACER__ENABLE_JAEGER=true -WATCHTOWER__OCR__DEDOC__ADDRESS=localhost:8004 +WATCHTOWER__SERVER__LOGGER__LEVEL=DEBUG +WATCHTOWER__SERVER__LOGGER__ADDRESS=localhost:3100 +WATCHTOWER__SERVER__LOGGER__ENABLE_LOKI=false + +WATCHTOWER__SERVER__TRACER__ADDRESS=localhost:4317 +WATCHTOWER__SERVER__TRACER__ENABLE_JAEGER=true + +WATCHTOWER__OCR__DEDOC__ADDRESS=http://localhost:8004 WATCHTOWER__OCR__DEDOC__TIMEOUT=100s -WATCHTOWER__DOCSTORAGE__DOC_SEARCHER__ADDRESS=http://localhost:2892 +WATCHTOWER__STORAGE__DOC_SEARCHER__ADDRESS=http://localhost:2892 WATCHTOWER__CACHER__REDIS__ADDRESS=localhost:6379 WATCHTOWER__CACHER__REDIS__USERNAME=redis diff --git a/cmd/watchtower/watchtower.go b/cmd/watchtower/watchtower.go index 906bab1..fa96906 100644 --- a/cmd/watchtower/watchtower.go +++ b/cmd/watchtower/watchtower.go @@ -78,7 +78,7 @@ func main() { ) useCase.LaunchWatcherListener(cCtx) - httpServer := httpserver.New(&servConfig.Server.Http, useCase) + httpServer := httpserver.New(&servConfig.Server, useCase) go func() { if err := httpServer.Start(cCtx); err != nil { log.Fatalf("http server start failed: %v", err) diff --git a/configs/config.toml b/configs/config.toml index d223953..2af0eae 100644 --- a/configs/config.toml +++ b/configs/config.toml @@ -5,12 +5,12 @@ chunk_overlap = 1000 [server.http] address = "0.0.0.0:2893" -[server.http.logger] +[server.logger] level = "info" address = "loki:3100" enable_loki = true -[server.http.tracer] +[server.tracer] address = "jaeger:4317" enable_jaeger = false @@ -18,7 +18,7 @@ enable_jaeger = false address = "http://localhost:8004" timeout = 300 -[docstorage.docsearcher] +[storage.docsearcher] address = "http://localhost:2892" [cacher.redis] diff --git a/configs/testing.toml b/configs/testing.toml index 95c5a94..6c1b4f8 100644 --- a/configs/testing.toml +++ b/configs/testing.toml @@ -5,12 +5,12 @@ chunk_overlap = 1000 [server.http] address = "0.0.0.0:2893" -[server.http.logger] +[server.logger] level = "info" enable_loki = false address = "localhost:3100" -[server.http.tracer] +[server.tracer] address = "localhost:4317" enable_jaeger = false @@ -18,7 +18,7 @@ enable_jaeger = false address = "http://localhost:8004" timeout = 300 -[docstorage.docsearcher] +[storage.docsearcher] address = "http://localhost:2892" [cacher.redis] diff --git a/internal/application/services/server/public.go b/internal/application/services/server/public.go index 26d9c9e..505ab09 100644 --- a/internal/application/services/server/public.go +++ b/internal/application/services/server/public.go @@ -2,6 +2,8 @@ package server import "context" +const AppName = "watchtower" + type IServer interface { Start(ctx context.Context) error Shutdown(ctx context.Context) error diff --git a/internal/application/utils/sender.go b/internal/application/utils/sender.go index 97b0704..719b1e4 100644 --- a/internal/application/utils/sender.go +++ b/internal/application/utils/sender.go @@ -9,6 +9,10 @@ import ( "time" "github.com/labstack/echo/v4" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "watchtower/internal/application/utils/telemetry" ) func PUT(ctx context.Context, body *bytes.Buffer, url, mime string, timeout time.Duration) ([]byte, error) { @@ -19,7 +23,7 @@ func PUT(ctx context.Context, body *bytes.Buffer, url, mime string, timeout time req.Header.Set(echo.HeaderContentType, mime) client := &http.Client{Timeout: timeout} - return SendRequest(client, req) + return SendRequest(ctx, client, req) } func POST(ctx context.Context, body *bytes.Buffer, url, mime string, timeout time.Duration) ([]byte, error) { @@ -30,28 +34,61 @@ func POST(ctx context.Context, body *bytes.Buffer, url, mime string, timeout tim req.Header.Set(echo.HeaderContentType, mime) client := &http.Client{Timeout: timeout} - return SendRequest(client, req) + return SendRequest(ctx, client, req) } -func SendRequest(client *http.Client, req *http.Request) ([]byte, error) { +func SendRequest(ctx context.Context, client *http.Client, req *http.Request) ([]byte, error) { + ctx, span := telemetry.GlobalTracer.Start(ctx, "http-request") + defer span.End() + + span.SetAttributes( + attribute.String("request.method", req.Method), + attribute.String("request.uri", req.RequestURI), + ) + + injectTracingToHeader(ctx, req) response, err := client.Do(req) if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) + err = fmt.Errorf("failed to send request: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return nil, err } defer func() { _ = response.Body.Close() }() respData, err := io.ReadAll(response.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + err = fmt.Errorf("failed to read response body: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return nil, err } if response.StatusCode/100 > 2 { - return nil, fmt.Errorf("non success response %s: %s", response.Status, string(respData)) + err = fmt.Errorf("non success response %s: %s", response.Status, string(respData)) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return nil, err } return respData, nil } +func injectTracingToHeader(ctx context.Context, req *http.Request) { + span := trace.SpanFromContext(ctx) + sCtx := span.SpanContext() + + headers := req.Header + headers.Add("trace-id", sCtx.TraceID().String()) + headers.Add("span-id", sCtx.SpanID().String()) + headers.Add("trace-flags", sCtx.TraceFlags().String()) + headers.Add("trace-state", sCtx.TraceState().String()) +} + +func extractTracingFromHeader(ctx context.Context, resp *http.Response) { + +} + func BuildTargetURL(host, path string) string { targetURL := fmt.Sprintf("%s%s", host, path) return targetURL diff --git a/internal/infrastructure/httpserver/config.go b/internal/application/utils/telemetry/config.go similarity index 61% rename from internal/infrastructure/httpserver/config.go rename to internal/application/utils/telemetry/config.go index 9f22038..fc814c2 100644 --- a/internal/infrastructure/httpserver/config.go +++ b/internal/application/utils/telemetry/config.go @@ -1,10 +1,4 @@ -package httpserver - -type Config struct { - Address string `mapstructure:"address"` - Logger LoggerConfig `mapstructure:"logger"` - Tracer TracerConfig `mapstructure:"tracer"` -} +package telemetry type LoggerConfig struct { Level string `mapstructure:"level"` diff --git a/internal/application/utils/telemetry/logger.go b/internal/application/utils/telemetry/logger.go new file mode 100644 index 0000000..151e2e5 --- /dev/null +++ b/internal/application/utils/telemetry/logger.go @@ -0,0 +1,38 @@ +package telemetry + +import ( + "fmt" + "log/slog" + "time" + + slogloki "github.com/samber/slog-loki/v2" +) + +var ( + filterURI = []string{ + "/metrics", + "/swagger/*", + } +) + +type SlogLokiLogger struct { + Client *slog.Logger + FilterURI []string +} + +func InitLokiLogger(config LoggerConfig) SlogLokiLogger { + lokiConfig := slogloki.Option{ + Endpoint: fmt.Sprintf("%s/api/prom/push", config.Address), + Level: slog.LevelInfo, + BatchWait: time.Second * 5, + BatchEntriesNumber: 10, + } + + logger := slog.New(lokiConfig.NewLokiHandler()). + With("service_name", AppName). + With("service", AppName). + With("detected_level", config.Level). + With("level", config.Level) + + return SlogLokiLogger{Client: logger, FilterURI: filterURI} +} diff --git a/internal/infrastructure/httpserver/tracer.go b/internal/application/utils/telemetry/tracer.go similarity index 65% rename from internal/infrastructure/httpserver/tracer.go rename to internal/application/utils/telemetry/tracer.go index 7a95eda..1492f98 100644 --- a/internal/infrastructure/httpserver/tracer.go +++ b/internal/application/utils/telemetry/tracer.go @@ -1,40 +1,33 @@ -package httpserver +package telemetry import ( "context" "fmt" - "strings" - "github.com/labstack/echo/v4" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/trace" + "watchtower/internal/application/services/server" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) +const AppName = server.AppName + var ( - excludedPaths = []string{ - "/health", - "/metrics", - "/favicon.ico", - "/static/", - } - propagator = propagation.NewCompositeTextMapPropagator( + GlobalTracer trace.Tracer + TracePropagator = propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, ) ) -var GlobalTracer trace.Tracer - -func InitTracer(config *Config) (trace.Tracer, error) { - tracerConfig := config.Tracer +func InitTracer(config TracerConfig) (trace.Tracer, error) { client := otlptracegrpc.NewClient( - otlptracegrpc.WithEndpoint(tracerConfig.Address), + otlptracegrpc.WithEndpoint(config.Address), otlptracegrpc.WithInsecure(), ) @@ -60,22 +53,8 @@ func InitTracer(config *Config) (trace.Tracer, error) { sdktrace.WithSpanProcessor(bsp), ) - otel.SetTextMapPropagator(propagator) + otel.SetTextMapPropagator(TracePropagator) GlobalTracer = tp.Tracer(AppName) otel.SetTracerProvider(tp) return GlobalTracer, nil } - -func TracerSkipper(c echo.Context) bool { - for _, excluded := range excludedPaths { - if strings.HasPrefix(c.Path(), excluded) { - return true - } - } - - if c.Request().Method == "OPTIONS" { - return true - } - - return false -} diff --git a/internal/infrastructure/config/config.go b/internal/infrastructure/config/config.go index b484c9f..52fe658 100644 --- a/internal/infrastructure/config/config.go +++ b/internal/infrastructure/config/config.go @@ -6,9 +6,9 @@ import ( "github.com/joho/godotenv" "github.com/spf13/viper" + "watchtower/internal/application/utils/telemetry" "watchtower/internal/infrastructure/dedoc" "watchtower/internal/infrastructure/doc-storage" - "watchtower/internal/infrastructure/httpserver" "watchtower/internal/infrastructure/redis" "watchtower/internal/infrastructure/rmq" "watchtower/internal/infrastructure/s3" @@ -17,7 +17,7 @@ import ( type Config struct { Server ServerConfig `mapstructure:"server"` Ocr OcrConfig `mapstructure:"ocr"` - DocStorage DocStorageConfig `mapstructure:"docstorage"` + DocStorage DocStorageConfig `mapstructure:"storage"` Cacher CacherConfig `mapstructure:"cacher"` Queue QueueConfig `mapstructure:"queue"` Cloud CloudConfig `mapstructure:"cloud"` @@ -30,7 +30,13 @@ type SettingsConfig struct { } type ServerConfig struct { - Http httpserver.Config `mapstructure:"http"` + Http HttpServerConfig `mapstructure:"http"` + Logger telemetry.LoggerConfig `mapstructure:"logger"` + Tracer telemetry.TracerConfig `mapstructure:"tracer"` +} + +type HttpServerConfig struct { + Address string `mapstructure:"address"` } type OcrConfig struct { @@ -81,23 +87,25 @@ func FromFile(filePath string) (*Config, error) { if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } - bindErr = viperInstance.BindEnv("server.http.logger.level", "WATCHTOWER__SERVER__HTTP__LOGGER__LEVEL") + + bindErr = viperInstance.BindEnv("server.logger.level", "WATCHTOWER__SERVER__LOGGER__LEVEL") if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } - bindErr = viperInstance.BindEnv("server.http.logger.address", "WATCHTOWER__SERVER__HTTP__LOGGER__ADDRESS") + bindErr = viperInstance.BindEnv("server.logger.address", "WATCHTOWER__SERVER__LOGGER__ADDRESS") if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } - bindErr = viperInstance.BindEnv("server.http.logger.enable_loki", "WATCHTOWER__SERVER__HTTP__LOGGER__ENABLE_LOKI") + bindErr = viperInstance.BindEnv("server.logger.enable_loki", "WATCHTOWER__SERVER__LOGGER__ENABLE_LOKI") + if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } - bindErr = viperInstance.BindEnv("server.http.tracer.address", "WATCHTOWER__SERVER__HTTP__TRACER__ADDRESS") + bindErr = viperInstance.BindEnv("server.tracer.address", "WATCHTOWER__SERVER__TRACER__ADDRESS") if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } - bindErr = viperInstance.BindEnv("server.http.tracer.enable_jaeger", "WATCHTOWER__SERVER__HTTP__TRACER__ENABLE_JAEGER") + bindErr = viperInstance.BindEnv("server.tracer.enable_jaeger", "WATCHTOWER__SERVER__TRACER__ENABLE_JAEGER") if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } @@ -113,7 +121,7 @@ func FromFile(filePath string) (*Config, error) { } // Storage doc-searcher config - bindErr = viperInstance.BindEnv("docstorage.docsearcher.address", "WATCHTOWER__DOCSTORAGE__DOC_SEARCHER__ADDRESS") + bindErr = viperInstance.BindEnv("storage.docsearcher.address", "WATCHTOWER__STORAGE__DOC_SEARCHER__ADDRESS") if bindErr != nil { return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) } diff --git a/internal/infrastructure/dedoc/dedoc.go b/internal/infrastructure/dedoc/dedoc.go index eb6962b..1f4e236 100644 --- a/internal/infrastructure/dedoc/dedoc.go +++ b/internal/infrastructure/dedoc/dedoc.go @@ -11,7 +11,7 @@ import ( "go.opentelemetry.io/otel/codes" "watchtower/internal/application/dto" "watchtower/internal/application/utils" - "watchtower/internal/infrastructure/httpserver" + "watchtower/internal/application/utils/telemetry" ) const RecognitionURL = "/ocr_extract_text" @@ -27,7 +27,7 @@ func New(config *Config) *DedocClient { } func (dc *DedocClient) Recognize(ctx context.Context, inputFile dto.InputFile) (*dto.Recognized, error) { - ctx, span := httpserver.GlobalTracer.Start(ctx, "recognize-file") + ctx, span := telemetry.GlobalTracer.Start(ctx, "recognize-file") defer span.End() var buf bytes.Buffer diff --git a/internal/infrastructure/doc-storage/doc-storage.go b/internal/infrastructure/doc-storage/doc-storage.go index 4e57a02..5b914d4 100644 --- a/internal/infrastructure/doc-storage/doc-storage.go +++ b/internal/infrastructure/doc-storage/doc-storage.go @@ -14,7 +14,7 @@ import ( "go.opentelemetry.io/otel/codes" "watchtower/internal/application/dto" "watchtower/internal/application/utils" - "watchtower/internal/infrastructure/httpserver" + "watchtower/internal/application/utils/telemetry" ) const DocumentJsonMime = "application/json" @@ -35,7 +35,7 @@ func (dsc *DocSearcherClient) StoreDocument( folder string, doc *dto.DocumentObject, ) (string, error) { - ctx, span := httpserver.GlobalTracer.Start(ctx, "store-document") + ctx, span := telemetry.GlobalTracer.Start(ctx, "store-document") defer span.End() storeDoc := StoreDocumentForm{ diff --git a/internal/infrastructure/httpserver/httpserver.go b/internal/infrastructure/httpserver/httpserver.go index d00a775..69464b6 100644 --- a/internal/infrastructure/httpserver/httpserver.go +++ b/internal/infrastructure/httpserver/httpserver.go @@ -10,21 +10,25 @@ import ( "github.com/labstack/echo/v4/middleware" "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" "go.opentelemetry.io/otel/trace" + "watchtower/internal/application/services/server" "watchtower/internal/application/usecase" + "watchtower/internal/application/utils/telemetry" + "watchtower/internal/infrastructure/config" + "watchtower/internal/infrastructure/httpserver/mw" echoSwagger "github.com/swaggo/echo-swagger" _ "watchtower/docs" ) type Server struct { - config *Config + config *config.ServerConfig server *echo.Echo tracer trace.Tracer uc *usecase.UseCase } -func New(config *Config, watcherUC *usecase.UseCase) *Server { +func New(config *config.ServerConfig, watcherUC *usecase.UseCase) *Server { return &Server{ config: config, uc: watcherUC, @@ -34,29 +38,9 @@ func New(config *Config, watcherUC *usecase.UseCase) *Server { func (s *Server) setupServer() { s.server = echo.New() - s.server.Use(echoprometheus.NewMiddleware(AppName)) - s.server.GET("/metrics", echoprometheus.NewHandler()) - - if s.config.Logger.EnableLoki { - lokiLog := InitLokiLogger(s.config.Logger) - s.server.Use(lokiLog.LokiLoggerMW()) - } else { - s.server.Use(InitLocalLogger(s.config.Logger)) - } - - if s.config.Tracer.EnableJaeger { - tp, err := InitTracer(s.config) - if err != nil { - slog.Error("failed to initialize tracer", slog.String("err", err.Error())) - } else { - s.tracer = tp - s.server.Use(otelecho.Middleware( - AppName, - otelecho.WithPropagators(propagator), - otelecho.WithSkipper(TracerSkipper), - )) - } - } + s.initMeterMW() + s.initLoggerMW() + s.initTracerMW() s.server.Use(middleware.CORS()) s.server.Use(middleware.Recover()) @@ -70,7 +54,7 @@ func (s *Server) setupServer() { func (s *Server) Start(_ context.Context) error { s.setupServer() - if err := s.server.Start(s.config.Address); err != nil { + if err := s.server.Start(s.config.Http.Address); err != nil { return fmt.Errorf("failed to start server: %w", err) } @@ -80,3 +64,33 @@ func (s *Server) Start(_ context.Context) error { func (s *Server) Shutdown(ctx context.Context) error { return s.server.Shutdown(ctx) } + +func (s *Server) initMeterMW() { + s.server.Use(echoprometheus.NewMiddleware(server.AppName)) + s.server.GET("/metrics", echoprometheus.NewHandler()) +} + +func (s *Server) initLoggerMW() { + if s.config.Logger.EnableLoki { + lokiLog := telemetry.InitLokiLogger(s.config.Logger) + s.server.Use(mw.CreateLokiLoggerMW(&lokiLog)) + } else { + s.server.Use(mw.InitLocalLogger(s.config.Logger)) + } +} + +func (s *Server) initTracerMW() { + if s.config.Tracer.EnableJaeger { + tp, err := telemetry.InitTracer(s.config.Tracer) + if err != nil { + slog.Error("failed to initialize tracer", slog.String("err", err.Error())) + } else { + s.tracer = tp + s.server.Use(otelecho.Middleware( + server.AppName, + otelecho.WithPropagators(telemetry.TracePropagator), + otelecho.WithSkipper(mw.TracerSkipper), + )) + } + } +} diff --git a/internal/infrastructure/httpserver/logger.go b/internal/infrastructure/httpserver/mw/logger.go similarity index 61% rename from internal/infrastructure/httpserver/logger.go rename to internal/infrastructure/httpserver/mw/logger.go index 478434e..1197b35 100644 --- a/internal/infrastructure/httpserver/logger.go +++ b/internal/infrastructure/httpserver/mw/logger.go @@ -1,4 +1,4 @@ -package httpserver +package mw import ( "context" @@ -11,17 +11,10 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" - slogloki "github.com/samber/slog-loki/v2" + "watchtower/internal/application/utils/telemetry" ) -const AppName = "watchtower" - -type SlogLokiLogger struct { - client *slog.Logger - filterURI []string -} - -func InitLocalLogger(config LoggerConfig) echo.MiddlewareFunc { +func InitLocalLogger(config telemetry.LoggerConfig) echo.MiddlewareFunc { logConfig := middleware.LoggerConfig{ Skipper: func(c echo.Context) bool { uri := c.Path() @@ -43,32 +36,10 @@ func InitLocalLogger(config LoggerConfig) echo.MiddlewareFunc { return middleware.LoggerWithConfig(logConfig) } -func InitLokiLogger(config LoggerConfig) SlogLokiLogger { - lokiConfig := slogloki.Option{ - Endpoint: fmt.Sprintf("%s/api/prom/push", config.Address), - Level: slog.LevelInfo, - BatchWait: time.Second * 5, - BatchEntriesNumber: 10, - } - - logger := slog.New(lokiConfig.NewLokiHandler()). - With("service_name", AppName). - With("service", AppName). - With("detected_level", config.Level). - With("level", config.Level) - - filterURI := []string{ - "/metrics", - "/swagger/*", - } - - return SlogLokiLogger{client: logger, filterURI: filterURI} -} - -func (sll *SlogLokiLogger) LokiLoggerMW() echo.MiddlewareFunc { +func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(eCtx echo.Context) error { - if slices.Contains(sll.filterURI, eCtx.Path()) { + if slices.Contains(sll.FilterURI, eCtx.Path()) { return next(eCtx) } @@ -101,7 +72,7 @@ func (sll *SlogLokiLogger) LokiLoggerMW() echo.MiddlewareFunc { } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - sll.client.Log(ctx, logLevel, string(jsonMessage)) + sll.Client.Log(ctx, logLevel, string(jsonMessage)) defer cancel() return err diff --git a/internal/infrastructure/httpserver/mw/tracer.go b/internal/infrastructure/httpserver/mw/tracer.go new file mode 100644 index 0000000..7753415 --- /dev/null +++ b/internal/infrastructure/httpserver/mw/tracer.go @@ -0,0 +1,30 @@ +package mw + +import ( + "strings" + + "github.com/labstack/echo/v4" +) + +var ( + excludedPaths = []string{ + "/health", + "/metrics", + "/favicon.ico", + "/static/", + } +) + +func TracerSkipper(c echo.Context) bool { + for _, excluded := range excludedPaths { + if strings.HasPrefix(c.Path(), excluded) { + return true + } + } + + if c.Request().Method == "OPTIONS" { + return true + } + + return false +} diff --git a/internal/infrastructure/rmq/rmq.go b/internal/infrastructure/rmq/rmq.go index 7bbc095..797df80 100644 --- a/internal/infrastructure/rmq/rmq.go +++ b/internal/infrastructure/rmq/rmq.go @@ -13,10 +13,10 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" - "watchtower/internal/infrastructure/httpserver" + "watchtower/internal/application/dto" + "watchtower/internal/application/utils/telemetry" amqp "github.com/rabbitmq/amqp091-go" - "watchtower/internal/application/dto" ) const ConsumerName = "watchtower-consumer" @@ -62,7 +62,7 @@ func (r *RmqClient) GetConsumerChannel() chan dto.Message { } func (r *RmqClient) Publish(ctx context.Context, msg dto.Message) error { - ctx, span := httpserver.GlobalTracer.Start(ctx, "rmq-publish") + ctx, span := telemetry.GlobalTracer.Start(ctx, "rmq-publish") defer span.End() headers := injectSpanContextToHeaders(ctx) diff --git a/internal/infrastructure/s3/s3.go b/internal/infrastructure/s3/s3.go index 4b9507c..141f93b 100644 --- a/internal/infrastructure/s3/s3.go +++ b/internal/infrastructure/s3/s3.go @@ -11,7 +11,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" "go.opentelemetry.io/otel/codes" "watchtower/internal/application/dto" - "watchtower/internal/infrastructure/httpserver" + "watchtower/internal/application/utils/telemetry" ) type S3Client struct { @@ -149,7 +149,7 @@ func (s *S3Client) MoveFile(ctx context.Context, bucket, srcPath, dstPath string } func (s *S3Client) DownloadFile(ctx context.Context, bucket, filePath string) (bytes.Buffer, error) { - ctx, span := httpserver.GlobalTracer.Start(ctx, "s3-download-file") + ctx, span := telemetry.GlobalTracer.Start(ctx, "s3-download-file") defer span.End() var objBody bytes.Buffer From dc12679c71e58b6b850c89f99172dfafc4046f15 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 23 Sep 2025 11:30:02 +0300 Subject: [PATCH 06/47] chore: updated swagger + added /api/v1 --- cmd/watchtower/watchtower.go | 3 +++ docs/docs.go | 4 ++-- docs/swagger.json | 2 ++ docs/swagger.yaml | 2 ++ internal/infrastructure/httpserver/httpserver.go | 2 +- internal/infrastructure/httpserver/routes_bucket.go | 2 +- internal/infrastructure/httpserver/routes_object.go | 2 +- internal/infrastructure/httpserver/routes_task.go | 2 +- 8 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cmd/watchtower/watchtower.go b/cmd/watchtower/watchtower.go index fa96906..e7ca53b 100644 --- a/cmd/watchtower/watchtower.go +++ b/cmd/watchtower/watchtower.go @@ -30,6 +30,9 @@ import ( // Processing -> 2; // Successful -> 3. // +// @host localhost:2893 +// @BasePath /api/v1 +// // @tag.name buckets // @tag.description CRUD APIs to manage cloud buckets diff --git a/docs/docs.go b/docs/docs.go index 571522d..6f74db5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -904,8 +904,8 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "0.0.1", - Host: "", - BasePath: "", + Host: "localhost:2893", + BasePath: "/api/v1", Schemes: []string{}, Title: "Watchtower service", Description: "Watchtower is a project designed to provide processing files created into cloud by events.", diff --git a/docs/swagger.json b/docs/swagger.json index 880503c..6d06416 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6,6 +6,8 @@ "contact": {}, "version": "0.0.1" }, + "host": "localhost:2893", + "basePath": "/api/v1", "paths": { "/cloud/bucket": { "put": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9735112..8d818f0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,3 +1,4 @@ +basePath: /api/v1 definitions: dto.TaskEvent: properties: @@ -107,6 +108,7 @@ definitions: example: test-file.docx type: string type: object +host: localhost:2893 info: contact: {} description: Watchtower is a project designed to provide processing files created diff --git a/internal/infrastructure/httpserver/httpserver.go b/internal/infrastructure/httpserver/httpserver.go index 69464b6..86d0000 100644 --- a/internal/infrastructure/httpserver/httpserver.go +++ b/internal/infrastructure/httpserver/httpserver.go @@ -49,7 +49,7 @@ func (s *Server) setupServer() { _ = s.CreateStorageBucketsGroup() _ = s.CreateStorageObjectsGroup() - s.server.GET("/swagger/*", echoSwagger.WrapHandler) + s.server.GET("/api/v1/swagger/*", echoSwagger.WrapHandler) } func (s *Server) Start(_ context.Context) error { diff --git a/internal/infrastructure/httpserver/routes_bucket.go b/internal/infrastructure/httpserver/routes_bucket.go index f2210dc..45dad94 100644 --- a/internal/infrastructure/httpserver/routes_bucket.go +++ b/internal/infrastructure/httpserver/routes_bucket.go @@ -9,7 +9,7 @@ import ( ) func (s *Server) CreateStorageBucketsGroup() error { - group := s.server.Group("/cloud") + group := s.server.Group("/api/v1/cloud") group.GET("/buckets", s.GetBuckets) group.PUT("/bucket", s.CreateBucket) diff --git a/internal/infrastructure/httpserver/routes_object.go b/internal/infrastructure/httpserver/routes_object.go index 6e87f40..dd9a824 100644 --- a/internal/infrastructure/httpserver/routes_object.go +++ b/internal/infrastructure/httpserver/routes_object.go @@ -14,7 +14,7 @@ import ( ) func (s *Server) CreateStorageObjectsGroup() error { - group := s.server.Group("/cloud") + group := s.server.Group("/api/v1/cloud") group.POST("/:bucket/files", s.GetFiles) group.POST("/:bucket/file/copy", s.CopyFile) diff --git a/internal/infrastructure/httpserver/routes_task.go b/internal/infrastructure/httpserver/routes_task.go index 92cd4a2..ef1b43a 100644 --- a/internal/infrastructure/httpserver/routes_task.go +++ b/internal/infrastructure/httpserver/routes_task.go @@ -12,7 +12,7 @@ import ( ) func (s *Server) CreateTasksGroup() error { - group := s.server.Group("/tasks") + group := s.server.Group("/api/v1/tasks") group.GET("/:bucket", s.LoadTasks) group.GET("/:bucket/:task_id", s.LoadTaskByID) From 382bd263b2bae9106113b847be08e4e6dee95137 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Thu, 27 Nov 2025 10:25:06 +0300 Subject: [PATCH 07/47] chore: removed servers from swagger --- cmd/watchtower/httpserver/httpserver.go | 3 --- cmd/watchtower/httpserver/routes_bucket.go | 7 ++++--- cmd/watchtower/httpserver/routes_object.go | 18 +++++++++--------- cmd/watchtower/httpserver/routes_task.go | 4 ++-- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cmd/watchtower/httpserver/httpserver.go b/cmd/watchtower/httpserver/httpserver.go index 1b35fac..dfc49b1 100644 --- a/cmd/watchtower/httpserver/httpserver.go +++ b/cmd/watchtower/httpserver/httpserver.go @@ -34,9 +34,6 @@ import ( // processConsumedTask -> 2; // Successful -> 3. // -// @host localhost:2893 -// @BasePath /api/v1 -// // @tag.name buckets // @tag.description CRUD APIs to manage cloud buckets // diff --git a/cmd/watchtower/httpserver/routes_bucket.go b/cmd/watchtower/httpserver/routes_bucket.go index 312a245..3f1d842 100644 --- a/cmd/watchtower/httpserver/routes_bucket.go +++ b/cmd/watchtower/httpserver/routes_bucket.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/labstack/echo/v4" + "watchtower/cmd/watchtower/httpserver/form" ) @@ -26,7 +27,7 @@ func (s *Server) CreateStorageBucketsGroup() error { // @Produce json // @Success 200 {array} string "Ok" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/buckets [get] +// @Router /api/v1/cloud/buckets [get] func (s *Server) GetBuckets(eCtx echo.Context) error { ctx := eCtx.Request().Context() buckets, err := s.state.GetObjectStorage().GetAllBuckets(ctx) @@ -53,7 +54,7 @@ func (s *Server) GetBuckets(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/bucket [put] +// @Router /api/v1/cloud/bucket [put] func (s *Server) CreateBucket(eCtx echo.Context) error { ctx := eCtx.Request().Context() @@ -82,7 +83,7 @@ func (s *Server) CreateBucket(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket} [delete] +// @Router /api/v1/cloud/{bucket} [delete] func (s *Server) RemoveBucket(eCtx echo.Context) error { ctx := eCtx.Request().Context() diff --git a/cmd/watchtower/httpserver/routes_object.go b/cmd/watchtower/httpserver/routes_object.go index 8022ec7..eff8cf2 100644 --- a/cmd/watchtower/httpserver/routes_object.go +++ b/cmd/watchtower/httpserver/routes_object.go @@ -43,7 +43,7 @@ func (s *Server) CreateStorageObjectsGroup() error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/copy [post] +// @Router /api/v1/cloud/{bucket}/file/copy [post] func (s *Server) CopyFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -80,7 +80,7 @@ func (s *Server) CopyFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/move [post] +// @Router /api/v1/cloud/{bucket}/file/move [post] func (s *Server) MoveFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -118,7 +118,7 @@ func (s *Server) MoveFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/upload [put] +// @Router /api/v1/cloud/{bucket}/file/upload [put] func (s *Server) UploadFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() @@ -210,7 +210,7 @@ func (s *Server) UploadFile(eCtx echo.Context) error { // @Success 200 {file} io.Writer "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/download [post] +// @Router /api/v1/cloud/{bucket}/file/download [post] func (s *Server) DownloadFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -241,7 +241,7 @@ func (s *Server) DownloadFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/remove [delete] +// @Router /api/v1/cloud/{bucket}/file/remove [delete] func (s *Server) RemoveFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -270,7 +270,7 @@ func (s *Server) RemoveFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file [delete] +// @Router /api/v1/cloud/{bucket}/file [delete] func (s *Server) RemoveFile2(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -294,7 +294,7 @@ func (s *Server) RemoveFile2(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/files [post] +// @Router /api/v1/cloud/{bucket}/files [post] func (s *Server) GetFiles(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -335,7 +335,7 @@ func (s *Server) GetFiles(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/attributes [post] +// @Router /api/v1/cloud/{bucket}/file/attributes [post] func (s *Server) GetFileInfo(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -367,7 +367,7 @@ func (s *Server) GetFileInfo(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/share [post] +// @Router /api/v1/cloud/{bucket}/file/share [post] func (s *Server) ShareFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") diff --git a/cmd/watchtower/httpserver/routes_task.go b/cmd/watchtower/httpserver/routes_task.go index 8c59638..3ee7a1d 100644 --- a/cmd/watchtower/httpserver/routes_task.go +++ b/cmd/watchtower/httpserver/routes_task.go @@ -34,7 +34,7 @@ func (s *Server) CreateTasksGroup() error { // @Success 200 {object} []form.TaskSchema "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /tasks/{bucket} [get] +// @Router /api/v1/tasks/{bucket} [get] func (s *Server) LoadTasks(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -82,7 +82,7 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { // @Success 200 {object} form.TaskSchema "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /tasks/{bucket}/{task_id} [get] +// @Router /api/v1/tasks/{bucket}/{task_id} [get] func (s *Server) LoadTaskByID(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") From a25b3838e18910192a284a37e081933716cd4565 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Thu, 27 Nov 2025 10:25:17 +0300 Subject: [PATCH 08/47] chore: added new TODO --- internal/support/task/application/usecase.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/support/task/application/usecase.go b/internal/support/task/application/usecase.go index 23c7fa0..032e309 100644 --- a/internal/support/task/application/usecase.go +++ b/internal/support/task/application/usecase.go @@ -164,6 +164,7 @@ func (p *TaskUseCase) Recognize( FileData: fileData, } + // TODO: impled retry pattern recData, err := p.recognizer.Recognize(ctx, inputFile) if err != nil { task.SetStatusAndText(domain.Failed, "failed to recognize file") From 4f5f64c212b70b8bf172b5138dded018c65abf83 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Thu, 27 Nov 2025 10:25:33 +0300 Subject: [PATCH 09/47] chore: updated swagger docs after all changes --- docs/docs.go | 28 ++++++++++++++-------------- docs/swagger.json | 28 ++++++++++++++-------------- docs/swagger.yaml | 28 ++++++++++++++-------------- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index aa9d2c6..3de2fa7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,7 +15,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/cloud/bucket": { + "/api/v1/cloud/bucket": { "put": { "description": "Create new bucket into cloud", "consumes": [ @@ -62,7 +62,7 @@ const docTemplate = `{ } } }, - "/cloud/buckets": { + "/api/v1/cloud/buckets": { "get": { "description": "Get watched bucket list", "produces": [ @@ -92,7 +92,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}": { + "/api/v1/cloud/{bucket}": { "delete": { "description": "Remove bucket from cloud", "produces": [ @@ -134,7 +134,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file": { + "/api/v1/cloud/{bucket}/file": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -183,7 +183,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file/attributes": { + "/api/v1/cloud/{bucket}/file/attributes": { "post": { "description": "Get file attributes", "consumes": [ @@ -237,7 +237,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file/copy": { + "/api/v1/cloud/{bucket}/file/copy": { "post": { "description": "Copy file to another location into bucket", "consumes": [ @@ -291,7 +291,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file/download": { + "/api/v1/cloud/{bucket}/file/download": { "post": { "description": "Download file from cloud", "consumes": [ @@ -345,7 +345,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file/move": { + "/api/v1/cloud/{bucket}/file/move": { "post": { "description": "Move file to another location into bucket", "consumes": [ @@ -399,7 +399,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file/remove": { + "/api/v1/cloud/{bucket}/file/remove": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -450,7 +450,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file/share": { + "/api/v1/cloud/{bucket}/file/share": { "post": { "description": "Get share URL for file", "consumes": [ @@ -504,7 +504,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/file/upload": { + "/api/v1/cloud/{bucket}/file/upload": { "put": { "description": "Upload files to cloud", "consumes": [ @@ -562,7 +562,7 @@ const docTemplate = `{ } } }, - "/cloud/{bucket}/files": { + "/api/v1/cloud/{bucket}/files": { "post": { "description": "Get files list into bucket", "consumes": [ @@ -616,7 +616,7 @@ const docTemplate = `{ } } }, - "/tasks/{bucket}": { + "/api/v1/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", "consumes": [ @@ -670,7 +670,7 @@ const docTemplate = `{ } } }, - "/tasks/{bucket}/{task_id}": { + "/api/v1/tasks/{bucket}/{task_id}": { "get": { "description": "Load processing/unrecognized/done task by id of uploaded file", "consumes": [ diff --git a/docs/swagger.json b/docs/swagger.json index 6171313..93fddbf 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,7 +4,7 @@ "contact": {} }, "paths": { - "/cloud/bucket": { + "/api/v1/cloud/bucket": { "put": { "description": "Create new bucket into cloud", "consumes": [ @@ -51,7 +51,7 @@ } } }, - "/cloud/buckets": { + "/api/v1/cloud/buckets": { "get": { "description": "Get watched bucket list", "produces": [ @@ -81,7 +81,7 @@ } } }, - "/cloud/{bucket}": { + "/api/v1/cloud/{bucket}": { "delete": { "description": "Remove bucket from cloud", "produces": [ @@ -123,7 +123,7 @@ } } }, - "/cloud/{bucket}/file": { + "/api/v1/cloud/{bucket}/file": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -172,7 +172,7 @@ } } }, - "/cloud/{bucket}/file/attributes": { + "/api/v1/cloud/{bucket}/file/attributes": { "post": { "description": "Get file attributes", "consumes": [ @@ -226,7 +226,7 @@ } } }, - "/cloud/{bucket}/file/copy": { + "/api/v1/cloud/{bucket}/file/copy": { "post": { "description": "Copy file to another location into bucket", "consumes": [ @@ -280,7 +280,7 @@ } } }, - "/cloud/{bucket}/file/download": { + "/api/v1/cloud/{bucket}/file/download": { "post": { "description": "Download file from cloud", "consumes": [ @@ -334,7 +334,7 @@ } } }, - "/cloud/{bucket}/file/move": { + "/api/v1/cloud/{bucket}/file/move": { "post": { "description": "Move file to another location into bucket", "consumes": [ @@ -388,7 +388,7 @@ } } }, - "/cloud/{bucket}/file/remove": { + "/api/v1/cloud/{bucket}/file/remove": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -439,7 +439,7 @@ } } }, - "/cloud/{bucket}/file/share": { + "/api/v1/cloud/{bucket}/file/share": { "post": { "description": "Get share URL for file", "consumes": [ @@ -493,7 +493,7 @@ } } }, - "/cloud/{bucket}/file/upload": { + "/api/v1/cloud/{bucket}/file/upload": { "put": { "description": "Upload files to cloud", "consumes": [ @@ -551,7 +551,7 @@ } } }, - "/cloud/{bucket}/files": { + "/api/v1/cloud/{bucket}/files": { "post": { "description": "Get files list into bucket", "consumes": [ @@ -605,7 +605,7 @@ } } }, - "/tasks/{bucket}": { + "/api/v1/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", "consumes": [ @@ -659,7 +659,7 @@ } } }, - "/tasks/{bucket}/{task_id}": { + "/api/v1/tasks/{bucket}/{task_id}": { "get": { "description": "Load processing/unrecognized/done task by id of uploaded file", "consumes": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6cfbcd9..6e7579c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -96,7 +96,7 @@ definitions: info: contact: {} paths: - /cloud/{bucket}: + /api/v1/cloud/{bucket}: delete: description: Remove bucket from cloud operationId: remove-bucket @@ -124,7 +124,7 @@ paths: summary: Remove bucket from cloud tags: - buckets - /cloud/{bucket}/file: + /api/v1/cloud/{bucket}/file: delete: description: Remove file from cloud operationId: remove-file-2 @@ -157,7 +157,7 @@ paths: summary: Remove file from cloud tags: - files - /cloud/{bucket}/file/attributes: + /api/v1/cloud/{bucket}/file/attributes: post: consumes: - application/json @@ -193,7 +193,7 @@ paths: summary: Get file attributes tags: - files - /cloud/{bucket}/file/copy: + /api/v1/cloud/{bucket}/file/copy: post: consumes: - application/json @@ -229,7 +229,7 @@ paths: summary: Copy file to another location into bucket tags: - files - /cloud/{bucket}/file/download: + /api/v1/cloud/{bucket}/file/download: post: consumes: - application/json @@ -265,7 +265,7 @@ paths: summary: Download file from cloud tags: - files - /cloud/{bucket}/file/move: + /api/v1/cloud/{bucket}/file/move: post: consumes: - application/json @@ -301,7 +301,7 @@ paths: summary: Move file to another location into bucket tags: - files - /cloud/{bucket}/file/remove: + /api/v1/cloud/{bucket}/file/remove: delete: description: Remove file from cloud operationId: remove-file @@ -335,7 +335,7 @@ paths: summary: Remove file from cloud tags: - files - /cloud/{bucket}/file/share: + /api/v1/cloud/{bucket}/file/share: post: consumes: - application/json @@ -371,7 +371,7 @@ paths: summary: Get share URL for file tags: - share - /cloud/{bucket}/file/upload: + /api/v1/cloud/{bucket}/file/upload: put: consumes: - multipart/form @@ -410,7 +410,7 @@ paths: summary: Upload files to cloud tags: - files - /cloud/{bucket}/files: + /api/v1/cloud/{bucket}/files: post: consumes: - application/json @@ -446,7 +446,7 @@ paths: summary: Get files list into bucket tags: - files - /cloud/bucket: + /api/v1/cloud/bucket: put: consumes: - application/json @@ -477,7 +477,7 @@ paths: summary: Create new bucket into cloud tags: - buckets - /cloud/buckets: + /api/v1/cloud/buckets: get: description: Get watched bucket list operationId: get-buckets @@ -497,7 +497,7 @@ paths: summary: Get watched bucket list tags: - buckets - /tasks/{bucket}: + /api/v1/tasks/{bucket}: get: consumes: - application/json @@ -533,7 +533,7 @@ paths: summary: Load processing tasks of uploaded files into bucket tags: - tasks - /tasks/{bucket}/{task_id}: + /api/v1/tasks/{bucket}/{task_id}: get: consumes: - application/json From 50be5581a85913501475fbd2d135e9c07e6cf097 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Feb 2026 16:25:22 +0300 Subject: [PATCH 10/47] chore: updated routes and swagger docs --- cmd/watchtower/httpserver/routes_bucket.go | 17 ++++++++++--- cmd/watchtower/httpserver/routes_object.go | 18 +++++++------- cmd/watchtower/httpserver/routes_task.go | 4 ++-- docs/docs.go | 28 +++++++++++----------- docs/swagger.json | 28 +++++++++++----------- docs/swagger.yaml | 28 +++++++++++----------- 6 files changed, 67 insertions(+), 56 deletions(-) diff --git a/cmd/watchtower/httpserver/routes_bucket.go b/cmd/watchtower/httpserver/routes_bucket.go index 3f1d842..c2ca158 100644 --- a/cmd/watchtower/httpserver/routes_bucket.go +++ b/cmd/watchtower/httpserver/routes_bucket.go @@ -27,7 +27,7 @@ func (s *Server) CreateStorageBucketsGroup() error { // @Produce json // @Success 200 {array} string "Ok" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/buckets [get] +// @Router /cloud/buckets [get] func (s *Server) GetBuckets(eCtx echo.Context) error { ctx := eCtx.Request().Context() buckets, err := s.state.GetObjectStorage().GetAllBuckets(ctx) @@ -54,7 +54,7 @@ func (s *Server) GetBuckets(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/bucket [put] +// @Router /cloud/bucket [put] func (s *Server) CreateBucket(eCtx echo.Context) error { ctx := eCtx.Request().Context() @@ -65,6 +65,17 @@ func (s *Server) CreateBucket(eCtx echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } + exists, err := s.state.GetObjectStorage().IsBucketExists(ctx, jsonForm.BucketName) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + if exists { + // TODO: Temporary solution. Need to return 409 http error + //return echo.NewHTTPError(http.StatusConflict, "bucket already exists") + return echo.NewHTTPError(http.StatusOK, "bucket already exists") + } + err = s.state.GetObjectStorage().CreateBucket(ctx, jsonForm.BucketName) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) @@ -83,7 +94,7 @@ func (s *Server) CreateBucket(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket} [delete] +// @Router /cloud/{bucket} [delete] func (s *Server) RemoveBucket(eCtx echo.Context) error { ctx := eCtx.Request().Context() diff --git a/cmd/watchtower/httpserver/routes_object.go b/cmd/watchtower/httpserver/routes_object.go index eff8cf2..8022ec7 100644 --- a/cmd/watchtower/httpserver/routes_object.go +++ b/cmd/watchtower/httpserver/routes_object.go @@ -43,7 +43,7 @@ func (s *Server) CreateStorageObjectsGroup() error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/copy [post] +// @Router /cloud/{bucket}/file/copy [post] func (s *Server) CopyFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -80,7 +80,7 @@ func (s *Server) CopyFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/move [post] +// @Router /cloud/{bucket}/file/move [post] func (s *Server) MoveFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -118,7 +118,7 @@ func (s *Server) MoveFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/upload [put] +// @Router /cloud/{bucket}/file/upload [put] func (s *Server) UploadFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() @@ -210,7 +210,7 @@ func (s *Server) UploadFile(eCtx echo.Context) error { // @Success 200 {file} io.Writer "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/download [post] +// @Router /cloud/{bucket}/file/download [post] func (s *Server) DownloadFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -241,7 +241,7 @@ func (s *Server) DownloadFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/remove [delete] +// @Router /cloud/{bucket}/file/remove [delete] func (s *Server) RemoveFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -270,7 +270,7 @@ func (s *Server) RemoveFile(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file [delete] +// @Router /cloud/{bucket}/file [delete] func (s *Server) RemoveFile2(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -294,7 +294,7 @@ func (s *Server) RemoveFile2(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/files [post] +// @Router /cloud/{bucket}/files [post] func (s *Server) GetFiles(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -335,7 +335,7 @@ func (s *Server) GetFiles(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/attributes [post] +// @Router /cloud/{bucket}/file/attributes [post] func (s *Server) GetFileInfo(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -367,7 +367,7 @@ func (s *Server) GetFileInfo(eCtx echo.Context) error { // @Success 200 {object} form.ResponseForm "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/share [post] +// @Router /cloud/{bucket}/file/share [post] func (s *Server) ShareFile(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") diff --git a/cmd/watchtower/httpserver/routes_task.go b/cmd/watchtower/httpserver/routes_task.go index 3ee7a1d..8c59638 100644 --- a/cmd/watchtower/httpserver/routes_task.go +++ b/cmd/watchtower/httpserver/routes_task.go @@ -34,7 +34,7 @@ func (s *Server) CreateTasksGroup() error { // @Success 200 {object} []form.TaskSchema "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/tasks/{bucket} [get] +// @Router /tasks/{bucket} [get] func (s *Server) LoadTasks(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") @@ -82,7 +82,7 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { // @Success 200 {object} form.TaskSchema "Ok" // @Failure 400 {object} form.BadRequestForm "Bad Request message" // @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /api/v1/tasks/{bucket}/{task_id} [get] +// @Router /tasks/{bucket}/{task_id} [get] func (s *Server) LoadTaskByID(eCtx echo.Context) error { ctx := eCtx.Request().Context() bucket := eCtx.Param("bucket") diff --git a/docs/docs.go b/docs/docs.go index 3de2fa7..aa9d2c6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,7 +15,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/v1/cloud/bucket": { + "/cloud/bucket": { "put": { "description": "Create new bucket into cloud", "consumes": [ @@ -62,7 +62,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/buckets": { + "/cloud/buckets": { "get": { "description": "Get watched bucket list", "produces": [ @@ -92,7 +92,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}": { + "/cloud/{bucket}": { "delete": { "description": "Remove bucket from cloud", "produces": [ @@ -134,7 +134,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file": { + "/cloud/{bucket}/file": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -183,7 +183,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file/attributes": { + "/cloud/{bucket}/file/attributes": { "post": { "description": "Get file attributes", "consumes": [ @@ -237,7 +237,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file/copy": { + "/cloud/{bucket}/file/copy": { "post": { "description": "Copy file to another location into bucket", "consumes": [ @@ -291,7 +291,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file/download": { + "/cloud/{bucket}/file/download": { "post": { "description": "Download file from cloud", "consumes": [ @@ -345,7 +345,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file/move": { + "/cloud/{bucket}/file/move": { "post": { "description": "Move file to another location into bucket", "consumes": [ @@ -399,7 +399,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file/remove": { + "/cloud/{bucket}/file/remove": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -450,7 +450,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file/share": { + "/cloud/{bucket}/file/share": { "post": { "description": "Get share URL for file", "consumes": [ @@ -504,7 +504,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/file/upload": { + "/cloud/{bucket}/file/upload": { "put": { "description": "Upload files to cloud", "consumes": [ @@ -562,7 +562,7 @@ const docTemplate = `{ } } }, - "/api/v1/cloud/{bucket}/files": { + "/cloud/{bucket}/files": { "post": { "description": "Get files list into bucket", "consumes": [ @@ -616,7 +616,7 @@ const docTemplate = `{ } } }, - "/api/v1/tasks/{bucket}": { + "/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", "consumes": [ @@ -670,7 +670,7 @@ const docTemplate = `{ } } }, - "/api/v1/tasks/{bucket}/{task_id}": { + "/tasks/{bucket}/{task_id}": { "get": { "description": "Load processing/unrecognized/done task by id of uploaded file", "consumes": [ diff --git a/docs/swagger.json b/docs/swagger.json index 93fddbf..6171313 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,7 +4,7 @@ "contact": {} }, "paths": { - "/api/v1/cloud/bucket": { + "/cloud/bucket": { "put": { "description": "Create new bucket into cloud", "consumes": [ @@ -51,7 +51,7 @@ } } }, - "/api/v1/cloud/buckets": { + "/cloud/buckets": { "get": { "description": "Get watched bucket list", "produces": [ @@ -81,7 +81,7 @@ } } }, - "/api/v1/cloud/{bucket}": { + "/cloud/{bucket}": { "delete": { "description": "Remove bucket from cloud", "produces": [ @@ -123,7 +123,7 @@ } } }, - "/api/v1/cloud/{bucket}/file": { + "/cloud/{bucket}/file": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -172,7 +172,7 @@ } } }, - "/api/v1/cloud/{bucket}/file/attributes": { + "/cloud/{bucket}/file/attributes": { "post": { "description": "Get file attributes", "consumes": [ @@ -226,7 +226,7 @@ } } }, - "/api/v1/cloud/{bucket}/file/copy": { + "/cloud/{bucket}/file/copy": { "post": { "description": "Copy file to another location into bucket", "consumes": [ @@ -280,7 +280,7 @@ } } }, - "/api/v1/cloud/{bucket}/file/download": { + "/cloud/{bucket}/file/download": { "post": { "description": "Download file from cloud", "consumes": [ @@ -334,7 +334,7 @@ } } }, - "/api/v1/cloud/{bucket}/file/move": { + "/cloud/{bucket}/file/move": { "post": { "description": "Move file to another location into bucket", "consumes": [ @@ -388,7 +388,7 @@ } } }, - "/api/v1/cloud/{bucket}/file/remove": { + "/cloud/{bucket}/file/remove": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -439,7 +439,7 @@ } } }, - "/api/v1/cloud/{bucket}/file/share": { + "/cloud/{bucket}/file/share": { "post": { "description": "Get share URL for file", "consumes": [ @@ -493,7 +493,7 @@ } } }, - "/api/v1/cloud/{bucket}/file/upload": { + "/cloud/{bucket}/file/upload": { "put": { "description": "Upload files to cloud", "consumes": [ @@ -551,7 +551,7 @@ } } }, - "/api/v1/cloud/{bucket}/files": { + "/cloud/{bucket}/files": { "post": { "description": "Get files list into bucket", "consumes": [ @@ -605,7 +605,7 @@ } } }, - "/api/v1/tasks/{bucket}": { + "/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", "consumes": [ @@ -659,7 +659,7 @@ } } }, - "/api/v1/tasks/{bucket}/{task_id}": { + "/tasks/{bucket}/{task_id}": { "get": { "description": "Load processing/unrecognized/done task by id of uploaded file", "consumes": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6e7579c..6cfbcd9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -96,7 +96,7 @@ definitions: info: contact: {} paths: - /api/v1/cloud/{bucket}: + /cloud/{bucket}: delete: description: Remove bucket from cloud operationId: remove-bucket @@ -124,7 +124,7 @@ paths: summary: Remove bucket from cloud tags: - buckets - /api/v1/cloud/{bucket}/file: + /cloud/{bucket}/file: delete: description: Remove file from cloud operationId: remove-file-2 @@ -157,7 +157,7 @@ paths: summary: Remove file from cloud tags: - files - /api/v1/cloud/{bucket}/file/attributes: + /cloud/{bucket}/file/attributes: post: consumes: - application/json @@ -193,7 +193,7 @@ paths: summary: Get file attributes tags: - files - /api/v1/cloud/{bucket}/file/copy: + /cloud/{bucket}/file/copy: post: consumes: - application/json @@ -229,7 +229,7 @@ paths: summary: Copy file to another location into bucket tags: - files - /api/v1/cloud/{bucket}/file/download: + /cloud/{bucket}/file/download: post: consumes: - application/json @@ -265,7 +265,7 @@ paths: summary: Download file from cloud tags: - files - /api/v1/cloud/{bucket}/file/move: + /cloud/{bucket}/file/move: post: consumes: - application/json @@ -301,7 +301,7 @@ paths: summary: Move file to another location into bucket tags: - files - /api/v1/cloud/{bucket}/file/remove: + /cloud/{bucket}/file/remove: delete: description: Remove file from cloud operationId: remove-file @@ -335,7 +335,7 @@ paths: summary: Remove file from cloud tags: - files - /api/v1/cloud/{bucket}/file/share: + /cloud/{bucket}/file/share: post: consumes: - application/json @@ -371,7 +371,7 @@ paths: summary: Get share URL for file tags: - share - /api/v1/cloud/{bucket}/file/upload: + /cloud/{bucket}/file/upload: put: consumes: - multipart/form @@ -410,7 +410,7 @@ paths: summary: Upload files to cloud tags: - files - /api/v1/cloud/{bucket}/files: + /cloud/{bucket}/files: post: consumes: - application/json @@ -446,7 +446,7 @@ paths: summary: Get files list into bucket tags: - files - /api/v1/cloud/bucket: + /cloud/bucket: put: consumes: - application/json @@ -477,7 +477,7 @@ paths: summary: Create new bucket into cloud tags: - buckets - /api/v1/cloud/buckets: + /cloud/buckets: get: description: Get watched bucket list operationId: get-buckets @@ -497,7 +497,7 @@ paths: summary: Get watched bucket list tags: - buckets - /api/v1/tasks/{bucket}: + /tasks/{bucket}: get: consumes: - application/json @@ -533,7 +533,7 @@ paths: summary: Load processing tasks of uploaded files into bucket tags: - tasks - /api/v1/tasks/{bucket}/{task_id}: + /tasks/{bucket}/{task_id}: get: consumes: - application/json From 86d0fc97e92aa6803bba10263301fc224be92f04 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 15:57:47 +0300 Subject: [PATCH 11/47] chore: refactored configs --- cmd/config.go | 136 ++++++++++++++ cmd/watchtower/config/config.go | 204 --------------------- configs/{testing.toml => development.toml} | 0 configs/{config.toml => production.toml} | 0 4 files changed, 136 insertions(+), 204 deletions(-) create mode 100644 cmd/config.go delete mode 100644 cmd/watchtower/config/config.go rename configs/{testing.toml => development.toml} (100%) rename configs/{config.toml => production.toml} (100%) diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..222339b --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,136 @@ +package cmd + +import ( + "fmt" + "log/slog" + "os" + "strings" + + "github.com/spf13/viper" + + "watchtower/cmd/watchtower/httpserver" + "watchtower/internal/core/cloud/infrastructure/s3" + "watchtower/internal/process" + "watchtower/internal/shared/telemetry" + "watchtower/internal/support/task/infrastructure/docparser" + "watchtower/internal/support/task/infrastructure/docsearch" + "watchtower/internal/support/task/infrastructure/redis" + "watchtower/internal/support/task/infrastructure/rmq" +) + +type Config struct { + Orchestrator process.Config `mapstructure:"orchestrator"` + Otlp telemetry.OtlpConfig `mapstructure:"otlp"` + Server ServerConfig `mapstructure:"server"` + Storage StorageConfig `mapstructure:"storage"` + Task TaskConfig `mapstructure:"task"` +} + +type ServerConfig struct { + Http httpserver.Config `mapstructure:"http"` +} + +type StorageConfig struct { + S3 s3.Config `mapstructure:"s3"` +} + +type TaskConfig struct { + TaskStorage TaskStorageConfig `mapstructure:"storage"` + TaskQueue TaskQueueConfig `mapstructure:"queue"` + Processor ProcessorConfig `mapstructure:"processor"` +} + +type TaskStorageConfig struct { + Redis redis.Config `mapstructure:"redis"` +} + +type TaskQueueConfig struct { + Rmq rmq.Config `mapstructure:"rmq"` +} + +type ProcessorConfig struct { + DocParser docparser.Config `mapstructure:"docparser"` + DocStorage docsearch.Config `mapstructure:"docstorage"` +} + +const ( + launchModeEnvKey = "WATCHTOWER__RUN_MODE" + defaultLaunchMode = "development" + serviceEnvPrefix = "WATCHTOWER" +) + +func InitConfig() (*Config, error) { + launchMode := os.Getenv(launchModeEnvKey) + if launchMode == "" { + launchMode = defaultLaunchMode + } + + viperInst := viper.New() + + viperInst.SetConfigName(launchMode) + viperInst.SetConfigType("toml") + + viperInst.AddConfigPath(".") + viperInst.AddConfigPath("./configs") + viperInst.AddConfigPath("../configs") + + if err := viperInst.ReadInConfig(); err != nil { + //nolint + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + slog.Info("config file not found, using env vars") + } else { + return nil, fmt.Errorf("error reading config file: %w", err) + } + } + + setupEnv(viperInst) + + config := &Config{} + if err := viper.Unmarshal(config); err != nil { + confErr := fmt.Errorf("failed while unmarshaling config: %w", err) + return config, confErr + } + + return config, nil +} + +func setupEnv(viperInst *viper.Viper) { + viperInst.AutomaticEnv() + viperInst.SetEnvPrefix(serviceEnvPrefix) + viperInst.SetEnvKeyReplacer(strings.NewReplacer(".", "__")) + + envMappings := map[string]string{ + "orchestrator.semaphore_size": "ORCHESTRATOR__SEMAPHORE_SIZE", + "otlp.logger.level": "OTLP__LOGGER__LEVEL", + "otlp.logger.address": "OTLP__LOGGER__ADDRESS", + "otlp.logger.enable_loki": "OTLP__LOGGER__ENABLE_LOKI", + "otlp.tracer.address": "OTLP__TRACER__ADDRESS", + "otlp.tracer.enable_jaeger": "OTLP__TRACER__ENABLE_JAEGER", + "server.http.address": "SERVER__HTTP__ADDRESS", + "storage.s3.address": "STORAGE__S3__ADDRESS", + "storage.s3.access_id": "STORAGE__S3__ACCESS_ID", + "storage.s3.secret_key": "STORAGE__S3__SECRET_KEY", + "storage.s3.enable_ssl": "STORAGE__S3__ENABLE_SSL", + "storage.s3.token": "STORAGE__S3__TOKEN", + "task.storage.redis.address": "TASK__STORAGE__REDIS__ADDRESS", + "task.storage.redis.username": "TASK__STORAGE__REDIS__USERNAME", + "task.storage.redis.password": "TASK__STORAGE__REDIS__PASSWORD", + "task.storage.redis.expired": "TASK__STORAGE__REDIS__EXPIRED", + "task.queue.rmq.address": "TASK__QUEUE__RMQ__ADDRESS", + "task.queue.rmq.exchange": "TASK__QUEUE__RMQ__EXCHANGE", + "task.queue.rmq.routing_key": "TASK__QUEUE__RMQ__ROUTING_KEY", + "task.queue.rmq.queue": "TASK__QUEUE__RMQ__QUEUE", + "task.processor.docstorage.address": "TASK__PROCESSOR__DOCSTORAGE__ADDRESS", + "task.processor.docstorage.timeout": "TASK__PROCESSOR__DOCSTORAGE__TIMEOUT", + "task.processor.docparser.address": "TASK__PROCESSOR__DOCPARSER__ADDRESS", + "task.processor.docparser.timeout": "TASK__PROCESSOR__DOCPARSER__TIMEOUT", + } + + var bindErr error + for key, value := range envMappings { + bindErr = viperInst.BindEnv(key, fmt.Sprintf("%s__%s", serviceEnvPrefix, value)) + if bindErr != nil { + slog.Warn("failed to bind env var", bindErr) + } + } +} diff --git a/cmd/watchtower/config/config.go b/cmd/watchtower/config/config.go deleted file mode 100644 index 3aa127c..0000000 --- a/cmd/watchtower/config/config.go +++ /dev/null @@ -1,204 +0,0 @@ -package config - -import ( - "fmt" - "strings" - - "github.com/joho/godotenv" - "github.com/spf13/viper" - - "watchtower/cmd/watchtower/httpserver" - "watchtower/internal/core/cloud/infrastructure/s3" - "watchtower/internal/process" - "watchtower/internal/shared/telemetry" - "watchtower/internal/support/task/infrastructure/docparser" - "watchtower/internal/support/task/infrastructure/docsearch" - "watchtower/internal/support/task/infrastructure/redis" - "watchtower/internal/support/task/infrastructure/rmq" -) - -type Config struct { - Orchestrator process.Config `mapstructure:"orchestrator"` - Otlp telemetry.OtlpConfig `mapstructure:"otlp"` - Server ServerConfig `mapstructure:"server"` - Storage StorageConfig `mapstructure:"storage"` - Task TaskConfig `mapstructure:"task"` -} - -type ServerConfig struct { - Http httpserver.Config `mapstructure:"http"` -} - -type StorageConfig struct { - S3 s3.Config `mapstructure:"s3"` -} - -type TaskConfig struct { - TaskStorage TaskStorageConfig `mapstructure:"storage"` - TaskQueue TaskQueueConfig `mapstructure:"queue"` - Processor ProcessorConfig `mapstructure:"processor"` -} - -type TaskStorageConfig struct { - Redis redis.Config `mapstructure:"redis"` -} - -type TaskQueueConfig struct { - Rmq rmq.Config `mapstructure:"rmq"` -} - -type ProcessorConfig struct { - DocParser docparser.Config `mapstructure:"docparser"` - DocStorage docsearch.Config `mapstructure:"docstorage"` -} - -func FromFile(filePath string) (*Config, error) { - _ = godotenv.Load() - - config := &Config{} - - viperInstance := viper.New() - viperInstance.SetConfigFile(filePath) - viperInstance.SetConfigType("toml") - - viperInstance.AutomaticEnv() - viperInstance.SetEnvPrefix("watchtower") - viperInstance.SetEnvKeyReplacer(strings.NewReplacer(".", "__")) - - // Orchestrator config - bindErr := viperInstance.BindEnv("orchestrator.semaphore_size", "WATCHTOWER__ORCHESTRATOR__SEMAPHORE_SIZE") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - // Otlp config - bindErr = viperInstance.BindEnv("otlp.logger.level", "WATCHTOWER__OTLP__LOGGER__LEVEL") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("otlp.logger.address", "WATCHTOWER__OTLP__LOGGER__ADDRESS") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("otlp.logger.enable_loki", "WATCHTOWER__OTLP__LOGGER__ENABLE_LOKI") - - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("otlp.tracer.address", "WATCHTOWER__OTLP__TRACER__ADDRESS") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("otlp.tracer.enable_jaeger", "WATCHTOWER__OTLP__TRACER__ENABLE_JAEGER") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - // Server config - bindErr = viperInstance.BindEnv("server.http.address", "WATCHTOWER__SERVER__HTTP__ADDRESS") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - // Storage s3 config - bindErr = viperInstance.BindEnv("storage.s3.address", "WATCHTOWER__STORAGE__S3__ADDRESS") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("storage.s3.access_id", "WATCHTOWER__STORAGE__S3__ACCESS_ID") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("storage.s3.secret_key", "WATCHTOWER__STORAGE__S3__SECRET_KEY") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("storage.s3.enable_ssl", "WATCHTOWER__STORAGE__S3__ENABLE_SSL") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("storage.s3.token", "WATCHTOWER__STORAGE__S3__TOKEN") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - // Cache redis config - bindErr = viperInstance.BindEnv("task.storage.redis.address", "WATCHTOWER__TASK__STORAGE__REDIS__ADDRESS") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("task.storage.redis.username", "WATCHTOWER__TASK__STORAGE__REDIS__USERNAME") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("task.storage.redis.password", "WATCHTOWER__TASK__STORAGE__REDIS__PASSWORD") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("task.storage.redis.expired", "WATCHTOWER__TASK__STORAGE__REDIS__EXPIRED") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - // Queue emq config - bindErr = viperInstance.BindEnv("task.queue.rmq.address", "WATCHTOWER__TASK__QUEUE__RMQ__ADDRESS") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("task.queue.rmq.exchange", "WATCHTOWER__TASK__QUEUE__RMQ__EXCHANGE") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("task.queue.rmq.routing_key", "WATCHTOWER__TASK__QUEUE__RMQ__ROUTING_KEY") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv("task.queue.rmq.queue", "WATCHTOWER__TASK__QUEUE__RMQ__QUEUE") - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - // Docstorage config - bindErr = viperInstance.BindEnv( - "task.processor.docstorage.address", - "WATCHTOWER__TASK__PROCESSOR__DOCSTORAGE__ADDRESS", - ) - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv( - "task.processor.docstorage.timeout", - "WATCHTOWER__TASK__PROCESSOR__DOCSTORAGE__TIMEOUT", - ) - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - // Docparser config - bindErr = viperInstance.BindEnv( - "task.processor.docparser.address", - "WATCHTOWER__TASK__PROCESSOR__DOCPARSER__ADDRESS", - ) - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - bindErr = viperInstance.BindEnv( - "task.processor.docparser.timeout", - "WATCHTOWER__TASK__PROCESSOR__DOCPARSER__TIMEOUT", - ) - if bindErr != nil { - return nil, fmt.Errorf("failed to bine env varialbe: %w", bindErr) - } - - if err := viperInstance.ReadInConfig(); err != nil { - confErr := fmt.Errorf("failed while reading config file %s: %w", filePath, err) - return config, confErr - } - - if err := viperInstance.Unmarshal(config); err != nil { - confErr := fmt.Errorf("failed while unmarshaling config file %s: %w", filePath, err) - return config, confErr - } - - return config, nil -} diff --git a/configs/testing.toml b/configs/development.toml similarity index 100% rename from configs/testing.toml rename to configs/development.toml diff --git a/configs/config.toml b/configs/production.toml similarity index 100% rename from configs/config.toml rename to configs/production.toml From e1c27c7c03092c939b18c87c28c44ed03000253c Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 15:58:08 +0300 Subject: [PATCH 12/47] chore: updated env vars --- .env | 6 ++++-- .../support/task/infrastructure/docparser/docparser.go | 8 +++----- .../support/task/infrastructure/docsearch/docsearch.go | 9 +++++---- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.env b/.env index cb699df..e0d6d4a 100644 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ +WATCHTOWER__RUN_MODE=development + WATCHTOWER__ORCHESTRATOR__SEMAPHORE_SIZE=10 WATCHTOWER__OTLP__LOGGER__LEVEL=DEBUG @@ -25,8 +27,8 @@ WATCHTOWER__TASK__QUEUE__RMQ__EXCHANGE=watchtower WATCHTOWER__TASK__QUEUE__RMQ__ROUTING_KEY=task WATCHTOWER__TASK__QUEUE__RMQ__QUEUE=watchtower-tasks -WATCHTOWER__TASK__PROCESSOR__DOCPARSER__ADDRESS=http://localhost:8012/api/v1 +WATCHTOWER__TASK__PROCESSOR__DOCPARSER__ADDRESS=http://localhost:8012 WATCHTOWER__TASK__PROCESSOR__DOCPARSER__TIMEOUT=100s -WATCHTOWER__TASK__PROCESSOR__DOCSTORAGE__ADDRESS=http://localhost:2892/api/v1 +WATCHTOWER__TASK__PROCESSOR__DOCSTORAGE__ADDRESS=http://localhost:2892 WATCHTOWER__TASK__PROCESSOR__DOCSTORAGE__TIMEOUT=100s \ No newline at end of file diff --git a/internal/support/task/infrastructure/docparser/docparser.go b/internal/support/task/infrastructure/docparser/docparser.go index f50ad63..64b20bb 100644 --- a/internal/support/task/infrastructure/docparser/docparser.go +++ b/internal/support/task/infrastructure/docparser/docparser.go @@ -2,19 +2,17 @@ package docparser import ( "bytes" - "context" "encoding/json" "fmt" "mime/multipart" "time" + "watchtower/internal/shared/kernel" "watchtower/internal/shared/utils" "watchtower/internal/support/task/application/service/recognizer" ) -type Ctx = context.Context - -const RecognitionURL = "/parser/parse/text" +const RecognitionURL = "/api/v1/parser/parse/text" type DocParser struct { config Config @@ -24,7 +22,7 @@ func New(config Config) recognizer.IRecognizer { return &DocParser{config} } -func (dc *DocParser) Recognize(ctx Ctx, params *recognizer.RecognizeParams) (*recognizer.Recognized, error) { +func (dc *DocParser) Recognize(ctx kernel.Ctx, params *recognizer.RecognizeParams) (*recognizer.Recognized, error) { var buf bytes.Buffer mpw := multipart.NewWriter(&buf) diff --git a/internal/support/task/infrastructure/docsearch/docsearch.go b/internal/support/task/infrastructure/docsearch/docsearch.go index d2e5500..f351e79 100644 --- a/internal/support/task/infrastructure/docsearch/docsearch.go +++ b/internal/support/task/infrastructure/docsearch/docsearch.go @@ -2,13 +2,13 @@ package docsearch import ( "bytes" - "context" "encoding/json" "fmt" "log/slog" "strings" "time" + "watchtower/internal/shared/kernel" "watchtower/internal/shared/utils" "watchtower/internal/support/task/application/service/docstorage" ) @@ -25,7 +25,7 @@ func New(config Config) docstorage.IDocumentStorage { } } -func (ds *DocSearch) StoreDocument(ctx context.Context, doc *docstorage.Document) (docstorage.DocumentID, error) { +func (ds *DocSearch) StoreDocument(ctx kernel.Ctx, doc *docstorage.Document) (docstorage.DocumentID, error) { index := doc.Index storeDoc := StoreDocumentForm{ FileName: doc.Name, @@ -42,9 +42,10 @@ func (ds *DocSearch) StoreDocument(ctx context.Context, doc *docstorage.Document return "", err } - buildURL := strings.Builder{} + urlPath := fmt.Sprintf("/api/v1/storage/%s/create?force=true", index) + buildURL := &strings.Builder{} buildURL.WriteString(ds.config.Address) - buildURL.WriteString(fmt.Sprintf("/storage/%s/create?force=true", index)) + buildURL.WriteString(urlPath) targetURL := buildURL.String() slog.Debug("storing document to index", From 6b2eb939e723eb10cbd3d6295953b390fea4511a Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:02:57 +0300 Subject: [PATCH 13/47] chore: migrate to fiber from echo --- cmd/watchtower/httpserver/form/form.go | 22 -- cmd/watchtower/httpserver/httpserver.go | 76 +++--- cmd/watchtower/httpserver/mw/logger.go | 118 ++++---- cmd/watchtower/httpserver/mw/tracer.go | 7 +- cmd/watchtower/httpserver/routes_bucket.go | 108 ++++---- cmd/watchtower/httpserver/routes_object.go | 297 +++++++++++---------- cmd/watchtower/httpserver/routes_system.go | 16 +- cmd/watchtower/httpserver/routes_task.go | 88 +++--- cmd/watchtower/watchtower.go | 12 +- go.mod | 69 +++-- go.sum | 194 ++++++++++++++ 11 files changed, 619 insertions(+), 388 deletions(-) diff --git a/cmd/watchtower/httpserver/form/form.go b/cmd/watchtower/httpserver/form/form.go index 1ef2497..5af89df 100644 --- a/cmd/watchtower/httpserver/form/form.go +++ b/cmd/watchtower/httpserver/form/form.go @@ -1,27 +1,5 @@ package form -func CreateStatusResponse(msg string) *ResponseForm { - return &ResponseForm{Status: 200, Message: msg} -} - -// ResponseForm example -type ResponseForm struct { - Status int `json:"status" example:"200"` - Message string `json:"message" example:"Done"` -} - -// BadRequestForm example -type BadRequestForm struct { - Status int `json:"status" example:"400"` - Message string `json:"message" example:"Bad Request message"` -} - -// ServerErrorForm example -type ServerErrorForm struct { - Status int `json:"status" example:"503"` - Message string `json:"message" example:"Server Error message"` -} - // AddDirectoryToWatcherForm example type AddDirectoryToWatcherForm struct { BucketName string `json:"bucket" example:"test-folder"` diff --git a/cmd/watchtower/httpserver/httpserver.go b/cmd/watchtower/httpserver/httpserver.go index b163810..ae64405 100644 --- a/cmd/watchtower/httpserver/httpserver.go +++ b/cmd/watchtower/httpserver/httpserver.go @@ -1,23 +1,24 @@ package httpserver import ( - "context" "fmt" - "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" "go.opentelemetry.io/otel/trace" - "github.com/labstack/echo-contrib/echoprometheus" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + "github.com/ansrivas/fiberprometheus/v2" + "github.com/gofiber/contrib/otelfiber/v2" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/monitor" + "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/gofiber/swagger" "watchtower/cmd/watchtower/httpserver/mw" "watchtower/internal/process" + "watchtower/internal/shared/kernel" "watchtower/internal/shared/telemetry" - docs "watchtower/docs" - - echoSwagger "github.com/swaggo/echo-swagger" + _ "watchtower/docs" ) // Server @@ -46,11 +47,11 @@ type Server struct { tracer trace.Tracer state *process.Orchestrator - server *echo.Echo + Server *fiber.App } func SetupServer( - config telemetry.OtlpConfig, + otlpConfig telemetry.OtlpConfig, state *process.Orchestrator, tracer trace.Tracer, ) *Server { @@ -59,56 +60,59 @@ func SetupServer( state: state, } - serverApp.server = echo.New() + serverApp.Server = fiber.New() - serverApp.server.Use(middleware.CORS()) - serverApp.server.Use(middleware.Recover()) + serverApp.Server.Use(cors.New(cors.Config{})) + serverApp.Server.Use(recover.New()) serverApp.initMeterMW() - serverApp.initTracerMW() - serverApp.initLoggerMW(config.Logger) + serverApp.initTracerMW(otlpConfig.Tracer) + serverApp.initLoggerMW(otlpConfig.Logger) + + serverApp.Server.Get("/monitor", monitor.New()) + + api := serverApp.Server.Group("/api") + api.Get("/swagger/*", swagger.HandlerDefault) - _ = serverApp.CreateSystemGroup() - _ = serverApp.CreateTasksGroup() - _ = serverApp.CreateStorageBucketsGroup() - _ = serverApp.CreateStorageObjectsGroup() + v1Api := api.Group("/v1") - docs.SwaggerInfo.BasePath = "/api/v1" - serverApp.server.GET("/api/swagger/*", echoSwagger.WrapHandler) + serverApp.CreateSystemGroup(v1Api) + serverApp.CreateTasksGroup(v1Api) + serverApp.CreateStorageBucketsGroup(v1Api) + serverApp.CreateStorageObjectsGroup(v1Api) return serverApp } -func (s *Server) Start(_ context.Context, config Config) error { - if err := s.server.Start(config.Address); err != nil { - return fmt.Errorf("failed to start server: %w", err) +func (s *Server) Start(_ kernel.Ctx, config Config) error { + if err := s.Server.Listen(config.Address); err != nil { + return fmt.Errorf("failed to start Server: %w", err) } return nil } -func (s *Server) Shutdown(ctx context.Context) error { - return s.server.Shutdown(ctx) +func (s *Server) Shutdown(_ kernel.Ctx) error { + return s.Server.Shutdown() } func (s *Server) initMeterMW() { - s.server.Use(echoprometheus.NewMiddleware(telemetry.AppName)) - s.server.GET("/metrics", echoprometheus.NewHandler()) + prometheus := fiberprometheus.New(telemetry.AppName) + prometheus.RegisterAt(s.Server, "/metrics") + prometheus.SetSkipPaths([]string{"/swagger"}) + prometheus.SetIgnoreStatusCodes([]int{401, 403, 404}) + s.Server.Use(prometheus.Middleware) } func (s *Server) initLoggerMW(logConfig telemetry.LoggerConfig) { if logConfig.EnableLoki { lokiLog := telemetry.InitLokiLogger(logConfig) - s.server.Use(mw.CreateLokiLoggerMW(&lokiLog)) + s.Server.Use(mw.CreateLokiLoggerMW(&lokiLog)) } else { - s.server.Use(mw.InitLocalLogger(logConfig)) + s.Server.Use(mw.InitLocalLogger(logConfig)) } } -func (s *Server) initTracerMW() { - s.server.Use(otelecho.Middleware( - telemetry.AppName, - otelecho.WithPropagators(telemetry.TracePropagator), - otelecho.WithSkipper(mw.TracerSkipper), - )) +func (s *Server) initTracerMW(_ telemetry.TracerConfig) { + s.Server.Use(otelfiber.Middleware()) } diff --git a/cmd/watchtower/httpserver/mw/logger.go b/cmd/watchtower/httpserver/mw/logger.go index e9fae84..a283458 100644 --- a/cmd/watchtower/httpserver/mw/logger.go +++ b/cmd/watchtower/httpserver/mw/logger.go @@ -9,73 +9,81 @@ import ( "strings" "time" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" "watchtower/internal/shared/telemetry" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" ) -func InitLocalLogger(config telemetry.LoggerConfig) echo.MiddlewareFunc { - logConfig := middleware.LoggerConfig{ - Skipper: func(c echo.Context) bool { - uri := c.Path() - return strings.Contains(uri, "swagger") +func InitLocalLogger(config telemetry.LoggerConfig) fiber.Handler { + return logger.New( + logger.Config{ + TimeFormat: "2006/01/02 15:04:05", + Format: fmt.Sprintf( + "%s %s http-response={%s %s %s %s %s}\n", + "${time_custom}", + config.Level, + "method=${method}", + "uri=${path}", + "latency=${latency}", + "status=${status}", + "error=\"${error}\"", + ), + Next: func(eCtx *fiber.Ctx) bool { + uri := eCtx.Request().URI().String() + excludeSwagger := strings.Contains(uri, "swagger") + excludeMetrics := strings.Contains(uri, "metrics") + return excludeSwagger || excludeMetrics + }, }, - CustomTimeFormat: "2006/01/02 15:04:05", - Format: fmt.Sprintf( - "%s %s http-response={%s %s %s %s %s}\n", - "${time_custom}", - config.Level, - "method=${method}", - "uri=${path}", - "latency=${latency}", - "status=${status}", - "error=\"${error}\"", - ), - } - - return middleware.LoggerWithConfig(logConfig) + ) } -func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(eCtx echo.Context) error { - if slices.Contains(sll.FilterURI, eCtx.Path()) { - return next(eCtx) - } - - start := time.Now() +func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { + return func(eCtx *fiber.Ctx) error { + if slices.Contains(sll.FilterURI, eCtx.Path()) { + return eCtx.Next() + } - err := next(eCtx) - if err != nil { - eCtx.Error(err) - } + start := time.Now() - latency := time.Since(start) + err := eCtx.Next() + if err != nil { + return err + } - logMessage := map[string]interface{}{ - "message": eCtx.Response().Status, - "latency": latency.String(), - "status": eCtx.Response().Status, - "method": eCtx.Request().Method, - "uri": eCtx.Path(), - "client_ip": eCtx.RealIP(), - "user_agent": eCtx.Request().UserAgent(), - } - jsonMessage, _ := json.Marshal(logMessage) + latency := time.Since(start) - var logLevel slog.Level - statusCategory := eCtx.Response().Status / 100 - if statusCategory < 3 && statusCategory >= 2 { - logLevel = slog.LevelInfo - } else { - logLevel = slog.LevelError - } + var responseMsg string + if eCtx.Response().StatusCode() >= 200 { + responseMsg = "Ok" + } else { + responseMsg = eCtx.Response().String() + } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - sll.Client.Log(ctx, logLevel, string(jsonMessage)) - defer cancel() + logMessage := map[string]interface{}{ + "message": responseMsg, + "latency": latency.String(), + "status": eCtx.Response().StatusCode(), + "method": eCtx.Method(), + "uri": eCtx.Path(), + "client_ip": eCtx.IP(), + "user_agent": eCtx.Request(), + } + jsonMessage, _ := json.Marshal(logMessage) - return err + var logLevel slog.Level + statusCategory := eCtx.Response().StatusCode() / 100 + if statusCategory < 3 && statusCategory >= 2 { + logLevel = slog.LevelInfo + } else { + logLevel = slog.LevelError } + + ctx, cancel := context.WithTimeout(eCtx.Context(), 5*time.Second) + sll.Client.Log(ctx, logLevel, string(jsonMessage)) + defer cancel() + + return err } } diff --git a/cmd/watchtower/httpserver/mw/tracer.go b/cmd/watchtower/httpserver/mw/tracer.go index 5c8d13e..17091d3 100644 --- a/cmd/watchtower/httpserver/mw/tracer.go +++ b/cmd/watchtower/httpserver/mw/tracer.go @@ -1,10 +1,9 @@ package mw import ( - "net/http" "strings" - "github.com/labstack/echo/v4" + "github.com/gofiber/fiber/v2" ) var ( @@ -17,12 +16,12 @@ var ( } ) -func TracerSkipper(eCtx echo.Context) bool { +func TracerSkipper(eCtx *fiber.Ctx) bool { for _, excluded := range excludedPaths { if strings.HasPrefix(eCtx.Path(), excluded) { return true } } - return eCtx.Request().Method == http.MethodOptions + return eCtx.Request().Header.IsOptions() } diff --git a/cmd/watchtower/httpserver/routes_bucket.go b/cmd/watchtower/httpserver/routes_bucket.go index c2ca158..7fffc25 100644 --- a/cmd/watchtower/httpserver/routes_bucket.go +++ b/cmd/watchtower/httpserver/routes_bucket.go @@ -2,21 +2,16 @@ package httpserver import ( "encoding/json" - "net/http" - "github.com/labstack/echo/v4" + "github.com/gofiber/fiber/v2" "watchtower/cmd/watchtower/httpserver/form" ) -func (s *Server) CreateStorageBucketsGroup() error { - group := s.server.Group("/api/v1/cloud") - - group.GET("/buckets", s.GetBuckets) - group.PUT("/bucket", s.CreateBucket) - group.DELETE("/:bucket", s.RemoveBucket) - - return nil +func (s *Server) CreateStorageBucketsGroup(group fiber.Router) { + group.Get("/cloud/buckets", s.GetBuckets) + group.Put("/cloud/bucket", s.CreateBucket) + group.Delete("/cloud/:bucket", s.RemoveBucket) } // GetBuckets @@ -25,14 +20,17 @@ func (s *Server) CreateStorageBucketsGroup() error { // @ID get-buckets // @Tags buckets // @Produce json -// @Success 200 {array} string "Ok" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/buckets [get] -func (s *Server) GetBuckets(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - buckets, err := s.state.GetObjectStorage().GetAllBuckets(ctx) +// @Success 200 {object} []form.BucketSchema "Loaded buckets info" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/buckets [get] +func (s *Server) GetBuckets(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + objStorage := s.state.GetObjectStorage() + buckets, err := objStorage.GetAllBuckets(ctx) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } bucketsDto := make([]form.BucketSchema, len(buckets)) @@ -40,7 +38,7 @@ func (s *Server) GetBuckets(eCtx echo.Context) error { bucketsDto[index] = form.BucketFromDomain(bucket) } - return eCtx.JSON(200, buckets) + return eCtx.Status(fiber.StatusOK).JSON(bucketsDto) } // CreateBucket @@ -51,37 +49,38 @@ func (s *Server) GetBuckets(eCtx echo.Context) error { // @Accept json // @Produce json // @Param jsonQuery body form.CreateBucketForm true "Bucket name to create" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/bucket [put] -func (s *Server) CreateBucket(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - - jsonForm := &form.CreateBucketForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - err := decoder.Decode(jsonForm) +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/bucket [put] +func (s *Server) CreateBucket(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + var jsonForm form.CreateBucketForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - exists, err := s.state.GetObjectStorage().IsBucketExists(ctx, jsonForm.BucketName) + objStorage := s.state.GetObjectStorage() + exists, err := objStorage.IsBucketExists(ctx, jsonForm.BucketName) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } if exists { // TODO: Temporary solution. Need to return 409 http error - //return echo.NewHTTPError(http.StatusConflict, "bucket already exists") - return echo.NewHTTPError(http.StatusOK, "bucket already exists") + // return eCtx.Status(fiber.StatusConflict).SendString("bucket already exists") + return eCtx.Status(fiber.StatusOK).SendString("bucket already exists") } - err = s.state.GetObjectStorage().CreateBucket(ctx, jsonForm.BucketName) + err = objStorage.CreateBucket(ctx, jsonForm.BucketName) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(201, form.CreateStatusResponse("Ok")) + return eCtx.Status(fiber.StatusCreated).SendString("Ok") } // RemoveBucket @@ -91,18 +90,33 @@ func (s *Server) CreateBucket(eCtx echo.Context) error { // @Tags buckets // @Produce json // @Param bucket path string true "Bucket name to remove" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket} [delete] -func (s *Server) RemoveBucket(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - - bucket := eCtx.Param("bucket") - err := s.state.GetObjectStorage().DeleteBucket(ctx, bucket) +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket} [delete] +func (s *Server) RemoveBucket(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + bucket := eCtx.Params("bucket") + + objStorage := s.state.GetObjectStorage() + exists, err := objStorage.IsBucketExists(ctx, bucket) + if err != nil { + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + + if !exists { + // TODO: Temporary solution. Need to return 409 http error + // return eCtx.Status(fiber.StatusConflict).SendString("bucket already exists") + return eCtx.Status(fiber.StatusNotFound).SendString("bucket already exists") + } + + err = objStorage.DeleteBucket(ctx, bucket) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.CreateStatusResponse("Ok")) + return eCtx.Status(fiber.StatusOK).SendString("Ok") } diff --git a/cmd/watchtower/httpserver/routes_object.go b/cmd/watchtower/httpserver/routes_object.go index 8022ec7..b09d0a1 100644 --- a/cmd/watchtower/httpserver/routes_object.go +++ b/cmd/watchtower/httpserver/routes_object.go @@ -11,24 +11,19 @@ import ( "watchtower/cmd/watchtower/httpserver/form" "watchtower/internal/core/cloud/domain" - "github.com/labstack/echo/v4" + "github.com/gofiber/fiber/v2" ) -func (s *Server) CreateStorageObjectsGroup() error { - group := s.server.Group("/api/v1/cloud") - - group.POST("/:bucket/files", s.GetFiles) - group.POST("/:bucket/file/copy", s.CopyFile) - group.POST("/:bucket/file/move", s.MoveFile) - group.PUT("/:bucket/file/upload", s.UploadFile) - group.POST("/:bucket/file/download", s.DownloadFile) - group.DELETE("/:bucket/file", s.RemoveFile2) - group.DELETE("/:bucket/file/remove", s.RemoveFile) - group.POST("/:bucket/file/attributes", s.GetFileInfo) - - group.POST("/:bucket/file/share", s.ShareFile) - - return nil +func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { + group.Post("/cloud/:bucket/files", s.GetFiles) + group.Post("/cloud/:bucket/file/copy", s.CopyFile) + group.Post("/cloud/:bucket/file/move", s.MoveFile) + group.Put("/cloud/:bucket/file/upload", s.UploadFile) + group.Post("/cloud/:bucket/file/download", s.DownloadFile) + group.Delete("/cloud/:bucket/file", s.RemoveFile2) + group.Delete("/cloud/:bucket/file/remove", s.RemoveFile) + group.Post("/cloud/:bucket/file/attributes", s.GetFileInfo) + group.Post("/cloud/:bucket/file/share", s.ShareFile) } // CopyFile @@ -40,19 +35,20 @@ func (s *Server) CreateStorageObjectsGroup() error { // @Produce json // @Param bucket path string true "Bucket name of src file" // @Param jsonQuery body form.CopyFileForm true "Params to copy file" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/copy [post] -func (s *Server) CopyFile(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - - jsonForm := &form.CopyFileForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - err := decoder.Decode(jsonForm) +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket or file not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file/copy [post] +func (s *Server) CopyFile(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + + var jsonForm form.CopyFileForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } params := &domain.CopyObjectParams{ @@ -62,10 +58,10 @@ func (s *Server) CopyFile(eCtx echo.Context) error { err = s.state.GetObjectStorage().CopyObject(ctx, bucket, params) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.CreateStatusResponse("Ok")) + return eCtx.Status(fiber.StatusOK).SendString("Ok") } // MoveFile @@ -77,19 +73,20 @@ func (s *Server) CopyFile(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket name of src file" // @Param jsonQuery body form.CopyFileForm true "Params to move file" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/move [post] -func (s *Server) MoveFile(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - - jsonForm := &form.CopyFileForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - err := decoder.Decode(jsonForm) +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket or file not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file/move [post] +func (s *Server) MoveFile(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + + var jsonForm form.CopyFileForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } params := &domain.CopyObjectParams{ @@ -99,10 +96,10 @@ func (s *Server) MoveFile(eCtx echo.Context) error { err = s.state.GetObjectStorage().MoveObject(ctx, bucket, params) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.CreateStatusResponse("Ok")) + return eCtx.Status(fiber.StatusOK).SendString("Ok") } // UploadFile @@ -115,33 +112,39 @@ func (s *Server) MoveFile(eCtx echo.Context) error { // @Param bucket path string true "Bucket name to upload files" // @Param files formData file true "Files multipart form" // @Param expired query string false "File datetime expired like 2025-01-01T12:01:01Z" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/upload [put] -func (s *Server) UploadFile(eCtx echo.Context) error { - ctx := eCtx.Request().Context() +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file/upload [put] +func (s *Server) UploadFile(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() var fileData bytes.Buffer multipartForm, err := eCtx.MultipartForm() if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + bucket := eCtx.Params("bucket") + exist, err := s.state.GetObjectStorage().IsBucketExists(ctx, bucket) + if err != nil { + return eCtx.Status(http.StatusBadRequest).SendString(err.Error()) } - bucket := eCtx.Param("bucket") - exist, err := s.state.GetObjectStorage().IsBucketExists(eCtx.Request().Context(), bucket) - if err != nil || !exist { - retErr := fmt.Errorf("specified bucket %s does not exist", bucket) - return echo.NewHTTPError(http.StatusBadRequest, retErr.Error()) + if !exist { + err = fmt.Errorf("specified bucket %s does not exist", bucket) + return eCtx.Status(http.StatusNotFound).SendString(err.Error()) } if multipartForm.File["files"] == nil { err = fmt.Errorf("there are no files into multipart form") - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - expired := eCtx.QueryParam("expired") + expired := eCtx.Query("expired") timeVal, timeParseErr := time.Parse(time.RFC3339, expired) if timeParseErr != nil { slog.Warn("failed to parse expired time param", @@ -195,7 +198,7 @@ func (s *Server) UploadFile(eCtx echo.Context) error { uploadedFiles[index] = form.TaskFromDomain(*task) } - return eCtx.JSON(200, uploadedFiles) + return eCtx.Status(fiber.StatusOK).JSON(uploadedFiles) } // DownloadFile @@ -207,27 +210,29 @@ func (s *Server) UploadFile(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket name to download file" // @Param jsonQuery body form.DownloadFileForm true "Parameters to download file" -// @Success 200 {file} io.Writer "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/download [post] -func (s *Server) DownloadFile(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - - jsonForm := &form.DownloadFileForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - if err := decoder.Decode(jsonForm); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) +// @Success 200 {file} io.Writer "Returned file bytes" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file/download [post] +func (s *Server) DownloadFile(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + + var jsonForm form.DownloadFileForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) + if err != nil { + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } fileData, err := s.state.GetObjectStorage().GetObjectData(ctx, bucket, jsonForm.FileName) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } defer fileData.Reset() - return eCtx.Blob(200, echo.MIMEMultipartForm, fileData.Bytes()) + return eCtx.Send(fileData.Bytes()) } // RemoveFile @@ -238,25 +243,27 @@ func (s *Server) DownloadFile(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket name to remove file" // @Param jsonQuery body form.RemoveFileForm true "Parameters to remove file" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/remove [delete] -func (s *Server) RemoveFile(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - - jsonForm := &form.RemoveFileForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - if err := decoder.Decode(jsonForm); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file/remove [delete] +func (s *Server) RemoveFile(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + + var jsonForm form.RemoveFileForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) + if err != nil { + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } if err := s.state.GetObjectStorage().DeleteObject(ctx, bucket, jsonForm.FileName); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.CreateStatusResponse("Ok")) + return eCtx.Status(fiber.StatusOK).SendString("Ok") } // RemoveFile2 @@ -267,19 +274,21 @@ func (s *Server) RemoveFile(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket name to remove file" // @Param file_name query string true "Parameters to remove file" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file [delete] -func (s *Server) RemoveFile2(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - fileName := eCtx.QueryParam("file_name") +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file [delete] +func (s *Server) RemoveFile2(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + fileName := eCtx.Query("file_name") if err := s.state.GetObjectStorage().DeleteObject(ctx, bucket, fileName); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.CreateStatusResponse("Ok")) + return eCtx.Status(fiber.StatusOK).SendString("Ok") } // GetFiles @@ -291,28 +300,29 @@ func (s *Server) RemoveFile2(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket name to get list files" // @Param jsonQuery body form.GetFilesForm true "Parameters to get list files" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/files [post] -func (s *Server) GetFiles(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - - jsonForm := &form.GetFilesForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - err := decoder.Decode(jsonForm) +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/files [post] +func (s *Server) GetFiles(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + + var jsonForm form.GetFilesForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } params := &domain.GetObjectsParams{ PrefixPath: jsonForm.DirectoryName, } - listObjects, err := s.state.GetObjectStorage().GetBucketObjects(ctx, bucket, params) + listObjects, err := s.state.GetObjectStorage().LoadBucketObjects(ctx, bucket, params) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } objectsDto := make([]form.ObjectSchema, len(listObjects)) @@ -320,7 +330,7 @@ func (s *Server) GetFiles(eCtx echo.Context) error { objectsDto[index] = form.ObjectFromDomain(object) } - return eCtx.JSON(200, objectsDto) + return eCtx.Status(fiber.StatusOK).JSON(objectsDto) } // GetFileInfo @@ -332,27 +342,29 @@ func (s *Server) GetFiles(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket name to get list files" // @Param jsonQuery body form.GetFileAttributesForm true "Parameters to get list files" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/attributes [post] -func (s *Server) GetFileInfo(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - - jsonForm := &form.GetFileAttributesForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - err := decoder.Decode(jsonForm) +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket or Object not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file/attributes [post] +func (s *Server) GetFileInfo(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + + var jsonForm form.GetFileAttributesForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } object, err := s.state.GetObjectStorage().GetObjectInfo(ctx, bucket, jsonForm.FilePath) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.ObjectFromDomain(*object)) + objectSchema := form.ObjectFromDomain(*object) + return eCtx.Status(fiber.StatusOK).JSON(objectSchema) } // ShareFile @@ -364,27 +376,28 @@ func (s *Server) GetFileInfo(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket name to share file" // @Param jsonQuery body form.ShareFileForm true "Parameters to share file" -// @Success 200 {object} form.ResponseForm "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /cloud/{bucket}/file/share [post] -func (s *Server) ShareFile(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") - - shareForm := &form.ShareFileForm{} - decoder := json.NewDecoder(eCtx.Request().Body) - err := decoder.Decode(shareForm) +// @Success 200 {object} form.Success "URL with shared file" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket of object not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file/share [post] +func (s *Server) ShareFile(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + bucket := eCtx.Params("bucket") + + var jsonForm form.ShareFileForm + err := json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - expired := time.Second * time.Duration(shareForm.ExpiredSecs) - params := &domain.ShareObjectParams{FilePath: shareForm.FilePath, Expired: expired} + expired := time.Second * time.Duration(jsonForm.ExpiredSecs) + params := &domain.ShareObjectParams{FilePath: jsonForm.FilePath, Expired: expired} url, err := s.state.GetObjectStorage().GenShareURL(ctx, bucket, params) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.CreateStatusResponse(url)) + return eCtx.Status(fiber.StatusOK).JSON(form.SuccessResponse(url)) } diff --git a/cmd/watchtower/httpserver/routes_system.go b/cmd/watchtower/httpserver/routes_system.go index 398ced8..ec05cc5 100644 --- a/cmd/watchtower/httpserver/routes_system.go +++ b/cmd/watchtower/httpserver/routes_system.go @@ -1,21 +1,21 @@ package httpserver import ( - "net/http" "os" - "github.com/labstack/echo/v4" + "github.com/gofiber/fiber/v2" ) -func (s *Server) CreateSystemGroup() error { - s.server.GET("/", s.Home) - return nil +func (s *Server) CreateSystemGroup(group fiber.Router) { + group.Get("/", s.Home) } -func (s *Server) Home(eCtx echo.Context) error { +func (s *Server) Home(eCtx *fiber.Ctx) error { fileData, err := os.ReadFile("./static/index.html") if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return eCtx.SendStatus(fiber.StatusInternalServerError) } - return eCtx.HTMLBlob(http.StatusOK, fileData) + + eCtx.Set(fiber.HeaderContentType, fiber.MIMETextHTML) + return eCtx.SendString(string(fileData)) } diff --git a/cmd/watchtower/httpserver/routes_task.go b/cmd/watchtower/httpserver/routes_task.go index 8c59638..7f0aaa5 100644 --- a/cmd/watchtower/httpserver/routes_task.go +++ b/cmd/watchtower/httpserver/routes_task.go @@ -1,25 +1,21 @@ package httpserver import ( - "net/http" "strconv" "golang.org/x/exp/slices" + "github.com/gofiber/fiber/v2" "github.com/google/uuid" - "github.com/labstack/echo/v4" "watchtower/cmd/watchtower/httpserver/form" + task "watchtower/internal/support/task/domain" ) -func (s *Server) CreateTasksGroup() error { - group := s.server.Group("/api/v1/tasks") - - group.GET("/:bucket", s.LoadTasks) - group.GET("/:bucket/:task_id", s.LoadTaskByID) - - return nil +func (s *Server) CreateTasksGroup(group fiber.Router) { + group.Get("/tasks/:bucket", s.LoadTasks) + group.Get("/tasks/:bucket/:task_id", s.LoadTaskByID) } // LoadTasks @@ -31,30 +27,30 @@ func (s *Server) CreateTasksGroup() error { // @Produce json // @Param bucket path string true "Bucket id of uploaded files" // @Param status query string false "Status tasks to filter target result" -// @Success 200 {object} []form.TaskSchema "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /tasks/{bucket} [get] -func (s *Server) LoadTasks(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") +// @Success 200 {object} []form.TaskSchema "Loaded tasks" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/tasks/{bucket} [get] +func (s *Server) LoadTasks(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + bucket := eCtx.Params("bucket") if bucket == "" { - return echo.NewHTTPError(http.StatusBadRequest, "bucket is required") + return eCtx.Status(fiber.StatusBadRequest).SendString("bucket is required") } - tasks, err := s.state.GetTaskProcessor().GetBucketTasks(ctx, bucket) + status := eCtx.Query("status") + inputTaskStatus, err := strconv.Atoi(status) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString("unknown status") } - status := eCtx.QueryParam("status") - if status == "" { - return eCtx.JSON(200, tasks) - } - - inputTaskStatus, err := strconv.Atoi(status) + taskStorage := s.state.GetTaskProcessor() + tasks, err := taskStorage.GetBucketTasks(ctx, bucket) if err != nil { - return echo.NewHTTPError(http.StatusUnprocessableEntity, "unknown status") + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } taskStatus := task.TaskStatus(inputTaskStatus) @@ -63,11 +59,11 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { }) foundedTasksDto := make([]form.TaskSchema, len(foundedTasks)) - for index, task := range foundedTasks { - foundedTasksDto[index] = form.TaskFromDomain(*task) + for index, taskIt := range foundedTasks { + foundedTasksDto[index] = form.TaskFromDomain(*taskIt) } - return eCtx.JSON(200, foundedTasksDto) + return eCtx.Status(fiber.StatusOK).JSON(foundedTasksDto) } // LoadTaskByID @@ -79,31 +75,37 @@ func (s *Server) LoadTasks(eCtx echo.Context) error { // @Produce json // @Param bucket path string true "Bucket id of processing task" // @Param task_id path string true "Task ID" -// @Success 200 {object} form.TaskSchema "Ok" -// @Failure 400 {object} form.BadRequestForm "Bad Request message" -// @Failure 503 {object} form.ServerErrorForm "Server does not available" -// @Router /tasks/{bucket}/{task_id} [get] -func (s *Server) LoadTaskByID(eCtx echo.Context) error { - ctx := eCtx.Request().Context() - bucket := eCtx.Param("bucket") +// @Success 200 {object} form.TaskSchema "Loaded tasks" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Task not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/tasks/{bucket}/{task_id} [get] +func (s *Server) LoadTaskByID(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + bucket := eCtx.Params("bucket") if bucket == "" { - return echo.NewHTTPError(http.StatusBadRequest, "bucket is required") + return eCtx.Status(fiber.StatusBadRequest).SendString("bucket is required") } - taskIDParam := eCtx.Param("task_id") + taskIDParam := eCtx.Params("task_id") if taskIDParam == "" { - return echo.NewHTTPError(http.StatusBadRequest, "task_id is required") + return eCtx.Status(fiber.StatusBadRequest).SendString("task_id is required") } taskID, err := uuid.Parse(taskIDParam) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - task, err := s.state.GetTaskProcessor().GetTask(ctx, bucket, taskID) + taskStorage := s.state.GetTaskProcessor() + foundedTask, err := taskStorage.GetTask(ctx, bucket, taskID) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - return eCtx.JSON(200, form.TaskFromDomain(*task)) + taskSchema := form.TaskFromDomain(*foundedTask) + + return eCtx.Status(fiber.StatusOK).JSON(taskSchema) } diff --git a/cmd/watchtower/watchtower.go b/cmd/watchtower/watchtower.go index 49e3e35..d3078f8 100644 --- a/cmd/watchtower/watchtower.go +++ b/cmd/watchtower/watchtower.go @@ -23,7 +23,6 @@ import ( ) func main() { - ctx := context.Background() servConfig := cmd.Execute() traceProvider, err := telemetry.InitTracer(servConfig.Otlp.Tracer) @@ -31,6 +30,8 @@ func main() { slog.Warn("failed to init tracer", slog.String("err", err.Error())) } + ctx := context.Background() + taskStorage := redis.New(servConfig.Task.TaskStorage.Redis) taskQueue, err := rmq.New(servConfig.Task.TaskQueue.Rmq) if err != nil { @@ -38,14 +39,16 @@ func main() { } err = taskQueue.StartConsuming(ctx) if err != nil { - log.Fatalf("failed to launch task queue consumer: %v", err) + slog.Error("failed to launch task queue consumer", slog.String("err", err.Error())) + os.Exit(1) } docParser := docparser.New(servConfig.Task.Processor.DocParser) docStorage := docsearch.New(servConfig.Task.Processor.DocStorage) objStorage, err := s3.New(servConfig.Storage.S3) if err != nil { - log.Fatalf("object storage connection failed: %v", err) + slog.Error("object storage connection failed", slog.String("err", err.Error())) + os.Exit(1) } cCtx, cancel := context.WithCancel(ctx) @@ -58,7 +61,8 @@ func main() { httpServer := httpserver.SetupServer(servConfig.Otlp, orchestrator, traceProvider) go func() { if err := httpServer.Start(cCtx, servConfig.Server.Http); err != nil { - log.Fatalf("http server start failed: %v", err) + slog.Error("http server start failed", slog.String("err", err.Error())) + os.Exit(1) } }() diff --git a/go.mod b/go.mod index 84e1bc2..39e0653 100644 --- a/go.mod +++ b/go.mod @@ -17,21 +17,24 @@ require ( github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.6 go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 - go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/trace v1.42.0 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 - golang.org/x/sync v0.18.0 + golang.org/x/sync v0.20.0 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/ansrivas/fiberprometheus/v2 v2.17.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -40,28 +43,33 @@ require ( github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.22.1 // indirect - github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/spec v0.22.0 // indirect - github.com/go-openapi/swag/conv v0.25.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect - github.com/go-openapi/swag/jsonutils v0.25.1 // indirect - github.com/go-openapi/swag/loading v0.25.1 // indirect - github.com/go-openapi/swag/stringutils v0.25.1 // indirect - github.com/go-openapi/swag/typeutils v0.25.1 // indirect - github.com/go-openapi/swag/yamlutils v0.25.1 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/gofiber/contrib/otelfiber v1.0.10 // indirect + github.com/gofiber/contrib/otelfiber/v2 v2.2.3 // indirect + github.com/gofiber/fiber/v2 v2.52.12 // indirect + github.com/gofiber/swagger v1.1.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -69,10 +77,11 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -85,26 +94,32 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/swaggo/fiber-swagger v1.3.0 // indirect + github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect github.com/tinylib/msgp v1.3.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/grpc v1.75.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 7bb8984..32fc04c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,16 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c h1:AMDVOKGaiqse4qiRXSzRgpC9DCNTHCx6zpzdtXXrKM4= github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c/go.mod h1:p/7Wos+jcfrnwLqqzJMZ0s323kfVtJPW+HUvAANklVQ= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/ansrivas/fiberprometheus/v2 v2.17.0 h1:p0gqs5LsSCWGoSFF44fCJkyU+XcE6TLRqEMu80b2iCo= +github.com/ansrivas/fiberprometheus/v2 v2.17.0/go.mod h1:giWBvbFSHOHG8N2wjYhQG23oc/2pF9v1mN8CdZs5Z2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -12,7 +21,11 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -34,31 +47,67 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofiber/contrib/otelfiber v1.0.10 h1:Bu28Pi4pfYmGfIc/9+sNaBbFwTHGY/zpSIK5jBxuRtM= +github.com/gofiber/contrib/otelfiber v1.0.10/go.mod h1:jN6AvS1HolDHTQHFURsV+7jSX96FpXYeKH6nmkq8AIw= +github.com/gofiber/contrib/otelfiber/v2 v2.2.3 h1:WKW1XezHFAoohGZwnvC0R8TFJcNkabQwB5YIpdKmz00= +github.com/gofiber/contrib/otelfiber/v2 v2.2.3/go.mod h1:WdQ1tYbL83IYC6oBaWvKBMVGSAYvSTRuUWTcr0wK1T4= +github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY= +github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= +github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA= +github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -67,6 +116,7 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -75,13 +125,21 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -94,10 +152,15 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= @@ -108,6 +171,12 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= @@ -117,20 +186,29 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -142,6 +220,9 @@ github.com/samber/slog-common v0.11.0 h1:JdESCaXcEwdtoTCYHKQFfHGbWN2vZJq0DDGEE/l github.com/samber/slog-common v0.11.0/go.mod h1:Qjrfhwk79XiCIhBj8+jTq1Cr0u9rlWbjawh3dWXzaHk= github.com/samber/slog-loki/v2 v2.2.0 h1:Urh35FxmWxmHjDiqIVkxT98BtzdCu974rGOsgKRg9h8= github.com/samber/slog-loki/v2 v2.2.0/go.mod h1:ooEVylnfKGIB+3zJaaG3y/YcbuGPJAaE3RY9VWXXBEI= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -160,6 +241,9 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -170,63 +254,166 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= +github.com/swaggo/fiber-swagger v1.3.0 h1:RMjIVDleQodNVdKuu7GRs25Eq8RVXK7MwY9f5jbobNg= +github.com/swaggo/fiber-swagger v1.3.0/go.mod h1:18MuDqBkYEiUmeM/cAAB8CI28Bi62d/mys39j1QqF9w= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib v1.42.0 h1:845qj52z2T/bLInfZmG8AdbTO7delSd6eGVVHcAikzw= +go.opentelemetry.io/contrib v1.42.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 h1:6YeICKmGrvgJ5th4+OMNpcuoB6q/Xs8gt0YCO7MUv1k= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0/go.mod h1:ZEA7j2B35siNV0T00aapacNzjz4tvOlNoHp0ncCfwNQ= go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo= go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= @@ -237,13 +424,20 @@ google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 9a68aebdf4436475393de2367d82a724fc70fe9b Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:03:12 +0300 Subject: [PATCH 14/47] chore: replaced flag from config to dotenv enabling --- cmd/root.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 34a95c2..f9af41d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,13 +2,14 @@ package cmd import ( "log" + "log/slog" "os" + "github.com/joho/godotenv" "github.com/spf13/cobra" - "watchtower/cmd/watchtower/config" ) -var serviceConfig *config.Config +var serviceConfig *Config // rootCmd represents the base command when called without any subcommands. var rootCmd = &cobra.Command{ @@ -20,17 +21,25 @@ var rootCmd = &cobra.Command{ Run: func(cmd *cobra.Command, _ []string) { var parseErr error - filePath, _ := cmd.Flags().GetString("config") - serviceConfig, parseErr = config.FromFile(filePath) + + dotEnvEnabled, err := cmd.Flags().GetBool("dotenv") + if err == nil && dotEnvEnabled { + err = godotenv.Load(".env") + if err != nil { + slog.Warn("failed to load .env file") + } + } + + serviceConfig, parseErr = InitConfig() if parseErr != nil { - log.Fatal(parseErr) + log.Fatalf("launch failed: %s", parseErr.Error()) } }, } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() *config.Config { +func Execute() *Config { err := rootCmd.Execute() if err != nil { os.Exit(1) @@ -40,5 +49,5 @@ func Execute() *config.Config { func init() { flags := rootCmd.Flags() - flags.StringP("config", "c", "./configs/config.toml", "Parse options from config file.") + flags.BoolP("dotenv", "d", false, "load environment vars using dotenv") } From 22baba582f49a2421a8113a3cdf32a7c449b8e09 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:03:23 +0300 Subject: [PATCH 15/47] chore: updated swagger docs after all changes --- docs/docs.go | 360 ++++++++++++++++++++++++++++++++++++---------- docs/swagger.json | 360 ++++++++++++++++++++++++++++++++++++---------- docs/swagger.yaml | 293 +++++++++++++++++++++++++++---------- 3 files changed, 776 insertions(+), 237 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index aa9d2c6..c14b307 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,7 +15,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/cloud/bucket": { + "/api/v1/cloud/bucket": { "put": { "description": "Create new bucket into cloud", "consumes": [ @@ -44,25 +44,31 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/buckets": { + "/api/v1/cloud/buckets": { "get": { "description": "Get watched bucket list", "produces": [ @@ -75,24 +81,36 @@ const docTemplate = `{ "operationId": "get-buckets", "responses": { "200": { - "description": "Ok", + "description": "Loaded buckets info", "schema": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/form.BucketSchema" } } }, + "400": { + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" + } + }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}": { + "/api/v1/cloud/{bucket}": { "delete": { "description": "Remove bucket from cloud", "produces": [ @@ -116,25 +134,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file": { + "/api/v1/cloud/{bucket}/file": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -165,25 +195,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/attributes": { + "/api/v1/cloud/{bucket}/file/attributes": { "post": { "description": "Get file attributes", "consumes": [ @@ -219,25 +261,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket or Object not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/copy": { + "/api/v1/cloud/{bucket}/file/copy": { "post": { "description": "Copy file to another location into bucket", "consumes": [ @@ -273,25 +327,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket or file not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/download": { + "/api/v1/cloud/{bucket}/file/download": { "post": { "description": "Download file from cloud", "consumes": [ @@ -325,27 +391,39 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Ok", + "description": "Returned file bytes", "schema": { "type": "file" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/move": { + "/api/v1/cloud/{bucket}/file/move": { "post": { "description": "Move file to another location into bucket", "consumes": [ @@ -381,25 +459,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket or file not found", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/remove": { + "/api/v1/cloud/{bucket}/file/remove": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -432,25 +522,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/share": { + "/api/v1/cloud/{bucket}/file/share": { "post": { "description": "Get share URL for file", "consumes": [ @@ -484,27 +586,39 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Ok", + "description": "URL with shared file", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket of object not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/upload": { + "/api/v1/cloud/{bucket}/file/upload": { "put": { "description": "Upload files to cloud", "consumes": [ @@ -544,25 +658,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/files": { + "/api/v1/cloud/{bucket}/files": { "post": { "description": "Get files list into bucket", "consumes": [ @@ -598,25 +724,37 @@ const docTemplate = `{ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/tasks/{bucket}": { + "/api/v1/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", "consumes": [ @@ -647,7 +785,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Ok", + "description": "Loaded tasks", "schema": { "type": "array", "items": { @@ -656,21 +794,33 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/tasks/{bucket}/{task_id}": { + "/api/v1/tasks/{bucket}/{task_id}": { "get": { "description": "Load processing/unrecognized/done task by id of uploaded file", "consumes": [ @@ -702,21 +852,33 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Ok", + "description": "Loaded tasks", "schema": { "$ref": "#/definitions/form.TaskSchema" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Task not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } @@ -724,7 +886,7 @@ const docTemplate = `{ } }, "definitions": { - "form.BadRequestForm": { + "form.BadRequestError": { "type": "object", "properties": { "message": { @@ -737,6 +899,20 @@ const docTemplate = `{ } } }, + "form.BucketSchema": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, "form.CopyFileForm": { "type": "object", "properties": { @@ -786,34 +962,47 @@ const docTemplate = `{ } } }, - "form.RemoveFileForm": { + "form.InternalServerError": { "type": "object", "properties": { - "file_name": { + "message": { "type": "string", - "example": "test-file.docx" + "example": "Internal server error message" + }, + "status": { + "type": "integer", + "example": 500 } } }, - "form.ResponseForm": { + "form.NotFoundError": { "type": "object", "properties": { "message": { "type": "string", - "example": "Done" + "example": "Not found" }, "status": { "type": "integer", - "example": 200 + "example": 404 + } + } + }, + "form.RemoveFileForm": { + "type": "object", + "properties": { + "file_name": { + "type": "string", + "example": "test-file.docx" } } }, - "form.ServerErrorForm": { + "form.ServerUnavailableError": { "type": "object", "properties": { "message": { "type": "string", - "example": "Server Error message" + "example": "Server unavailable error message" }, "status": { "type": "integer", @@ -834,6 +1023,19 @@ const docTemplate = `{ } } }, + "form.Success": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Done" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, "form.TaskSchema": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 6171313..e566c73 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4,7 +4,7 @@ "contact": {} }, "paths": { - "/cloud/bucket": { + "/api/v1/cloud/bucket": { "put": { "description": "Create new bucket into cloud", "consumes": [ @@ -33,25 +33,31 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/buckets": { + "/api/v1/cloud/buckets": { "get": { "description": "Get watched bucket list", "produces": [ @@ -64,24 +70,36 @@ "operationId": "get-buckets", "responses": { "200": { - "description": "Ok", + "description": "Loaded buckets info", "schema": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/form.BucketSchema" } } }, + "400": { + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" + } + }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}": { + "/api/v1/cloud/{bucket}": { "delete": { "description": "Remove bucket from cloud", "produces": [ @@ -105,25 +123,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file": { + "/api/v1/cloud/{bucket}/file": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -154,25 +184,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/attributes": { + "/api/v1/cloud/{bucket}/file/attributes": { "post": { "description": "Get file attributes", "consumes": [ @@ -208,25 +250,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket or Object not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/copy": { + "/api/v1/cloud/{bucket}/file/copy": { "post": { "description": "Copy file to another location into bucket", "consumes": [ @@ -262,25 +316,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket or file not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/download": { + "/api/v1/cloud/{bucket}/file/download": { "post": { "description": "Download file from cloud", "consumes": [ @@ -314,27 +380,39 @@ ], "responses": { "200": { - "description": "Ok", + "description": "Returned file bytes", "schema": { "type": "file" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/move": { + "/api/v1/cloud/{bucket}/file/move": { "post": { "description": "Move file to another location into bucket", "consumes": [ @@ -370,25 +448,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket or file not found", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/remove": { + "/api/v1/cloud/{bucket}/file/remove": { "delete": { "description": "Remove file from cloud", "produces": [ @@ -421,25 +511,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/share": { + "/api/v1/cloud/{bucket}/file/share": { "post": { "description": "Get share URL for file", "consumes": [ @@ -473,27 +575,39 @@ ], "responses": { "200": { - "description": "Ok", + "description": "URL with shared file", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket of object not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/file/upload": { + "/api/v1/cloud/{bucket}/file/upload": { "put": { "description": "Upload files to cloud", "consumes": [ @@ -533,25 +647,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/cloud/{bucket}/files": { + "/api/v1/cloud/{bucket}/files": { "post": { "description": "Get files list into bucket", "consumes": [ @@ -587,25 +713,37 @@ "200": { "description": "Ok", "schema": { - "$ref": "#/definitions/form.ResponseForm" + "$ref": "#/definitions/form.Success" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/tasks/{bucket}": { + "/api/v1/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", "consumes": [ @@ -636,7 +774,7 @@ ], "responses": { "200": { - "description": "Ok", + "description": "Loaded tasks", "schema": { "type": "array", "items": { @@ -645,21 +783,33 @@ } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } } }, - "/tasks/{bucket}/{task_id}": { + "/api/v1/tasks/{bucket}/{task_id}": { "get": { "description": "Load processing/unrecognized/done task by id of uploaded file", "consumes": [ @@ -691,21 +841,33 @@ ], "responses": { "200": { - "description": "Ok", + "description": "Loaded tasks", "schema": { "$ref": "#/definitions/form.TaskSchema" } }, "400": { - "description": "Bad Request message", + "description": "Bad Request error", "schema": { - "$ref": "#/definitions/form.BadRequestForm" + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Task not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" } }, "503": { "description": "Server does not available", "schema": { - "$ref": "#/definitions/form.ServerErrorForm" + "$ref": "#/definitions/form.ServerUnavailableError" } } } @@ -713,7 +875,7 @@ } }, "definitions": { - "form.BadRequestForm": { + "form.BadRequestError": { "type": "object", "properties": { "message": { @@ -726,6 +888,20 @@ } } }, + "form.BucketSchema": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "path": { + "type": "string" + } + } + }, "form.CopyFileForm": { "type": "object", "properties": { @@ -775,34 +951,47 @@ } } }, - "form.RemoveFileForm": { + "form.InternalServerError": { "type": "object", "properties": { - "file_name": { + "message": { "type": "string", - "example": "test-file.docx" + "example": "Internal server error message" + }, + "status": { + "type": "integer", + "example": 500 } } }, - "form.ResponseForm": { + "form.NotFoundError": { "type": "object", "properties": { "message": { "type": "string", - "example": "Done" + "example": "Not found" }, "status": { "type": "integer", - "example": 200 + "example": 404 + } + } + }, + "form.RemoveFileForm": { + "type": "object", + "properties": { + "file_name": { + "type": "string", + "example": "test-file.docx" } } }, - "form.ServerErrorForm": { + "form.ServerUnavailableError": { "type": "object", "properties": { "message": { "type": "string", - "example": "Server Error message" + "example": "Server unavailable error message" }, "status": { "type": "integer", @@ -823,6 +1012,19 @@ } } }, + "form.Success": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Done" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, "form.TaskSchema": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6cfbcd9..c2c943b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,5 +1,5 @@ definitions: - form.BadRequestForm: + form.BadRequestError: properties: message: example: Bad Request message @@ -8,6 +8,15 @@ definitions: example: 400 type: integer type: object + form.BucketSchema: + properties: + created_at: + type: string + id: + type: string + path: + type: string + type: object form.CopyFileForm: properties: dst_path: @@ -41,25 +50,34 @@ definitions: example: test-folder/ type: string type: object - form.RemoveFileForm: + form.InternalServerError: properties: - file_name: - example: test-file.docx + message: + example: Internal server error message type: string + status: + example: 500 + type: integer type: object - form.ResponseForm: + form.NotFoundError: properties: message: - example: Done + example: Not found type: string status: - example: 200 + example: 404 type: integer type: object - form.ServerErrorForm: + form.RemoveFileForm: + properties: + file_name: + example: test-file.docx + type: string + type: object + form.ServerUnavailableError: properties: message: - example: Server Error message + example: Server unavailable error message type: string status: example: 503 @@ -74,6 +92,15 @@ definitions: example: test-file.docx type: string type: object + form.Success: + properties: + message: + example: Done + type: string + status: + example: 200 + type: integer + type: object form.TaskSchema: properties: bucket_id: @@ -96,7 +123,7 @@ definitions: info: contact: {} paths: - /cloud/{bucket}: + /api/v1/cloud/{bucket}: delete: description: Remove bucket from cloud operationId: remove-bucket @@ -112,19 +139,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Remove bucket from cloud tags: - buckets - /cloud/{bucket}/file: + /api/v1/cloud/{bucket}/file: delete: description: Remove file from cloud operationId: remove-file-2 @@ -145,19 +180,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Remove file from cloud tags: - files - /cloud/{bucket}/file/attributes: + /api/v1/cloud/{bucket}/file/attributes: post: consumes: - application/json @@ -181,19 +224,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket or Object not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Get file attributes tags: - files - /cloud/{bucket}/file/copy: + /api/v1/cloud/{bucket}/file/copy: post: consumes: - application/json @@ -217,19 +268,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket or file not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Copy file to another location into bucket tags: - files - /cloud/{bucket}/file/download: + /api/v1/cloud/{bucket}/file/download: post: consumes: - application/json @@ -251,21 +310,29 @@ paths: - application/json responses: "200": - description: Ok + description: Returned file bytes schema: type: file "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Download file from cloud tags: - files - /cloud/{bucket}/file/move: + /api/v1/cloud/{bucket}/file/move: post: consumes: - application/json @@ -289,19 +356,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket or file not found schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Move file to another location into bucket tags: - files - /cloud/{bucket}/file/remove: + /api/v1/cloud/{bucket}/file/remove: delete: description: Remove file from cloud operationId: remove-file @@ -323,19 +398,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Remove file from cloud tags: - files - /cloud/{bucket}/file/share: + /api/v1/cloud/{bucket}/file/share: post: consumes: - application/json @@ -357,21 +440,29 @@ paths: - application/json responses: "200": - description: Ok + description: URL with shared file schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket of object not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Get share URL for file tags: - share - /cloud/{bucket}/file/upload: + /api/v1/cloud/{bucket}/file/upload: put: consumes: - multipart/form @@ -398,19 +489,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Upload files to cloud tags: - files - /cloud/{bucket}/files: + /api/v1/cloud/{bucket}/files: post: consumes: - application/json @@ -434,19 +533,27 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Get files list into bucket tags: - files - /cloud/bucket: + /api/v1/cloud/bucket: put: consumes: - application/json @@ -465,19 +572,23 @@ paths: "200": description: Ok schema: - $ref: '#/definitions/form.ResponseForm' + $ref: '#/definitions/form.Success' "400": - description: Bad Request message + description: Bad Request error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.BadRequestError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Create new bucket into cloud tags: - buckets - /cloud/buckets: + /api/v1/cloud/buckets: get: description: Get watched bucket list operationId: get-buckets @@ -485,19 +596,27 @@ paths: - application/json responses: "200": - description: Ok + description: Loaded buckets info schema: items: - type: string + $ref: '#/definitions/form.BucketSchema' type: array + "400": + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Get watched bucket list tags: - buckets - /tasks/{bucket}: + /api/v1/tasks/{bucket}: get: consumes: - application/json @@ -517,23 +636,31 @@ paths: - application/json responses: "200": - description: Ok + description: Loaded tasks schema: items: $ref: '#/definitions/form.TaskSchema' type: array "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Load processing tasks of uploaded files into bucket tags: - tasks - /tasks/{bucket}/{task_id}: + /api/v1/tasks/{bucket}/{task_id}: get: consumes: - application/json @@ -554,17 +681,25 @@ paths: - application/json responses: "200": - description: Ok + description: Loaded tasks schema: $ref: '#/definitions/form.TaskSchema' "400": - description: Bad Request message + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Task not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error schema: - $ref: '#/definitions/form.BadRequestForm' + $ref: '#/definitions/form.InternalServerError' "503": description: Server does not available schema: - $ref: '#/definitions/form.ServerErrorForm' + $ref: '#/definitions/form.ServerUnavailableError' summary: Load processing task by id tags: - tasks From 42841e313d47aeea5fa4c19f7512a4c3d4bd7127 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:03:46 +0300 Subject: [PATCH 16/47] merge: local changes for internal --- cmd/watchtower/httpserver/form/result.go | 50 ++++ internal/core/cloud/application/usecase.go | 46 ++-- internal/core/cloud/domain/bucket.go | 19 +- internal/core/cloud/domain/object.go | 40 ++- internal/core/cloud/domain/params.go | 45 +++- internal/core/cloud/domain/storage.go | 250 +++++++++++++++++- internal/core/cloud/infrastructure/s3/s3.go | 40 +-- internal/process/orchestrator.go | 21 +- internal/shared/kernel/context.go | 7 + internal/shared/kernel/ids.go | 19 +- internal/shared/telemetry/tracer.go | 2 +- internal/shared/utils/sender.go | 13 +- .../service/docstorage/docstorage.go | 6 +- .../service/recognizer/recognizer.go | 4 +- internal/support/task/application/usecase.go | 19 +- internal/support/task/domain/error.go | 9 + internal/support/task/domain/message.go | 25 +- internal/support/task/domain/queue.go | 101 ++++++- internal/support/task/domain/storage.go | 77 +++++- internal/support/task/domain/task.go | 75 ++++-- .../task/infrastructure/redis/redis.go | 27 +- .../support/task/infrastructure/rmq/dto.go | 6 +- .../support/task/infrastructure/rmq/rmq.go | 11 +- 23 files changed, 747 insertions(+), 165 deletions(-) create mode 100644 cmd/watchtower/httpserver/form/result.go create mode 100644 internal/shared/kernel/context.go create mode 100644 internal/support/task/domain/error.go diff --git a/cmd/watchtower/httpserver/form/result.go b/cmd/watchtower/httpserver/form/result.go new file mode 100644 index 0000000..a8408b3 --- /dev/null +++ b/cmd/watchtower/httpserver/form/result.go @@ -0,0 +1,50 @@ +package form + +// Success example +type Success struct { + Status int `json:"status" example:"200"` + Message string `json:"message" example:"Done"` +} + +func SuccessResponse(msg string) Success { + return Success{ + Status: 200, + Message: msg, + } +} + +// BadRequestError example +type BadRequestError struct { + Status int `json:"status" example:"400"` + Message string `json:"message" example:"Bad Request message"` +} + +// NotAcceptableError example +type NotAcceptableError struct { + Status int `json:"status" example:"406"` + Message string `json:"message" example:"Not acceptable request"` +} + +// AuthError example +type AuthError struct { + Status int `json:"status" example:"400"` + Message string `json:"message" example:"auth error"` +} + +// NotFoundError example +type NotFoundError struct { + Status int `json:"status" example:"404"` + Message string `json:"message" example:"Not found"` +} + +// InternalServerError example +type InternalServerError struct { + Status int `json:"status" example:"500"` + Message string `json:"message" example:"Internal server error message"` +} + +// ServerUnavailableError example +type ServerUnavailableError struct { + Status int `json:"status" example:"503"` + Message string `json:"message" example:"Server unavailable error message"` +} diff --git a/internal/core/cloud/application/usecase.go b/internal/core/cloud/application/usecase.go index e4c0c9c..83ef6dd 100644 --- a/internal/core/cloud/application/usecase.go +++ b/internal/core/cloud/application/usecase.go @@ -1,18 +1,16 @@ package application import ( - "context" "fmt" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "watchtower/internal/core/cloud/domain" + "watchtower/internal/shared/kernel" "watchtower/internal/shared/telemetry" ) -type Ctx context.Context - type StorageUseCase struct { cloudStorage domain.ICloudStorage } @@ -21,7 +19,7 @@ func NewStorageUseCase(cloudStorage domain.ICloudStorage) *StorageUseCase { return &StorageUseCase{cloudStorage: cloudStorage} } -func (s *StorageUseCase) GetAllBuckets(ctx Ctx) ([]domain.Bucket, error) { +func (s *StorageUseCase) GetAllBuckets(ctx kernel.Ctx) ([]domain.Bucket, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "get-buckets") defer span.End() @@ -36,7 +34,7 @@ func (s *StorageUseCase) GetAllBuckets(ctx Ctx) ([]domain.Bucket, error) { return allBuckets, err } -func (s *StorageUseCase) CreateBucket(ctx Ctx, bucketID domain.BucketID) error { +func (s *StorageUseCase) CreateBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "create-bucket") defer span.End() @@ -53,7 +51,7 @@ func (s *StorageUseCase) CreateBucket(ctx Ctx, bucketID domain.BucketID) error { return nil } -func (s *StorageUseCase) DeleteBucket(ctx Ctx, bucketID domain.BucketID) error { +func (s *StorageUseCase) DeleteBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "remove-bucket") defer span.End() @@ -69,7 +67,7 @@ func (s *StorageUseCase) DeleteBucket(ctx Ctx, bucketID domain.BucketID) error { return nil } -func (s *StorageUseCase) IsBucketExists(ctx Ctx, bucketID domain.BucketID) (bool, error) { +func (s *StorageUseCase) IsBucketExists(ctx kernel.Ctx, bucketID kernel.BucketID) (bool, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "is-bucket-exists") defer span.End() @@ -87,9 +85,9 @@ func (s *StorageUseCase) IsBucketExists(ctx Ctx, bucketID domain.BucketID) (bool } func (s *StorageUseCase) GetObjectInfo( - ctx Ctx, - bucketID domain.BucketID, - objID domain.ObjectID, + ctx kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, ) (*domain.Object, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "get-file-metadata") defer span.End() @@ -110,7 +108,7 @@ func (s *StorageUseCase) GetObjectInfo( return &objectInfo, nil } -func (s *StorageUseCase) CopyObject(ctx Ctx, bucketID domain.BucketID, params *domain.CopyObjectParams) error { +func (s *StorageUseCase) CopyObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "copy-object") defer span.End() @@ -130,7 +128,7 @@ func (s *StorageUseCase) CopyObject(ctx Ctx, bucketID domain.BucketID, params *d return nil } -func (s *StorageUseCase) DeleteObject(ctx Ctx, bucketID domain.BucketID, objID domain.ObjectID) error { +func (s *StorageUseCase) DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "copy-object") defer span.End() @@ -149,7 +147,7 @@ func (s *StorageUseCase) DeleteObject(ctx Ctx, bucketID domain.BucketID, objID d return nil } -func (s *StorageUseCase) MoveObject(ctx Ctx, bucketID domain.BucketID, params *domain.CopyObjectParams) error { +func (s *StorageUseCase) MoveObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "move-file") defer span.End() @@ -178,9 +176,9 @@ func (s *StorageUseCase) MoveObject(ctx Ctx, bucketID domain.BucketID, params *d return nil } -func (s *StorageUseCase) GetBucketObjects( - ctx Ctx, - bucketID domain.BucketID, +func (s *StorageUseCase) LoadBucketObjects( + ctx kernel.Ctx, + bucketID kernel.BucketID, params *domain.GetObjectsParams, ) ([]domain.Object, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "get-bucket-objects") @@ -203,10 +201,10 @@ func (s *StorageUseCase) GetBucketObjects( } func (s *StorageUseCase) StoreObject( - ctx Ctx, - bucketID domain.BucketID, + ctx kernel.Ctx, + bucketID kernel.BucketID, params *domain.UploadObjectParams, -) (domain.ObjectID, error) { +) (kernel.ObjectID, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "upload-object") defer span.End() @@ -229,8 +227,8 @@ func (s *StorageUseCase) StoreObject( } func (s *StorageUseCase) GenShareURL( - ctx Ctx, - bucketID domain.BucketID, + ctx kernel.Ctx, + bucketID kernel.BucketID, params *domain.ShareObjectParams, ) (string, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "share-object") @@ -253,9 +251,9 @@ func (s *StorageUseCase) GenShareURL( } func (s *StorageUseCase) GetObjectData( - ctx Ctx, - bucketID domain.BucketID, - objID domain.ObjectID, + ctx kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, ) (domain.ObjectData, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "download-object") defer span.End() diff --git a/internal/core/cloud/domain/bucket.go b/internal/core/cloud/domain/bucket.go index 1afc93b..e932c6b 100644 --- a/internal/core/cloud/domain/bucket.go +++ b/internal/core/cloud/domain/bucket.go @@ -1,11 +1,20 @@ package domain -import "time" - -type BucketID = string +import ( + "time" + "watchtower/internal/shared/kernel" +) +// Bucket represents a storage bucket/container in the cloud storage system. +// Buckets are used to organize objects and control access at the container level. type Bucket struct { - ID BucketID - Path string + // ID is the unique identifier for the bucket + ID kernel.BucketID + + // Path is the full path or URI to the bucket + // Example: "s3://my-bucket" or "gs://my-bucket" + Path string + + // CreatedAt indicates when the bucket was created CreatedAt time.Time } diff --git a/internal/core/cloud/domain/object.go b/internal/core/cloud/domain/object.go index 50abcfa..239b656 100644 --- a/internal/core/cloud/domain/object.go +++ b/internal/core/cloud/domain/object.go @@ -5,16 +5,40 @@ import ( "time" ) -type ObjectID = string +// ObjectData represents the actual content of a stored object. +// Using *bytes.Buffer allows efficient reading and writing of object data. type ObjectData = *bytes.Buffer +// Object represents metadata about a stored object/file in cloud storage. +// It contains all relevant information about the object without its actual data. type Object struct { - Name string - Path string - Checksum string - ContentType string - Expired time.Time + // Name is the base name of the object (filename) + // Example: "document.pdf" + Name string + + // Path is the full path to the object within the bucket + // Example: "folder/subfolder/document.pdf" + Path string + + // Checksum is a hash of the object content for integrity verification + // Usually MD5, SHA256, or provider-specific checksum + Checksum string + + // ContentType is the MIME type of the object + // Example: "application/pdf", "image/jpeg" + ContentType string + + // Expired indicates when the object expires and may be automatically deleted + // Zero value means the object never expires + Expired time.Time + + // LastModified is the timestamp of the last modification LastModified time.Time - Size int64 - IsDirectory bool + + // Size is the object size in bytes + Size int64 + + // IsDirectory indicates if this "object" actually represents a directory/folder + // Some storage systems treat folders as objects with special handling + IsDirectory bool } diff --git a/internal/core/cloud/domain/params.go b/internal/core/cloud/domain/params.go index 83acdd1..84937fa 100644 --- a/internal/core/cloud/domain/params.go +++ b/internal/core/cloud/domain/params.go @@ -5,22 +5,61 @@ import ( "time" ) +// CopyObjectParams defines parameters for copying an object from one location to another +// within the same bucket or across different paths. type CopyObjectParams struct { - SourcePath string + // SourcePath is the full path of the source object + // Example: "documents/original/file.pdf" + SourcePath string + + // DestinationPath is the full path where the object should be copied + // Example: "backups/file.pdf" or "documents/copy/file.pdf" DestinationPath string } +// ShareObjectParams defines parameters for generating a shareable URL for an object. +// This enables temporary access to private objects. type ShareObjectParams struct { + // FilePath is the path to the object to share + // Example: "shared/report.pdf" FilePath string - Expired time.Duration + + // Expired specifies how long the shareable URL remains valid + // Example: 24 * time.Hour for a 24-hour link + Expired time.Duration } +// GetObjectsParams defines filtering and pagination parameters for listing objects. type GetObjectsParams struct { + // PrefixPath filters objects to those with paths starting with this prefix + // This effectively lists objects in a "directory" + // Example: "documents/2024/" to list all objects in the 2024 folder PrefixPath string + + // MaxKeys limits the number of objects returned (pagination) + // Zero means use provider default + MaxKeys int32 + + // ContinuationToken for pagination through large result sets + ContinuationToken string } +// UploadObjectParams defines parameters for uploading a new object to storage. type UploadObjectParams struct { + // FilePath is the destination path for the uploaded object + // Example: "uploads/images/profile.jpg" FilePath string + + // FileData contains the actual content to upload FileData *bytes.Buffer - Expired *time.Time + + // ContentType specifies the MIME type (optional, auto-detected if not provided) + ContentType string + + // Expired sets an expiration time for the object (optional) + // If nil, the object never expires + Expired *time.Time + + // Metadata allows attaching custom key-value pairs to the object + Metadata map[string]string } diff --git a/internal/core/cloud/domain/storage.go b/internal/core/cloud/domain/storage.go index 7d4d1e6..3e49018 100644 --- a/internal/core/cloud/domain/storage.go +++ b/internal/core/cloud/domain/storage.go @@ -1,10 +1,14 @@ package domain import ( - "context" "net/url" + + "watchtower/internal/shared/kernel" ) +// ICloudStorage defines the complete interface for cloud storage operations. +// It combines bucket management, object operations, and sharing capabilities +// into a unified API. type ICloudStorage interface { IBucketManager IObjectManager @@ -12,25 +16,247 @@ type ICloudStorage interface { IShareManager } +// IBucketManager defines operations for managing storage buckets/containers. +// Buckets are the top-level organizational units in cloud storage. type IBucketManager interface { - GetAllBuckets(ctx context.Context) ([]Bucket, error) - IsBucketExist(ctx context.Context, bucketID BucketID) (bool, error) - CreateBucket(ctx context.Context, bucketID BucketID) error - DeleteBucket(ctx context.Context, bucketID BucketID) error + // GetAllBuckets retrieves a list of all buckets available in the storage system. + // Returns a slice of Bucket objects or an error if the operation fails. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // + // Returns: + // - []Bucket: List of all buckets + // - error: ErrUnauthorized if credentials are invalid, + // ErrServiceUnavailable if cloud provider is unreachable, + // or other provider-specific errors + // + // Example: + // buckets, err := storage.GetAllBuckets(ctx) + // for _, bucket := range buckets { + // fmt.Printf("Bucket: %s, Created: %s\n", bucket.ID, bucket.CreatedAt) + // } + GetAllBuckets(ctx kernel.Ctx) ([]Bucket, error) + + // IsBucketExist checks if a bucket with the given ID exists. + // This is useful for validation before performing bucket operations. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket to check + // + // Returns: + // - bool: true if bucket exists, false otherwise + // - error: ErrUnauthorized if credentials are invalid, or other provider-specific errors + // + // Example: + // exists, err := storage.IsBucketExist(ctx, "my-app-data") + // if !exists { + // // Create bucket or handle missing bucket + // } + IsBucketExist(ctx kernel.Ctx, bucketID kernel.BucketID) (bool, error) + + // CreateBucket creates a new bucket with the specified ID. + // Bucket IDs must be globally unique across the storage system. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: Unique identifier for the new bucket + // + // Returns: + // - error: ErrBucketAlreadyExists if bucket ID is taken, + // ErrInvalidBucketID if ID format is invalid, + // ErrQuotaExceeded if account limit reached, + // or other provider-specific errors + // + // Example: + // err := storage.CreateBucket(ctx, "user-uploads-2024") + // if errors.Is(err, ErrBucketAlreadyExists) { + // // Handle existing bucket + // } + CreateBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error + + // DeleteBucket removes an existing bucket and all its contents. + // This operation is irreversible and should be used with caution. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket to delete + // + // Returns: + // - error: ErrBucketNotFound if bucket doesn't exist, + // ErrBucketNotEmpty if bucket still contains objects, + // or other provider-specific errors + // + // Note: Some providers require the bucket to be empty before deletion. + DeleteBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error } +// IObjectManager defines operations for managing individual objects/files +// within buckets. This includes CRUD operations and data access. type IObjectManager interface { - GetObjectInfo(ctx context.Context, bucketID BucketID, objID ObjectID) (Object, error) - GetObjectData(ctx context.Context, bucketID BucketID, objID ObjectID) (ObjectData, error) - StoreObject(ctx context.Context, bucketID BucketID, params *UploadObjectParams) (ObjectID, error) - CopyObject(ctx context.Context, bucketID BucketID, params *CopyObjectParams) error - DeleteObject(ctx context.Context, bucketID BucketID, objID ObjectID) error + // GetObjectInfo retrieves metadata about an object without downloading its content. + // Useful for checking object properties, size, or last modified time. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket containing the object + // - objID: ID or path of the object to inspect + // + // Returns: + // - Object: Complete object metadata (without data) + // - error: ErrObjectNotFound if object doesn't exist, + // ErrBucketNotFound if bucket doesn't exist, + // or other provider-specific errors + // + // Example: + // info, err := storage.GetObjectInfo(ctx, "documents", "reports/annual.pdf") + // if err == nil { + // fmt.Printf("Size: %d bytes, Modified: %s\n", info.Size, info.LastModified) + // } + GetObjectInfo(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) (Object, error) + + // GetObjectData retrieves both the object metadata and its content. + // The object data is returned as a buffer that can be read or streamed. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket containing the object + // - objID: ID or path of the object to download + // + // Returns: + // - ObjectData: Buffer containing the object's content + // - error: ErrObjectNotFound if object doesn't exist, + // ErrBucketNotFound if bucket doesn't exist, + // or other provider-specific errors + // + // Example: + // data, err := storage.GetObjectData(ctx, "images", "profile.jpg") + // if err == nil { + // imgBytes := data.Bytes() + // // Process image data... + // } + GetObjectData(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) (ObjectData, error) + + // StoreObject uploads a new object or replaces an existing one. + // If an object already exists at the specified path, it will be overwritten. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket to store the object in + // - params: Upload parameters including file path, data, and options + // + // Returns: + // - ObjectID: Unique identifier for the stored object + // - error: ErrBucketNotFound if bucket doesn't exist, + // ErrInvalidFilePath if path format is invalid, + // ErrQuotaExceeded if bucket or account limit reached, + // or other provider-specific errors + // + // Example: + // data := bytes.NewBuffer([]byte("file content")) + // params := &UploadObjectParams{ + // FilePath: "uploads/notes.txt", + // FileData: data, + // ContentType: "text/plain", + // Metadata: map[string]string{"author": "john"}, + // } + // objID, err := storage.StoreObject(ctx, "my-bucket", params) + StoreObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *UploadObjectParams) (kernel.ObjectID, error) + + // CopyObject duplicates an object from one location to another within the same bucket. + // This operation is often more efficient than download+upload for large files. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket containing both source and destination + // - params: Copy parameters with source and destination paths + // + // Returns: + // - error: ErrObjectNotFound if source doesn't exist, + // ErrInvalidPath if destination path is invalid, + // or other provider-specific errors + // + // Example: + // params := &CopyObjectParams{ + // SourcePath: "originals/document.pdf", + // DestinationPath: "backups/document-2024.pdf", + // } + // err := storage.CopyObject(ctx, "documents", params) + CopyObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *CopyObjectParams) error + + // DeleteObject permanently removes an object from storage. + // This operation cannot be undone. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket containing the object + // - objID: ID or path of the object to delete + // + // Returns: + // - error: ErrObjectNotFound if object doesn't exist (idempotent), + // ErrBucketNotFound if bucket doesn't exist, + // or other provider-specific errors + // + // Example: + // err := storage.DeleteObject(ctx, "temp-files", "cache/session-123.tmp") + // if err != nil && !errors.Is(err, ErrObjectNotFound) { + // // Handle error, but ignore "not found" as it's already gone + // } + DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error } +// IObjectWalker defines operations for listing and iterating through objects in a bucket. +// This is useful for directory listings, backups, and batch operations. type IObjectWalker interface { - GetBucketObjects(ctx context.Context, bucketID BucketID, params *GetObjectsParams) ([]Object, error) + // GetBucketObjects retrieves a list of objects in a bucket, optionally filtered by prefix. + // This implements directory-like listing functionality. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket to list objects from + // - params: Filtering and pagination parameters + // + // Returns: + // - []Object: Slice of object metadata matching the criteria + // - error: ErrBucketNotFound if bucket doesn't exist, or other provider-specific errors + // + // Example: + // params := &GetObjectsParams{ + // PrefixPath: "images/2024/", + // } + // objects, err := storage.GetBucketObjects(ctx, "media", params) + // for _, obj := range objects { + // fmt.Printf("Found: %s (%d bytes)\n", obj.Path, obj.Size) + // } + GetBucketObjects(ctx kernel.Ctx, bucketID kernel.BucketID, params *GetObjectsParams) ([]Object, error) } +// IShareManager defines operations for generating temporary access URLs to objects. +// This enables secure sharing of private objects without making them public. type IShareManager interface { - GenShareURL(ctx context.Context, bucketID BucketID, params *ShareObjectParams) (*url.URL, error) + // GenShareURL generates a time-limited URL that provides access to a private object. + // The URL includes authentication tokens and expires after the specified duration. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket containing the object + // - params: Sharing parameters including file path and expiration + // + // Returns: + // - *url.URL: Pre-signed URL that grants temporary access + // - error: ErrObjectNotFound if object doesn't exist, + // ErrInvalidExpiration if duration is invalid, + // or other provider-specific errors + // + // Example: + // params := &ShareObjectParams{ + // FilePath: "shared/report.pdf", + // Expired: 24 * time.Hour, + // } + // shareURL, err := storage.GenShareURL(ctx, "documents", params) + // if err == nil { + // fmt.Printf("Shareable link (valid for 24h): %s\n", shareURL.String()) + // } + GenShareURL(ctx kernel.Ctx, bucketID kernel.BucketID, params *ShareObjectParams) (*url.URL, error) } diff --git a/internal/core/cloud/infrastructure/s3/s3.go b/internal/core/cloud/infrastructure/s3/s3.go index 9d81289..725ea25 100644 --- a/internal/core/cloud/infrastructure/s3/s3.go +++ b/internal/core/cloud/infrastructure/s3/s3.go @@ -2,7 +2,6 @@ package s3 import ( "bytes" - "context" "fmt" "log/slog" "net/url" @@ -12,6 +11,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" "watchtower/internal/core/cloud/domain" + "watchtower/internal/shared/kernel" ) type S3Client struct { @@ -37,7 +37,7 @@ func New(config Config) (domain.ICloudStorage, error) { return client, nil } -func (s *S3Client) GetAllBuckets(ctx context.Context) ([]domain.Bucket, error) { +func (s *S3Client) GetAllBuckets(ctx kernel.Ctx) ([]domain.Bucket, error) { buckets, err := s.mc.ListBuckets(ctx) if err != nil { err = fmt.Errorf("s3 error: %w", err) @@ -56,7 +56,7 @@ func (s *S3Client) GetAllBuckets(ctx context.Context) ([]domain.Bucket, error) { return bucketNames, nil } -func (s *S3Client) IsBucketExist(ctx context.Context, bucketID domain.BucketID) (bool, error) { +func (s *S3Client) IsBucketExist(ctx kernel.Ctx, bucketID kernel.BucketID) (bool, error) { result, err := s.mc.BucketExists(ctx, bucketID) if err != nil { err = fmt.Errorf("s3 error: %w", err) @@ -65,7 +65,7 @@ func (s *S3Client) IsBucketExist(ctx context.Context, bucketID domain.BucketID) return result, nil } -func (s *S3Client) CreateBucket(ctx context.Context, bucketID domain.BucketID) error { +func (s *S3Client) CreateBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error { opts := minio.MakeBucketOptions{} if err := s.mc.MakeBucket(ctx, bucketID, opts); err != nil { err = fmt.Errorf("s3 error: %w", err) @@ -74,7 +74,7 @@ func (s *S3Client) CreateBucket(ctx context.Context, bucketID domain.BucketID) e return nil } -func (s *S3Client) DeleteBucket(ctx context.Context, bucketID domain.BucketID) error { +func (s *S3Client) DeleteBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error { if err := s.mc.RemoveBucket(ctx, bucketID); err != nil { err = fmt.Errorf("s3 error: %w", err) return err @@ -83,9 +83,9 @@ func (s *S3Client) DeleteBucket(ctx context.Context, bucketID domain.BucketID) e } func (s *S3Client) GetObjectInfo( - ctx context.Context, - bucketID domain.BucketID, - objID domain.ObjectID, + ctx kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, ) (domain.Object, error) { var objectAttrs domain.Object opts := minio.StatObjectOptions{} @@ -111,9 +111,9 @@ func (s *S3Client) GetObjectInfo( } func (s *S3Client) GetObjectData( - ctx context.Context, - bucketID domain.BucketID, - objID domain.ObjectID, + ctx kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, ) (domain.ObjectData, error) { opts := minio.GetObjectOptions{} filePath := path.Clean(objID) @@ -134,10 +134,10 @@ func (s *S3Client) GetObjectData( } func (s *S3Client) StoreObject( - ctx context.Context, - bucketID domain.BucketID, + ctx kernel.Ctx, + bucketID kernel.BucketID, params *domain.UploadObjectParams, -) (domain.ObjectID, error) { +) (kernel.ObjectID, error) { opts := minio.PutObjectOptions{} if params.Expired != nil { opts.Expires = *params.Expired @@ -153,7 +153,7 @@ func (s *S3Client) StoreObject( return filePath, nil } -func (s *S3Client) CopyObject(ctx context.Context, bucketID domain.BucketID, params *domain.CopyObjectParams) error { +func (s *S3Client) CopyObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { srcPath := path.Clean(params.SourcePath) dstPath := path.Clean(params.DestinationPath) @@ -168,7 +168,7 @@ func (s *S3Client) CopyObject(ctx context.Context, bucketID domain.BucketID, par return nil } -func (s *S3Client) DeleteObject(ctx context.Context, bucketID domain.BucketID, objID domain.ObjectID) error { +func (s *S3Client) DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error { opts := minio.RemoveObjectOptions{} filePath := path.Clean(objID) if err := s.mc.RemoveObject(ctx, bucketID, filePath, opts); err != nil { @@ -179,8 +179,8 @@ func (s *S3Client) DeleteObject(ctx context.Context, bucketID domain.BucketID, o } func (s *S3Client) GetBucketObjects( - ctx context.Context, - bucketID domain.BucketID, + ctx kernel.Ctx, + bucketID kernel.BucketID, params *domain.GetObjectsParams, ) ([]domain.Object, error) { opts := minio.ListObjectsOptions{ @@ -220,8 +220,8 @@ func (s *S3Client) GetBucketObjects( } func (s *S3Client) GenShareURL( - ctx context.Context, - bucketID domain.BucketID, + ctx kernel.Ctx, + bucketID kernel.BucketID, params *domain.ShareObjectParams, ) (*url.URL, error) { filePath := path.Clean(params.FilePath) diff --git a/internal/process/orchestrator.go b/internal/process/orchestrator.go index afd5893..3299d2f 100644 --- a/internal/process/orchestrator.go +++ b/internal/process/orchestrator.go @@ -1,7 +1,6 @@ package process import ( - "context" "fmt" "log/slog" @@ -19,8 +18,6 @@ import ( taskDomain "watchtower/internal/support/task/domain" ) -type Ctx = context.Context - type Orchestrator struct { config Config storageUC *cloudApp.StorageUseCase @@ -39,7 +36,7 @@ func (o *Orchestrator) GetTaskProcessor() *taskUC.TaskUseCase { return o.taskUC } -func (o *Orchestrator) LaunchListener(gCtx Ctx) { +func (o *Orchestrator) LaunchListener(ctx kernel.Ctx) { go func() { consumeCh := o.taskUC.GetConsumerChannel() sem := semaphore.NewWeighted(o.config.SemaphoreSize) @@ -61,7 +58,7 @@ func (o *Orchestrator) LaunchListener(gCtx Ctx) { ctx.Done() }() - case <-gCtx.Done(): + case <-ctx.Done(): slog.Info("terminating processing") return } @@ -70,8 +67,8 @@ func (o *Orchestrator) LaunchListener(gCtx Ctx) { } func (o *Orchestrator) UploadFile( - ctx Ctx, - bucketID domain.BucketID, + ctx kernel.Ctx, + bucketID kernel.BucketID, params *domain.UploadObjectParams, ) (*taskDomain.Task, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "upload-file") @@ -102,7 +99,11 @@ func (o *Orchestrator) UploadFile( return task, nil } -func (o *Orchestrator) CreateTask(ctx Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) (*taskDomain.Task, error) { +func (o *Orchestrator) CreateTask( + ctx kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, +) (*taskDomain.Task, error) { task := taskDomain.CreateNewTask(bucketID, objID) taskID := task.ID.String() @@ -141,7 +142,7 @@ func (o *Orchestrator) CreateTask(ctx Ctx, bucketID kernel.BucketID, objID kerne return task, nil } -func (o *Orchestrator) handleTask(ctx Ctx, task *taskDomain.Task) { +func (o *Orchestrator) handleTask(ctx kernel.Ctx, task *taskDomain.Task) { slog.Info("processing task event", slog.String("task-id", task.ID.String())) ctx, span := telemetry.GlobalTracer.Start(ctx, "handle-task-from-queue") @@ -170,7 +171,7 @@ func (o *Orchestrator) handleTask(ctx Ctx, task *taskDomain.Task) { slog.Info(msg, slog.String("task-id", task.ID.String())) } -func (o *Orchestrator) processTask(ctx Ctx, task *taskDomain.Task) error { +func (o *Orchestrator) processTask(ctx kernel.Ctx, task *taskDomain.Task) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "task-processing") defer span.End() diff --git a/internal/shared/kernel/context.go b/internal/shared/kernel/context.go new file mode 100644 index 0000000..0af0ebe --- /dev/null +++ b/internal/shared/kernel/context.go @@ -0,0 +1,7 @@ +package kernel + +import ( + "context" +) + +type Ctx context.Context diff --git a/internal/shared/kernel/ids.go b/internal/shared/kernel/ids.go index e805d83..9623e7b 100644 --- a/internal/shared/kernel/ids.go +++ b/internal/shared/kernel/ids.go @@ -1,6 +1,19 @@ package kernel -import "watchtower/internal/core/cloud/domain" +import "github.com/google/uuid" -type BucketID = domain.BucketID -type ObjectID = domain.ObjectID +// BucketID is a unique identifier for a storage bucket. +// Buckets are top-level containers that hold objects (files). +type BucketID = string + +// ObjectID is a unique identifier for an object within a bucket. +// Objects are the actual files stored in the bucket. +type ObjectID = string + +// MessageID is a unique identifier for a queue message using UUID v4. +// This is separate from TaskID as the same task might be queued multiple times. +type MessageID = uuid.UUID + +// TaskID is a unique identifier for a task using UUID v4. +// This ensures globally unique task identifiers across distributed systems. +type TaskID = uuid.UUID diff --git a/internal/shared/telemetry/tracer.go b/internal/shared/telemetry/tracer.go index 4facb40..7395053 100644 --- a/internal/shared/telemetry/tracer.go +++ b/internal/shared/telemetry/tracer.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/otel/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + semconv "go.opentelemetry.io/otel/semconv/v1.39.0" ) const AppName = "watchtower" diff --git a/internal/shared/utils/sender.go b/internal/shared/utils/sender.go index 5e4850a..ec57d99 100644 --- a/internal/shared/utils/sender.go +++ b/internal/shared/utils/sender.go @@ -2,7 +2,6 @@ package utils import ( "bytes" - "context" "fmt" "io" "net/http" @@ -15,10 +14,11 @@ import ( "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" + "watchtower/internal/shared/kernel" "watchtower/internal/shared/telemetry" ) -func PUT(ctx context.Context, body *bytes.Buffer, url, mime string, timeout time.Duration) ([]byte, error) { +func PUT(ctx kernel.Ctx, body *bytes.Buffer, url, mime string, timeout time.Duration) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -29,7 +29,7 @@ func PUT(ctx context.Context, body *bytes.Buffer, url, mime string, timeout time return sendRequest(ctx, client, req) } -func POST(ctx context.Context, body *bytes.Buffer, url, mime string, timeout time.Duration) ([]byte, error) { +func POST(ctx kernel.Ctx, body *bytes.Buffer, url, mime string, timeout time.Duration) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) @@ -40,7 +40,7 @@ func POST(ctx context.Context, body *bytes.Buffer, url, mime string, timeout tim return sendRequest(ctx, client, req) } -func sendRequest(ctx context.Context, client *http.Client, req *http.Request) ([]byte, error) { +func sendRequest(ctx kernel.Ctx, client *http.Client, req *http.Request) ([]byte, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "http-request") defer span.End() @@ -50,6 +50,7 @@ func sendRequest(ctx context.Context, client *http.Client, req *http.Request) ([ ) injectSpanContext(ctx, req) + //nolint response, err := client.Do(req) if err != nil { err = fmt.Errorf("sending request error: %w", err) @@ -78,13 +79,13 @@ func sendRequest(ctx context.Context, client *http.Client, req *http.Request) ([ return respData, nil } -func extractSpanContext(ctx context.Context, resp *http.Response) context.Context { +func extractSpanContext(ctx kernel.Ctx, resp *http.Response) kernel.Ctx { propagator := telemetry.TracePropagator carrier := propagation.HeaderCarrier(resp.Header) return propagator.Extract(ctx, carrier) } -func injectSpanContext(ctx context.Context, req *http.Request) { +func injectSpanContext(ctx kernel.Ctx, req *http.Request) { propagator := otel.GetTextMapPropagator() carrier := propagation.HeaderCarrier(req.Header) propagator.Inject(ctx, carrier) diff --git a/internal/support/task/application/service/docstorage/docstorage.go b/internal/support/task/application/service/docstorage/docstorage.go index 34bdf5d..1a27bd2 100644 --- a/internal/support/task/application/service/docstorage/docstorage.go +++ b/internal/support/task/application/service/docstorage/docstorage.go @@ -1,7 +1,9 @@ package docstorage -import "context" +import ( + "watchtower/internal/shared/kernel" +) type IDocumentStorage interface { - StoreDocument(ctx context.Context, document *Document) (DocumentID, error) + StoreDocument(ctx kernel.Ctx, document *Document) (DocumentID, error) } diff --git a/internal/support/task/application/service/recognizer/recognizer.go b/internal/support/task/application/service/recognizer/recognizer.go index 6113274..a0da7e0 100644 --- a/internal/support/task/application/service/recognizer/recognizer.go +++ b/internal/support/task/application/service/recognizer/recognizer.go @@ -1,9 +1,9 @@ package recognizer import ( - "context" + "watchtower/internal/shared/kernel" ) type IRecognizer interface { - Recognize(ctx context.Context, params *RecognizeParams) (*Recognized, error) + Recognize(ctx kernel.Ctx, params *RecognizeParams) (*Recognized, error) } diff --git a/internal/support/task/application/usecase.go b/internal/support/task/application/usecase.go index 032e309..c8f0e55 100644 --- a/internal/support/task/application/usecase.go +++ b/internal/support/task/application/usecase.go @@ -2,7 +2,6 @@ package application import ( "bytes" - "context" "fmt" "log/slog" "path" @@ -18,8 +17,6 @@ import ( "watchtower/internal/support/task/domain" ) -type Ctx context.Context - type TaskUseCase struct { taskStorage domain.ITaskManager taskQueue domain.ITaskQueue @@ -41,7 +38,7 @@ func NewTaskUseCase( } } -func (p *TaskUseCase) GetBucketTasks(ctx Ctx, bucketID kernel.BucketID) ([]*domain.Task, error) { +func (p *TaskUseCase) GetBucketTasks(ctx kernel.Ctx, bucketID kernel.BucketID) ([]*domain.Task, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "get-all-bucket-tasks") defer span.End() @@ -58,7 +55,7 @@ func (p *TaskUseCase) GetBucketTasks(ctx Ctx, bucketID kernel.BucketID) ([]*doma return allTasks, nil } -func (p *TaskUseCase) GetTask(ctx Ctx, bucketID kernel.BucketID, taskID domain.TaskID) (*domain.Task, error) { +func (p *TaskUseCase) GetTask(ctx kernel.Ctx, bucketID kernel.BucketID, taskID kernel.TaskID) (*domain.Task, error) { ctx, span := telemetry.GlobalTracer.Start(ctx, "get-task-by-id") defer span.End() @@ -69,7 +66,7 @@ func (p *TaskUseCase) GetTask(ctx Ctx, bucketID kernel.BucketID, taskID domain.T task, err := p.taskStorage.GetTask(ctx, bucketID, taskID) if err != nil { - err = fmt.Errorf("ftask manager error: %w", err) + err = fmt.Errorf("task manager error: %w", err) span.SetStatus(codes.Error, err.Error()) span.RecordError(err) return nil, err @@ -78,7 +75,7 @@ func (p *TaskUseCase) GetTask(ctx Ctx, bucketID kernel.BucketID, taskID domain.T return task, nil } -func (p *TaskUseCase) UpdateTaskStatus(ctx Ctx, task *domain.Task) { +func (p *TaskUseCase) UpdateTaskStatus(ctx kernel.Ctx, task *domain.Task) { ctx, span := telemetry.GlobalTracer.Start(ctx, "update-task-status") defer span.End() @@ -97,7 +94,7 @@ func (p *TaskUseCase) UpdateTaskStatus(ctx Ctx, task *domain.Task) { } } -func (p *TaskUseCase) IsTaskAlreadyExists(ctx Ctx, task *domain.Task) bool { +func (p *TaskUseCase) IsTaskAlreadyExists(ctx kernel.Ctx, task *domain.Task) bool { ctx, span := telemetry.GlobalTracer.Start(ctx, "check-task-already-created") defer span.End() @@ -135,7 +132,7 @@ func (p *TaskUseCase) IsTaskAlreadyExists(ctx Ctx, task *domain.Task) bool { } } -func (p *TaskUseCase) PublishTaskToQueue(ctx Ctx, task *domain.Task) error { +func (p *TaskUseCase) PublishTaskToQueue(ctx kernel.Ctx, task *domain.Task) error { msg := mapping.MessageFromTask(task) err := p.taskQueue.Publish(ctx, msg) return err @@ -146,7 +143,7 @@ func (p *TaskUseCase) GetConsumerChannel() chan domain.Message { } func (p *TaskUseCase) Recognize( - ctx Ctx, + ctx kernel.Ctx, task *domain.Task, fileData *bytes.Buffer, ) (*recognizer.Recognized, error) { @@ -178,7 +175,7 @@ func (p *TaskUseCase) Recognize( } func (p *TaskUseCase) StoreDocument( - ctx Ctx, + ctx kernel.Ctx, task *domain.Task, recData *recognizer.Recognized, ) (docstorage.DocumentID, error) { diff --git a/internal/support/task/domain/error.go b/internal/support/task/domain/error.go new file mode 100644 index 0000000..9123f06 --- /dev/null +++ b/internal/support/task/domain/error.go @@ -0,0 +1,9 @@ +package domain + +import "errors" + +var ( + ErrExecution = errors.New("execution error") + ErrTaskNotFound = errors.New("task not found") + ErrInvalidTaskData = errors.New("invalid task data") +) diff --git a/internal/support/task/domain/message.go b/internal/support/task/domain/message.go index 15c4d66..e74f327 100644 --- a/internal/support/task/domain/message.go +++ b/internal/support/task/domain/message.go @@ -1,15 +1,24 @@ package domain import ( - "context" - - "github.com/google/uuid" + "watchtower/internal/shared/kernel" ) -type MessageID = uuid.UUID - +// Message represents a task wrapped for queue transport. +// It includes context for distributed tracing and the actual task payload. type Message struct { - Ctx context.Context - EventId MessageID - Body Task + // Ctx carries cancellation signals and deadlines across service boundaries + Ctx kernel.Ctx + + // EventId uniquely identifies this specific message in the queue + EventId kernel.MessageID + + // Body contains the actual task to be processed + Body Task + + // Metadata holds additional routing and tracing information + Metadata map[string]string + + // DeliveryAttempt counts how many times this message has been delivered + DeliveryAttempt int } diff --git a/internal/support/task/domain/queue.go b/internal/support/task/domain/queue.go index 8773b6f..86ad8d2 100644 --- a/internal/support/task/domain/queue.go +++ b/internal/support/task/domain/queue.go @@ -1,20 +1,113 @@ package domain import ( - "context" + "watchtower/internal/shared/kernel" ) +// ITaskQueue defines the complete interface for task queue operations. +// It combines publishing and consuming capabilities for a complete +// producer-consumer pattern implementation. type ITaskQueue interface { IConsumer IPublisher } +// IPublisher defines operations for publishing tasks to the queue. +// Publishers are responsible for enqueuing tasks for asynchronous processing. type IPublisher interface { - Publish(ctx context.Context, msg Message) error + // Publish sends a task message to the queue for asynchronous processing. + // The message is persisted in the queue and will be delivered to a consumer. + // + // Parameters: + // - ctx: Context for cancellation and timeout + // - msg: Complete message containing the task and metadata + // + // Returns: + // - error: ErrQueueUnavailable if queue service is down, + // ErrInvalidMessage if message validation fails, + // ErrPublishTimeout if operation exceeds deadline, + // or other queue-specific errors + // + // Example: + // task := Task{ + // ID: uuid.New(), + // BucketID: "input-bucket", + // ObjectID: "data/file.json", + // Status: Received, + // CreatedAt: time.Now(), + // } + // + // msg := Message{ + // EventId: uuid.New(), + // Body: task, + // Metadata: map[string]string{"source": "api"}, + // } + // + // err := publisher.Publish(ctx, msg) + // if err != nil { + // log.Printf("Failed to publish task: %v", err) + // } + Publish(ctx kernel.Ctx, msg Message) error } +// IConsumer defines operations for consuming tasks from the queue. +// Consumers process tasks asynchronously and manage the consumption lifecycle. type IConsumer interface { + // GetConsumerChannel returns a read-only channel for receiving messages. + // This channel should be used in a select statement or range loop to + // process incoming tasks. + // + // Returns: + // - chan Message: Channel that delivers messages as they arrive + // + // Example: + // msgChan := consumer.GetConsumerChannel() + // for msg := range msgChan { + // go processMessage(msg) + // } GetConsumerChannel() chan Message - StartConsuming(ctx context.Context) error - StopConsuming(ctx context.Context) error + + // StartConsuming begins the message consumption process. + // This method typically connects to the queue service and begins + // delivering messages to the consumer channel. + // + // Parameters: + // - ctx: Context for controlling the consumption lifecycle + // + // Returns: + // - error: ErrConsumerAlreadyStarted if already consuming, + // ErrQueueUnavailable if cannot connect to queue, + // or other queue-specific errors + // + // Example: + // ctx, cancel := context.WithCancel(context.Background()) + // defer cancel() + // + // go func() { + // if err := consumer.StartConsuming(ctx); err != nil { + // log.Printf("Consumer failed: %v", err) + // } + // }() + StartConsuming(ctx kernel.Ctx) error + + // StopConsuming gracefully stops the message consumption process. + // It should complete any in-progress message handling before stopping. + // + // Parameters: + // - ctx: Context for timeout control during shutdown + // + // Returns: + // - error: ErrConsumerNotStarted if not consuming, + // ErrShutdownTimeout if graceful shutdown times out, + // or other queue-specific errors + // + // Example: + // // Graceful shutdown + // shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // defer cancel() + // + // if err := consumer.StopConsuming(shutdownCtx); err != nil { + // log.Printf("Force shutdown: %v", err) + // } + StopConsuming(ctx kernel.Ctx) error } diff --git a/internal/support/task/domain/storage.go b/internal/support/task/domain/storage.go index 8b214a4..f7c2c71 100644 --- a/internal/support/task/domain/storage.go +++ b/internal/support/task/domain/storage.go @@ -1,17 +1,82 @@ package domain import ( - "context" - - "watchtower/internal/core/cloud/domain" + "watchtower/internal/shared/kernel" ) +// ITaskStorage defines the interface for persistent task storage. +// This is used to track task state independently from the message queue. type ITaskStorage interface { ITaskManager } +// ITaskManager defines operations for managing task lifecycle in persistent storage. +// Tasks are stored independently of the queue to maintain state across system restarts +// and provide audit capabilities. type ITaskManager interface { - GetTask(ctx context.Context, bucketID domain.BucketID, taskID TaskID) (*Task, error) - GetAllBucketTasks(ctx context.Context, bucketID domain.BucketID) ([]*Task, error) - UpdateTask(ctx context.Context, task *Task) error + // GetTask retrieves a task by its bucket and task IDs. + // This is useful for checking task status or retrieving results. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket containing the task's input + // - taskID: Unique identifier of the task + // + // Returns: + // - *Task: Complete task information including current status + // - error: ErrExecution if returned operation error, + // ErrTaskNotFound if task not found, + // ErrInvalidTaskData if update would violate constraints, + // or other storage errors + // + // Example: + // task, err := storage.GetTask(ctx, "input-bucket", taskID) + // if err == nil { + // fmt.Printf("Task %s status: %s\n", task.ID, task.Status) + // } + GetTask(ctx kernel.Ctx, bucketID kernel.BucketID, taskID kernel.TaskID) (*Task, error) + + // GetAllBucketTasks retrieves all tasks associated with a specific bucket. + // This is useful for monitoring and batch operations. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket to get tasks for + // + // Returns: + // - []*Task: Slice of all tasks for the bucket, ordered by creation time + // - error: ErrExecution if returned operation error, + // ErrInvalidTaskData if update would violate constraints, + // or other storage errors + // + // Example: + // tasks, err := storage.GetAllBucketTasks(ctx, "input-bucket") + // for _, task := range tasks { + // fmt.Printf("Task %s: %s\n", task.ID, task.Status) + // } + GetAllBucketTasks(ctx kernel.Ctx, bucketID kernel.BucketID) ([]*Task, error) + + // UpdateTask updates an existing task's status and metadata. + // This is called as tasks progress through their lifecycle. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - task: Complete task object with updated fields + // + // Returns: + // - error: ErrExecution if returned operation error, + // ErrTaskNotFound if task not found, + // ErrInvalidTaskData if update would violate constraints, + // or other storage errors + // + // Example: + // task.Status = Processing + // task.StatusText = "Starting processing..." + // task.ModifiedAt = time.Now() + // + // err := storage.UpdateTask(ctx, task) + // if err != nil { + // log.Printf("Failed to update task: %v", err) + // } + UpdateTask(ctx kernel.Ctx, task *Task) error } diff --git a/internal/support/task/domain/task.go b/internal/support/task/domain/task.go index f54ac4e..ce71f82 100644 --- a/internal/support/task/domain/task.go +++ b/internal/support/task/domain/task.go @@ -6,7 +6,8 @@ import ( "time" "github.com/google/uuid" - "watchtower/internal/core/cloud/domain" + + "watchtower/internal/shared/kernel" ) const ( @@ -14,30 +15,72 @@ const ( ProcessingStatusText = "processing" ) +// TaskStatus represents the current state of a task in its lifecycle. +// The status follows a typical workflow: Received -> Pending -> Processing -> Successful, +// with Failed as a terminal error state. type TaskStatus int const ( - Failed TaskStatus = iota - 1 - Received - Pending - Processing - Successful -) + // Failed indicates the task processing encountered an error and could not complete. + // This is a terminal state. + Failed TaskStatus = iota - 1 // -1 + + // Received indicates the task has been accepted by the queue system + // but not yet scheduled for processing. + Received // 0 + + // Pending indicates the task is waiting to be processed by a worker. + Pending // 1 -type TaskID = uuid.UUID + // Processing indicates the task is currently being executed by a worker. + Processing // 2 + // Successful indicates the task completed successfully. + // This is a terminal state. + Successful // 3 +) + +// Task represents a unit of work to be processed asynchronously. +// It contains all necessary information for processing and tracks the task's +// lifecycle from creation to completion or failure. type Task struct { - ID TaskID - BucketID domain.BucketID - ObjectID domain.ObjectID + // ID uniquely identifies the task across the entire system + ID kernel.TaskID + + // BucketID identifies which storage bucket contains the input data + BucketID kernel.BucketID + + // ObjectID identifies the specific object in the bucket to process + ObjectID kernel.ObjectID + + // ObjectDataSize is the size of the input data in bytes, + // useful for progress tracking and resource estimation ObjectDataSize int - StatusText string - Status TaskStatus - CreatedAt time.Time - ModifiedAt time.Time + + // StatusText provides additional context about the current status, + // such as error messages for failed tasks or progress for processing tasks + StatusText string + + // Status indicates the current state in the task lifecycle + Status TaskStatus + + // CreatedAt is the timestamp when the task was initially created + CreatedAt time.Time + + // ModifiedAt is the timestamp of the last status update + ModifiedAt time.Time + + // RetryCount indicates how many times this task has been retried + RetryCount int + + // MaxRetries specifies the maximum number of retry attempts + MaxRetries int + + // ProcessingDuration tracks how long the task took to process (when completed) + ProcessingDuration time.Duration } -func CreateNewTask(bucketID domain.BucketID, objectID domain.ObjectID) *Task { +func CreateNewTask(bucketID kernel.BucketID, objectID kernel.ObjectID) *Task { // TODO: Disabled for TechDebt // taskID := GenerateUniqID(form.ID, form.FilePath) taskID := GenerateTaskID() diff --git a/internal/support/task/infrastructure/redis/redis.go b/internal/support/task/infrastructure/redis/redis.go index b527c50..8b95014 100644 --- a/internal/support/task/infrastructure/redis/redis.go +++ b/internal/support/task/infrastructure/redis/redis.go @@ -1,7 +1,6 @@ package redis import ( - "context" "encoding/json" "fmt" "log/slog" @@ -28,7 +27,7 @@ func New(config Config) domain.ITaskStorage { } } -func (rs *RedisClient) GetAllBucketTasks(ctx context.Context, bucketID kernel.BucketID) ([]*domain.Task, error) { +func (rs *RedisClient) GetAllBucketTasks(ctx kernel.Ctx, bucketID kernel.BucketID) ([]*domain.Task, error) { key := rs.generateUniqID(bucketID, "*") status := rs.rsConn.Scan(ctx, 0, key, -1) if status.Err() != nil { @@ -39,6 +38,7 @@ func (rs *RedisClient) GetAllBucketTasks(ctx context.Context, bucketID kernel.Bu tasks := make([]*domain.Task, len(rKeys)) for index, rKey := range rKeys { cmd := rs.rsConn.Get(ctx, rKey) + data, err := cmd.Bytes() if err != nil { slog.Warn("failed to get task", slog.String("err", err.Error())) @@ -64,46 +64,41 @@ func (rs *RedisClient) GetAllBucketTasks(ctx context.Context, bucketID kernel.Bu } func (rs *RedisClient) GetTask( - ctx context.Context, + ctx kernel.Ctx, bucketID kernel.BucketID, - taskID domain.TaskID, + taskID kernel.TaskID, ) (*domain.Task, error) { key := rs.generateUniqID(bucketID, taskID.String()) cmd := rs.rsConn.Get(ctx, key) if cmd.Err() != nil { - return nil, fmt.Errorf("redis error: %w", cmd.Err()) - } - - data, err := cmd.Bytes() - if err != nil { - return nil, fmt.Errorf("read bytes data error: %w", err) + return nil, fmt.Errorf("redis error: %w: %w", domain.ErrExecution, cmd.Err()) } value := &RedisValue{} - if err = json.Unmarshal(data, &value); err != nil { - return nil, fmt.Errorf("deserialize error: %w", err) + if err := cmd.Scan(value); err != nil { + return nil, fmt.Errorf("deserialize error: %w: %w", domain.ErrInvalidTaskData, err) } taskEvent, err := value.ConvertToTask() if err != nil { - return nil, fmt.Errorf("task validation error: %w", err) + return nil, fmt.Errorf("task validation error: %w: %w", domain.ErrInvalidTaskData, err) } return taskEvent, nil } -func (rs *RedisClient) UpdateTask(ctx context.Context, task *domain.Task) error { +func (rs *RedisClient) UpdateTask(ctx kernel.Ctx, task *domain.Task) error { key := rs.generateUniqID(task.BucketID, task.ID.String()) value := ConvertFromTaskEvent(task) jsonData, err := json.Marshal(value) if err != nil { - return fmt.Errorf("serialize error: %w", err) + return fmt.Errorf("serialize error: %w: %w", domain.ErrInvalidTaskData, err) } status := rs.rsConn.Set(ctx, key, jsonData, rs.config.Expired*time.Second) if status.Err() != nil { - return fmt.Errorf("redis error: %w", status.Err()) + return fmt.Errorf("redis error: %w: %w", domain.ErrExecution, status.Err()) } return nil diff --git a/internal/support/task/infrastructure/rmq/dto.go b/internal/support/task/infrastructure/rmq/dto.go index e088f1b..fa33d7a 100644 --- a/internal/support/task/infrastructure/rmq/dto.go +++ b/internal/support/task/infrastructure/rmq/dto.go @@ -1,14 +1,14 @@ package rmq import ( - "context" - "github.com/google/uuid" + + "watchtower/internal/shared/kernel" "watchtower/internal/support/task/domain" ) type Message struct { - Ctx context.Context + Ctx kernel.Ctx EventId uuid.UUID `json:"event_id"` Body domain.Task `json:"body"` } diff --git a/internal/support/task/infrastructure/rmq/rmq.go b/internal/support/task/infrastructure/rmq/rmq.go index fbd5c28..f1e898d 100644 --- a/internal/support/task/infrastructure/rmq/rmq.go +++ b/internal/support/task/infrastructure/rmq/rmq.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "time" + "watchtower/internal/shared/kernel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" @@ -62,7 +63,7 @@ func (r *RabbitMQClient) GetConsumerChannel() chan domain.Message { return r.redirect } -func (r *RabbitMQClient) Publish(ctx context.Context, msg domain.Message) error { +func (r *RabbitMQClient) Publish(ctx kernel.Ctx, msg domain.Message) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "rmq-publish") defer span.End() @@ -104,7 +105,7 @@ func (r *RabbitMQClient) Publish(ctx context.Context, msg domain.Message) error return nil } -func (r *RabbitMQClient) StartConsuming(_ context.Context) error { +func (r *RabbitMQClient) StartConsuming(_ kernel.Ctx) error { go r.handleReconnect() deliveries, err := r.channel.Consume( @@ -126,7 +127,7 @@ func (r *RabbitMQClient) StartConsuming(_ context.Context) error { return nil } -func (r *RabbitMQClient) StopConsuming(_ context.Context) error { +func (r *RabbitMQClient) StopConsuming(_ kernel.Ctx) error { if err := r.channel.Cancel(ConsumerName, true); err != nil { return fmt.Errorf("rmq: consumer cancel failed: %w", err) } @@ -215,7 +216,7 @@ func (r *RabbitMQClient) handleReconnect() { } } -func injectSpanContextToHeaders(ctx context.Context) amqp.Table { +func injectSpanContextToHeaders(ctx kernel.Ctx) amqp.Table { carrier := propagation.HeaderCarrier{} telemetry.TracePropagator.Inject(ctx, carrier) @@ -231,7 +232,7 @@ func injectSpanContextToHeaders(ctx context.Context) amqp.Table { return headers } -func extractSpanContextFromHeaders(headers amqp.Table) context.Context { +func extractSpanContextFromHeaders(headers amqp.Table) kernel.Ctx { ctx := context.Background() if headers == nil { return ctx From 62b593565043c98475f98cfa3f49ddaf29b1a827 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:04:20 +0300 Subject: [PATCH 17/47] chore: added new mocks into integration tests --- tests/common/mocks/docstorage.go | 4 +- tests/common/mocks/object_storage.go | 88 ++++++++++++++++++++++++++++ tests/common/mocks/recognizer.go | 8 +-- tests/common/mocks/task_queue.go | 33 +++++++++++ tests/common/mocks/task_storage.go | 27 +++++++++ 5 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 tests/common/mocks/object_storage.go create mode 100644 tests/common/mocks/task_queue.go create mode 100644 tests/common/mocks/task_storage.go diff --git a/tests/common/mocks/docstorage.go b/tests/common/mocks/docstorage.go index b0f30a9..ebb66c2 100644 --- a/tests/common/mocks/docstorage.go +++ b/tests/common/mocks/docstorage.go @@ -1,7 +1,7 @@ package mocks import ( - "context" + "watchtower/internal/shared/kernel" "watchtower/internal/support/task/application/service/docstorage" @@ -12,7 +12,7 @@ type MockDocStorage struct { mock.Mock } -func (m *MockDocStorage) StoreDocument(_ context.Context, doc *docstorage.Document) (docstorage.DocumentID, error) { +func (m *MockDocStorage) StoreDocument(_ kernel.Ctx, doc *docstorage.Document) (docstorage.DocumentID, error) { args := m.Called(doc) return args.Get(0).(string), args.Error(1) } diff --git a/tests/common/mocks/object_storage.go b/tests/common/mocks/object_storage.go new file mode 100644 index 0000000..8cb6b8b --- /dev/null +++ b/tests/common/mocks/object_storage.go @@ -0,0 +1,88 @@ +package mocks + +import ( + "net/url" + "watchtower/internal/core/cloud/domain" + "watchtower/internal/shared/kernel" + + "github.com/stretchr/testify/mock" +) + +type MockObjectStorage struct { + mock.Mock +} + +func (m *MockObjectStorage) GetAllBuckets(_ kernel.Ctx) ([]domain.Bucket, error) { + args := m.Called() + return args.Get(0).([]domain.Bucket), args.Error(1) +} + +func (m *MockObjectStorage) IsBucketExist(_ kernel.Ctx, bucketID kernel.BucketID) (bool, error) { + args := m.Called(bucketID) + return args.Bool(0), args.Error(1) +} + +func (m *MockObjectStorage) CreateBucket(_ kernel.Ctx, bucketID kernel.BucketID) error { + args := m.Called(bucketID) + return args.Error(0) +} + +func (m *MockObjectStorage) DeleteBucket(_ kernel.Ctx, bucketID kernel.BucketID) error { + args := m.Called(bucketID) + return args.Error(0) +} + +func (m *MockObjectStorage) GetObjectInfo( + _ kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, +) (domain.Object, error) { + args := m.Called(bucketID, objID) + return args.Get(0).(domain.Object), args.Error(1) +} + +func (m *MockObjectStorage) GetObjectData( + _ kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, +) (domain.ObjectData, error) { + args := m.Called(bucketID, objID) + return args.Get(0).(domain.ObjectData), args.Error(1) +} + +func (m *MockObjectStorage) StoreObject( + _ kernel.Ctx, + bucketID kernel.BucketID, + params *domain.UploadObjectParams, +) (kernel.ObjectID, error) { + args := m.Called(bucketID, params) + return args.Get(0).(kernel.ObjectID), args.Error(1) +} + +func (m *MockObjectStorage) CopyObject(_ kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { + args := m.Called(bucketID, params) + return args.Error(0) +} + +func (m *MockObjectStorage) DeleteObject(_ kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error { + args := m.Called(bucketID, objID) + return args.Error(0) +} + +func (m *MockObjectStorage) GetBucketObjects( + _ kernel.Ctx, + bucketID kernel.BucketID, + params *domain.GetObjectsParams, +) ([]domain.Object, error) { + args := m.Called(bucketID, params) + return args.Get(0).([]domain.Object), args.Error(1) +} + +func (m *MockObjectStorage) GenShareURL( + _ kernel.Ctx, + bucketID kernel.BucketID, + params *domain.ShareObjectParams, +) (*url.URL, error) { + args := m.Called(bucketID, params) + return args.Get(0).(*url.URL), args.Error(1) +} diff --git a/tests/common/mocks/recognizer.go b/tests/common/mocks/recognizer.go index 7da3520..1f89764 100644 --- a/tests/common/mocks/recognizer.go +++ b/tests/common/mocks/recognizer.go @@ -1,18 +1,18 @@ package mocks import ( - "context" + "github.com/stretchr/testify/mock" - rec "watchtower/internal/support/task/application/service/recognizer" + "watchtower/internal/shared/kernel" - "github.com/stretchr/testify/mock" + rec "watchtower/internal/support/task/application/service/recognizer" ) type MockRecognizer struct { mock.Mock } -func (m *MockRecognizer) Recognize(_ context.Context, params *rec.RecognizeParams) (*rec.Recognized, error) { +func (m *MockRecognizer) Recognize(_ kernel.Ctx, params *rec.RecognizeParams) (*rec.Recognized, error) { args := m.Called(params) return args.Get(0).(*rec.Recognized), args.Error(1) } diff --git a/tests/common/mocks/task_queue.go b/tests/common/mocks/task_queue.go new file mode 100644 index 0000000..4f244c9 --- /dev/null +++ b/tests/common/mocks/task_queue.go @@ -0,0 +1,33 @@ +package mocks + +import ( + "github.com/stretchr/testify/mock" + + "watchtower/internal/shared/kernel" + "watchtower/internal/support/task/domain" +) + +type MockTaskQueue struct { + mock.Mock + + Ch chan domain.Message +} + +func (m *MockTaskQueue) Publish(_ kernel.Ctx, msg domain.Message) error { + args := m.Called(msg) + return args.Error(0) +} + +func (m *MockTaskQueue) GetConsumerChannel() chan domain.Message { + return m.Ch +} + +func (m *MockTaskQueue) StartConsuming(_ kernel.Ctx) error { + args := m.Called() + return args.Error(0) +} + +func (m *MockTaskQueue) StopConsuming(_ kernel.Ctx) error { + args := m.Called() + return args.Error(0) +} diff --git a/tests/common/mocks/task_storage.go b/tests/common/mocks/task_storage.go new file mode 100644 index 0000000..6097e88 --- /dev/null +++ b/tests/common/mocks/task_storage.go @@ -0,0 +1,27 @@ +package mocks + +import ( + "github.com/stretchr/testify/mock" + + "watchtower/internal/shared/kernel" + "watchtower/internal/support/task/domain" +) + +type MockTaskStorage struct { + mock.Mock +} + +func (m *MockTaskStorage) GetTask(_ kernel.Ctx, bucketID kernel.BucketID, taskID kernel.TaskID) (*domain.Task, error) { + args := m.Called(bucketID, taskID) + return args.Get(0).(*domain.Task), args.Error(1) +} + +func (m *MockTaskStorage) GetAllBucketTasks(_ kernel.Ctx, bucketID kernel.BucketID) ([]*domain.Task, error) { + args := m.Called(bucketID) + return args.Get(0).([]*domain.Task), args.Error(1) +} + +func (m *MockTaskStorage) UpdateTask(_ kernel.Ctx, task *domain.Task) error { + args := m.Called(task) + return args.Error(0) +} From b227dd518bad3978313ac996bfa6086d55127e75 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:04:36 +0300 Subject: [PATCH 18/47] chore: added app server test env --- tests/common/env_app_server.go | 50 ++++++++++++++++++++++++++++++++++ tests/common/usecase.go | 5 ++-- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/common/env_app_server.go diff --git a/tests/common/env_app_server.go b/tests/common/env_app_server.go new file mode 100644 index 0000000..10009c5 --- /dev/null +++ b/tests/common/env_app_server.go @@ -0,0 +1,50 @@ +package common + +import ( + "fmt" + "watchtower/cmd" + "watchtower/cmd/watchtower/httpserver" + "watchtower/internal/process" + "watchtower/internal/shared/telemetry" + "watchtower/tests/common/mocks" + + cloudApp "watchtower/internal/core/cloud/application" + taskApp "watchtower/internal/support/task/application" +) + +type TestAppServerEnvironment struct { + ObjectStorage *mocks.MockObjectStorage + TaskStorage *mocks.MockTaskStorage + TaskQueue *mocks.MockTaskQueue + DocStorage *mocks.MockDocStorage + Recognizer *mocks.MockRecognizer +} + +func InitTestAppEnvironment() *TestAppServerEnvironment { + objectStorage := new(mocks.MockObjectStorage) + taskStorage := new(mocks.MockTaskStorage) + taskQueue := new(mocks.MockTaskQueue) + recognizer := new(mocks.MockRecognizer) + docStorage := new(mocks.MockDocStorage) + return &TestAppServerEnvironment{ + ObjectStorage: objectStorage, + TaskStorage: taskStorage, + TaskQueue: taskQueue, + DocStorage: docStorage, + Recognizer: recognizer, + } +} + +func (e *TestAppServerEnvironment) BuildAppServer(servConfig *cmd.Config) (*httpserver.Server, error) { + tracerProvider, err := telemetry.InitTracer(servConfig.Otlp.Tracer) + telemetry.GlobalTracer = tracerProvider + if err != nil { + return nil, fmt.Errorf("failed to initialize tracer: %w", err) + } + + storageUseCase := cloudApp.NewStorageUseCase(e.ObjectStorage) + taskUseCase := taskApp.NewTaskUseCase(e.TaskStorage, e.TaskQueue, e.Recognizer, e.DocStorage) + orchestrator := process.NewOrchestrator(servConfig.Orchestrator, storageUseCase, taskUseCase) + appServer := httpserver.SetupServer(servConfig.Otlp, orchestrator, tracerProvider) + return appServer, nil +} diff --git a/tests/common/usecase.go b/tests/common/usecase.go index 74ec55d..19e32ab 100644 --- a/tests/common/usecase.go +++ b/tests/common/usecase.go @@ -7,7 +7,7 @@ import ( "os" "time" - "watchtower/cmd/watchtower/config" + "watchtower/cmd" "watchtower/internal/core/cloud/infrastructure/s3" "watchtower/internal/process" "watchtower/internal/shared/telemetry" @@ -36,7 +36,8 @@ type TestEnvironment struct { func InitTestEnvironment(configFilePath string) (*TestEnvironment, error) { ctx := context.Background() - servConfig, err := config.FromFile(configFilePath) + + servConfig, err := cmd.InitConfig() if err != nil { return nil, fmt.Errorf("failed to read config file %s: %w", configFilePath, err) } From 13f798d9beb977db4152159e9fd9793c31682a76 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:04:47 +0300 Subject: [PATCH 19/47] chore: some changes --- tests/storage_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/storage_test.go b/tests/storage_test.go index ab9d1ca..aeb4fe4 100644 --- a/tests/storage_test.go +++ b/tests/storage_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "watchtower/internal/core/cloud/domain" + "watchtower/internal/shared/kernel" "watchtower/tests/common" ) @@ -63,7 +64,7 @@ func TestStorage(t *testing.T) { }) } -func StoreObjectToStorage(ctx context.Context, testEnv *common.TestEnvironment, filePath string) error { +func StoreObjectToStorage(ctx kernel.Ctx, testEnv *common.TestEnvironment, filePath string) error { uploadParams := domain.UploadObjectParams{ FilePath: filePath, FileData: bytes.NewBufferString("there is some file content"), From 75d9e47b5aeb88f3f926a1b0b6c232ba9e4bbae8 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:05:07 +0300 Subject: [PATCH 20/47] chore: impled news bucket and task routes tests --- tests/routes/bucket_routes_test.go | 255 +++++++++++++++++++++++++++++ tests/routes/task_routes_test.go | 190 +++++++++++++++++++++ 2 files changed, 445 insertions(+) create mode 100644 tests/routes/bucket_routes_test.go create mode 100644 tests/routes/task_routes_test.go diff --git a/tests/routes/bucket_routes_test.go b/tests/routes/bucket_routes_test.go new file mode 100644 index 0000000..2967e99 --- /dev/null +++ b/tests/routes/bucket_routes_test.go @@ -0,0 +1,255 @@ +package routes_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "watchtower/cmd" + "watchtower/cmd/watchtower/httpserver/form" + "watchtower/internal/core/cloud/domain" + "watchtower/tests/common" +) + +const ( + TestBucketName = "test-bucket-name" + TestBucketPath = "/" + + GetAllBucketsURL = "/api/v1/cloud/buckets" + CreateBucketURL = "/api/v1/cloud/bucket" + + GetAllBucketsMethod = "GetAllBuckets" + DeleteBucketMethod = "DeleteBucket" + CreateBucketMethod = "CreateBucket" + IsBucketExistsMethod = "IsBucketExist" +) + +var ( + BucketCreatedAt = time.Now() + TestBucket = domain.Bucket{ + ID: TestBucketName, + Path: TestBucketPath, + CreatedAt: BucketCreatedAt, + } +) + +func TestBucketAPIRoutes(t *testing.T) { + servConfig, err := cmd.InitConfig() + assert.NoError(t, err, "failed to read config file") + + var getBucketsTestCases = []struct { + TargetURL string + HttpMethod string + MockMethodName string + ReturnedData interface{} + ReturnedError error + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: GetAllBucketsURL, + HttpMethod: http.MethodGet, + MockMethodName: GetAllBucketsMethod, + ReturnedData: []domain.Bucket{TestBucket}, + ReturnedError: nil, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusOK, + }, + { + TargetURL: GetAllBucketsURL, + HttpMethod: http.MethodGet, + MockMethodName: GetAllBucketsMethod, + ReturnedData: []domain.Bucket{TestBucket}, + ReturnedError: fmt.Errorf("failed get all buckets"), + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + t.Run("Get buckets", func(t *testing.T) { + for index, testCase := range getBucketsTestCases { + testCaseName := fmt.Sprintf("Get buckets case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.ObjectStorage. + On(testCase.MockMethodName). + Return(testCase.ReturnedData, testCase.ReturnedError) + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "failed to create tag") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.ObjectStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) + + var createBucketTestCases = []struct { + TargetURL string + HttpMethod string + IsBucketExists bool + IsBucketExistsError error + MockMethodName string + ReturnedError error + RequestPayload *domain.Bucket + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: CreateBucketURL, + HttpMethod: http.MethodPut, + IsBucketExists: false, + IsBucketExistsError: nil, + MockMethodName: CreateBucketMethod, + ReturnedError: nil, + RequestPayload: &TestBucket, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusCreated, + }, + { + TargetURL: CreateBucketURL, + HttpMethod: http.MethodPut, + IsBucketExists: false, + IsBucketExistsError: nil, + MockMethodName: CreateBucketMethod, + ReturnedError: nil, + RequestPayload: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusBadRequest, + }, + { + TargetURL: CreateBucketURL, + HttpMethod: http.MethodPut, + IsBucketExists: true, + IsBucketExistsError: nil, + MockMethodName: CreateBucketMethod, + ReturnedError: nil, + RequestPayload: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusBadRequest, + // TODO: Temporary implementation + //ExpectedStatusCode: http.StatusConflict, + }, + { + TargetURL: CreateBucketURL, + HttpMethod: http.MethodPut, + IsBucketExists: false, + IsBucketExistsError: nil, + MockMethodName: CreateBucketMethod, + ReturnedError: fmt.Errorf("failed create bucket"), + RequestPayload: &TestBucket, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + t.Run("Delete bucket", func(t *testing.T) { + for index, testCase := range createBucketTestCases { + testCaseName := fmt.Sprintf("Create bucket case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.ObjectStorage. + On(IsBucketExistsMethod, TestBucket.ID). + Return(testCase.IsBucketExists, testCase.IsBucketExistsError) + + testEnv.ObjectStorage. + On(testCase.MockMethodName, TestBucket.ID). + Return(testCase.ReturnedError) + + var buffer = bytes.NewBuffer(nil) + if testCase.RequestPayload != nil { + jsonBytes, err := json.Marshal(form.CreateBucketForm{BucketName: testCase.RequestPayload.ID}) + assert.NoError(t, err, "failed to marshal request body") + buffer = bytes.NewBuffer(jsonBytes) + } + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "failed to delete tag") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.ObjectStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) + + var deleteBucketsTestCases = []struct { + TargetURL string + HttpMethod string + MockMethodName string + IsBucketExists bool + ReturnedError error + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s", TestBucket.ID), + HttpMethod: http.MethodDelete, + MockMethodName: DeleteBucketMethod, + IsBucketExists: true, + ReturnedError: nil, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusOK, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s", TestBucket.ID), + HttpMethod: http.MethodDelete, + MockMethodName: DeleteBucketMethod, + IsBucketExists: false, + ReturnedError: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusNotFound, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s", TestBucket.ID), + HttpMethod: http.MethodDelete, + MockMethodName: DeleteBucketMethod, + IsBucketExists: true, + ReturnedError: fmt.Errorf("failed to delete bucket"), + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + t.Run("Delete buckets", func(t *testing.T) { + for index, testCase := range deleteBucketsTestCases { + testCaseName := fmt.Sprintf("Delete bucket case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.ObjectStorage. + On(IsBucketExistsMethod, TestBucket.ID). + Return(testCase.IsBucketExists, nil) + + testEnv.ObjectStorage. + On(testCase.MockMethodName, TestBucket.ID). + Return(testCase.ReturnedError, testCase.ReturnedError) + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "delete bucket") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.ObjectStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) +} diff --git a/tests/routes/task_routes_test.go b/tests/routes/task_routes_test.go new file mode 100644 index 0000000..f195a29 --- /dev/null +++ b/tests/routes/task_routes_test.go @@ -0,0 +1,190 @@ +package routes_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "watchtower/cmd" + "watchtower/internal/shared/kernel" + "watchtower/internal/support/task/domain" + "watchtower/tests/common" +) + +const ( + TestTaskStatus = "done" + TestObjectDataSize = 1024 + + IncorrectTaskID = "incorrect-task-id" + + GetTaskMethod = "GetTask" + LoadTasksMethod = "GetAllBucketTasks" +) + +var ( + TestTaskID = uuid.New() + TestTaskCreated = time.Now() + TestTask = domain.Task{ + ID: TestTaskID, + BucketID: TestBucket.ID, + ObjectID: TestObjectID, + ObjectDataSize: TestObjectDataSize, + StatusText: TestTaskStatus, + Status: domain.Successful, + CreatedAt: TestTaskCreated, + ModifiedAt: TestTaskCreated, + RetryCount: 0, + MaxRetries: 0, + ProcessingDuration: 1 * time.Second, + } + + matchedBucketID = mock.MatchedBy(func(id kernel.BucketID) bool { + return id == TestBucket.ID + }) + + matchedTaskID = mock.MatchedBy(func(id kernel.TaskID) bool { + return id.String() == TestTaskID.String() + }) +) + +func TestTaskAPIRoutes(t *testing.T) { + servConfig, err := cmd.InitConfig() + assert.NoError(t, err, "failed to read config file") + + var loadTasksTestCases = []struct { + TargetURL string + HttpMethod string + MockMethodName string + ReturnedData interface{} + ReturnedError error + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: fmt.Sprintf("/api/v1/tasks/%s?status=0", TestBucketName), + HttpMethod: http.MethodGet, + MockMethodName: LoadTasksMethod, + ReturnedData: []*domain.Task{&TestTask}, + ReturnedError: nil, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusOK, + }, + { + TargetURL: fmt.Sprintf("/api/v1/tasks/%s", TestBucketName), + HttpMethod: http.MethodGet, + MockMethodName: LoadTasksMethod, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusBadRequest, + }, + { + TargetURL: fmt.Sprintf("/api/v1/tasks/%s?status=kek", TestBucketName), + HttpMethod: http.MethodGet, + MockMethodName: LoadTasksMethod, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusBadRequest, + }, + { + TargetURL: fmt.Sprintf("/api/v1/tasks/%s?status=0", TestBucketName), + HttpMethod: http.MethodGet, + MockMethodName: LoadTasksMethod, + ReturnedData: []*domain.Task{&TestTask}, + ReturnedError: fmt.Errorf("failed to load tasks"), + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + t.Run("Load tasks", func(t *testing.T) { + for index, testCase := range loadTasksTestCases { + testCaseName := fmt.Sprintf("Load tasks case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.TaskStorage. + On(testCase.MockMethodName, matchedBucketID). + Return(testCase.ReturnedData, testCase.ReturnedError) + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "failed to load tasks") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.TaskStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) + + var loadTaskByIDTestCases = []struct { + TargetURL string + HttpMethod string + MockMethodName string + ReturnedData interface{} + ReturnedError error + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: fmt.Sprintf("/api/v1/tasks/%s/%s", TestBucketName, TestTaskID.String()), + HttpMethod: http.MethodGet, + MockMethodName: GetTaskMethod, + ReturnedData: &TestTask, + ReturnedError: nil, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusOK, + }, + { + TargetURL: fmt.Sprintf("/api/v1/tasks/%s/%s", TestBucketName, IncorrectTaskID), + HttpMethod: http.MethodGet, + MockMethodName: GetTaskMethod, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusBadRequest, + }, + { + TargetURL: fmt.Sprintf("/api/v1/tasks/%s/%s", TestBucketName, TestTaskID.String()), + HttpMethod: http.MethodGet, + MockMethodName: GetTaskMethod, + ReturnedData: &TestTask, + ReturnedError: fmt.Errorf("failed load task by id"), + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + t.Run("Load task by id", func(t *testing.T) { + for index, testCase := range loadTaskByIDTestCases { + testCaseName := fmt.Sprintf("Load task by id case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.TaskStorage. + On(testCase.MockMethodName, matchedBucketID, matchedTaskID). + Return(testCase.ReturnedData, testCase.ReturnedError) + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "failed to load task by id") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.TaskStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) +} From 3d9a0b72f7f69c446a7f817d9610f2475864ebd0 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 13 Mar 2026 16:06:05 +0300 Subject: [PATCH 21/47] fix: lint warnings --- cmd/config.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/config.go b/cmd/config.go index 222339b..67a8a9a 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -99,6 +99,7 @@ func setupEnv(viperInst *viper.Viper) { viperInst.SetEnvPrefix(serviceEnvPrefix) viperInst.SetEnvKeyReplacer(strings.NewReplacer(".", "__")) + //nolint envMappings := map[string]string{ "orchestrator.semaphore_size": "ORCHESTRATOR__SEMAPHORE_SIZE", "otlp.logger.level": "OTLP__LOGGER__LEVEL", @@ -130,7 +131,7 @@ func setupEnv(viperInst *viper.Viper) { for key, value := range envMappings { bindErr = viperInst.BindEnv(key, fmt.Sprintf("%s__%s", serviceEnvPrefix, value)) if bindErr != nil { - slog.Warn("failed to bind env var", bindErr) + slog.Warn("failed to bind env var", slog.String("err", bindErr.Error())) } } } From c7581a85a7b560f9efc5f23ae0b1925782d5b223 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Sun, 15 Mar 2026 13:44:11 +0300 Subject: [PATCH 22/47] chore: updated tracer collecting --- cmd/watchtower/httpserver/helper.go | 87 ++++++++ cmd/watchtower/httpserver/httpserver.go | 4 +- cmd/watchtower/httpserver/mw/logger.go | 4 +- cmd/watchtower/httpserver/mw/tracer.go | 4 +- cmd/watchtower/httpserver/routes_bucket.go | 34 ++- cmd/watchtower/httpserver/routes_object.go | 229 +++++++++++++++++---- cmd/watchtower/httpserver/routes_task.go | 51 +++-- 7 files changed, 349 insertions(+), 64 deletions(-) create mode 100644 cmd/watchtower/httpserver/helper.go diff --git a/cmd/watchtower/httpserver/helper.go b/cmd/watchtower/httpserver/helper.go new file mode 100644 index 0000000..fa423a0 --- /dev/null +++ b/cmd/watchtower/httpserver/helper.go @@ -0,0 +1,87 @@ +package httpserver + +import ( + "fmt" + "log/slog" + "mime/multipart" + "strconv" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +func ExtractBucketParameter(eCtx *fiber.Ctx) (string, error) { + bucket := eCtx.Params("bucket") + if bucket == "" { + err := fmt.Errorf("bucket parameter is required") + return "", err + } + + return bucket, nil +} + +func ExtractTaskIDParameter(eCtx *fiber.Ctx) (uuid.UUID, error) { + taskIDParam := eCtx.Params("task_id") + if taskIDParam == "" { + err := fmt.Errorf("bucket parameter is required") + return uuid.Nil, err + } + + taskID, err := uuid.Parse(taskIDParam) + if err != nil { + return taskID, err + } + + return taskID, nil +} + +func ExtractTaskStatusParameter(eCtx *fiber.Ctx) (int, error) { + statusParam := eCtx.Query("status") + status, err := strconv.Atoi(statusParam) + if err != nil { + err = fmt.Errorf("unknown status parameter: %w", err) + return -1, err + } + + return status, nil +} + +func ExtractFileNameParameter(eCtx *fiber.Ctx) (string, error) { + fileNameQuery := eCtx.Query("file_name") + if fileNameQuery == "" { + err := fmt.Errorf("bucket parameter is required") + return "", err + } + + return fileNameQuery, nil +} + +func ExtractMultipartForm(eCtx *fiber.Ctx) (*multipart.Form, error) { + multipartForm, err := eCtx.MultipartForm() + if err != nil { + return nil, err + } + + if multipartForm.File["files"] == nil { + err = fmt.Errorf("there are no files into multipart form") + return nil, err + } + + return multipartForm, nil +} + +func ExtractExpiredDatetime(eCtx *fiber.Ctx) (*time.Time, error) { + expired := eCtx.Query("expired") + if expired == "" { + slog.Debug("expired parameter has not been set") + return nil, nil + } + + timeVal, err := time.Parse(time.RFC3339, expired) + if err != nil { + return nil, fmt.Errorf("failed to parse expired datetime: %w", err) + } + + return &timeVal, nil +} diff --git a/cmd/watchtower/httpserver/httpserver.go b/cmd/watchtower/httpserver/httpserver.go index ae64405..bca4a1b 100644 --- a/cmd/watchtower/httpserver/httpserver.go +++ b/cmd/watchtower/httpserver/httpserver.go @@ -114,5 +114,7 @@ func (s *Server) initLoggerMW(logConfig telemetry.LoggerConfig) { } func (s *Server) initTracerMW(_ telemetry.TracerConfig) { - s.Server.Use(otelfiber.Middleware()) + s.Server.Use(otelfiber.Middleware( + otelfiber.WithNext(mw.TracerURLSkipper), + )) } diff --git a/cmd/watchtower/httpserver/mw/logger.go b/cmd/watchtower/httpserver/mw/logger.go index a283458..38bdd93 100644 --- a/cmd/watchtower/httpserver/mw/logger.go +++ b/cmd/watchtower/httpserver/mw/logger.go @@ -9,10 +9,10 @@ import ( "strings" "time" - "watchtower/internal/shared/telemetry" - "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" + + "watchtower/internal/shared/telemetry" ) func InitLocalLogger(config telemetry.LoggerConfig) fiber.Handler { diff --git a/cmd/watchtower/httpserver/mw/tracer.go b/cmd/watchtower/httpserver/mw/tracer.go index 17091d3..1442ab5 100644 --- a/cmd/watchtower/httpserver/mw/tracer.go +++ b/cmd/watchtower/httpserver/mw/tracer.go @@ -12,11 +12,11 @@ var ( "/metrics", "/favicon.ico", "/static/", - "/api/v1/swagger", + "/api/swagger", } ) -func TracerSkipper(eCtx *fiber.Ctx) bool { +func TracerURLSkipper(eCtx *fiber.Ctx) bool { for _, excluded := range excludedPaths { if strings.HasPrefix(eCtx.Path(), excluded) { return true diff --git a/cmd/watchtower/httpserver/routes_bucket.go b/cmd/watchtower/httpserver/routes_bucket.go index 7fffc25..230caac 100644 --- a/cmd/watchtower/httpserver/routes_bucket.go +++ b/cmd/watchtower/httpserver/routes_bucket.go @@ -4,6 +4,9 @@ import ( "encoding/json" "github.com/gofiber/fiber/v2" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "watchtower/cmd/watchtower/httpserver/form" ) @@ -27,9 +30,13 @@ func (s *Server) CreateStorageBucketsGroup(group fiber.Router) { func (s *Server) GetBuckets(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() + span := trace.SpanFromContext(ctx) + objStorage := s.state.GetObjectStorage() buckets, err := objStorage.GetAllBuckets(ctx) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -57,19 +64,27 @@ func (s *Server) GetBuckets(eCtx *fiber.Ctx) error { func (s *Server) CreateBucket(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() + span := trace.SpanFromContext(ctx) + var jsonForm form.CreateBucketForm err := json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } objStorage := s.state.GetObjectStorage() exists, err := objStorage.IsBucketExists(ctx, jsonForm.BucketName) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } if exists { + span.SetStatus(codes.Error, "bucket already exists") + span.RecordError(err) // TODO: Temporary solution. Need to return 409 http error // return eCtx.Status(fiber.StatusConflict).SendString("bucket already exists") return eCtx.Status(fiber.StatusOK).SendString("bucket already exists") @@ -77,6 +92,8 @@ func (s *Server) CreateBucket(eCtx *fiber.Ctx) error { err = objStorage.CreateBucket(ctx, jsonForm.BucketName) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -99,15 +116,28 @@ func (s *Server) CreateBucket(eCtx *fiber.Ctx) error { func (s *Server) RemoveBucket(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) objStorage := s.state.GetObjectStorage() exists, err := objStorage.IsBucketExists(ctx, bucket) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } if !exists { + span.SetStatus(codes.Error, "buket does not exist") + span.RecordError(err) // TODO: Temporary solution. Need to return 409 http error // return eCtx.Status(fiber.StatusConflict).SendString("bucket already exists") return eCtx.Status(fiber.StatusNotFound).SendString("bucket already exists") @@ -115,6 +145,8 @@ func (s *Server) RemoveBucket(eCtx *fiber.Ctx) error { err = objStorage.DeleteBucket(ctx, bucket) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } diff --git a/cmd/watchtower/httpserver/routes_object.go b/cmd/watchtower/httpserver/routes_object.go index b09d0a1..dc377fd 100644 --- a/cmd/watchtower/httpserver/routes_object.go +++ b/cmd/watchtower/httpserver/routes_object.go @@ -12,6 +12,9 @@ import ( "watchtower/internal/core/cloud/domain" "github.com/gofiber/fiber/v2" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { @@ -43,11 +46,21 @@ func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { // @Router /api/v1/cloud/{bucket}/file/copy [post] func (s *Server) CopyFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } var jsonForm form.CopyFileForm - err := json.Unmarshal(eCtx.Body(), &jsonForm) + err = json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } @@ -58,6 +71,8 @@ func (s *Server) CopyFile(eCtx *fiber.Ctx) error { err = s.state.GetObjectStorage().CopyObject(ctx, bucket, params) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -81,11 +96,21 @@ func (s *Server) CopyFile(eCtx *fiber.Ctx) error { // @Router /api/v1/cloud/{bucket}/file/move [post] func (s *Server) MoveFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } var jsonForm form.CopyFileForm - err := json.Unmarshal(eCtx.Body(), &jsonForm) + err = json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } @@ -96,6 +121,8 @@ func (s *Server) MoveFile(eCtx *fiber.Ctx) error { err = s.state.GetObjectStorage().MoveObject(ctx, bucket, params) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -121,48 +148,68 @@ func (s *Server) MoveFile(eCtx *fiber.Ctx) error { func (s *Server) UploadFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - var fileData bytes.Buffer + span := trace.SpanFromContext(ctx) - multipartForm, err := eCtx.MultipartForm() + bucket, err := ExtractBucketParameter(eCtx) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - bucket := eCtx.Params("bucket") - exist, err := s.state.GetObjectStorage().IsBucketExists(ctx, bucket) + span.SetAttributes(attribute.String("bucket", bucket)) + + objectStorage := s.state.GetObjectStorage() + exist, err := objectStorage.IsBucketExists(ctx, bucket) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(http.StatusBadRequest).SendString(err.Error()) } if !exist { err = fmt.Errorf("specified bucket %s does not exist", bucket) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(http.StatusNotFound).SendString(err.Error()) } - if multipartForm.File["files"] == nil { - err = fmt.Errorf("there are no files into multipart form") + multipartForm, err := ExtractMultipartForm(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - expired := eCtx.Query("expired") - timeVal, timeParseErr := time.Parse(time.RFC3339, expired) - if timeParseErr != nil { - slog.Warn("failed to parse expired time param", - slog.String("err", timeParseErr.Error()), - ) + expiredDatetime, err := ExtractExpiredDatetime(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } + var fileData bytes.Buffer uploadedFiles := make([]form.TaskSchema, len(multipartForm.File["files"])) for index, fileForm := range multipartForm.File["files"] { fileName := fileForm.Filename fileHandler, err := fileForm.Open() if err != nil { - slog.Error("failed to open file form", slog.String("err", err.Error())) + err = fmt.Errorf("failed to open file form: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + + slog.Error("multipart error", + slog.String("file", fileName), + slog.String("err", err.Error()), + ) continue } defer func() { if err := fileHandler.Close(); err != nil { - slog.Error("failed to close file handler", + err = fmt.Errorf("failed to close file handler: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + slog.Error("multipart error", slog.String("file", fileName), slog.String("err", err.Error()), ) @@ -173,7 +220,10 @@ func (s *Server) UploadFile(eCtx *fiber.Ctx) error { fileData.Reset() _, err = fileData.ReadFrom(fileHandler) if err != nil { - slog.Error("failed to read file form", + err = fmt.Errorf("failed to read file form: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + slog.Error("multipart error", slog.String("file", fileName), slog.String("err", err.Error()), ) @@ -183,12 +233,15 @@ func (s *Server) UploadFile(eCtx *fiber.Ctx) error { params := &domain.UploadObjectParams{ FilePath: fileName, FileData: &fileData, - Expired: &timeVal, + Expired: expiredDatetime, } task, err := s.state.UploadFile(ctx, bucket, params) if err != nil { - slog.Error("failed to upload file to cloud", + err = fmt.Errorf("failed to upload file form: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + slog.Error("multipart error", slog.String("file", fileName), slog.String("err", err.Error()), ) @@ -218,16 +271,31 @@ func (s *Server) UploadFile(eCtx *fiber.Ctx) error { // @Router /api/v1/cloud/{bucket}/file/download [post] func (s *Server) DownloadFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) var jsonForm form.DownloadFileForm - err := json.Unmarshal(eCtx.Body(), &jsonForm) + err = json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - fileData, err := s.state.GetObjectStorage().GetObjectData(ctx, bucket, jsonForm.FileName) + objectStorage := s.state.GetObjectStorage() + fileData, err := objectStorage.GetObjectData(ctx, bucket, jsonForm.FileName) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } defer fileData.Reset() @@ -251,15 +319,30 @@ func (s *Server) DownloadFile(eCtx *fiber.Ctx) error { // @Router /api/v1/cloud/{bucket}/file/remove [delete] func (s *Server) RemoveFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) var jsonForm form.RemoveFileForm - err := json.Unmarshal(eCtx.Body(), &jsonForm) + err = json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - if err := s.state.GetObjectStorage().DeleteObject(ctx, bucket, jsonForm.FileName); err != nil { + objectStorage := s.state.GetObjectStorage() + if err = objectStorage.DeleteObject(ctx, bucket, jsonForm.FileName); err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -282,9 +365,31 @@ func (s *Server) RemoveFile(eCtx *fiber.Ctx) error { // @Router /api/v1/cloud/{bucket}/file [delete] func (s *Server) RemoveFile2(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") - fileName := eCtx.Query("file_name") - if err := s.state.GetObjectStorage().DeleteObject(ctx, bucket, fileName); err != nil { + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) + + fileName, err := ExtractFileNameParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("file_name", fileName)) + + objectStorage := s.state.GetObjectStorage() + if err = objectStorage.DeleteObject(ctx, bucket, fileName); err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -308,11 +413,23 @@ func (s *Server) RemoveFile2(eCtx *fiber.Ctx) error { // @Router /api/v1/cloud/{bucket}/files [post] func (s *Server) GetFiles(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) var jsonForm form.GetFilesForm - err := json.Unmarshal(eCtx.Body(), &jsonForm) + err = json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } @@ -320,8 +437,11 @@ func (s *Server) GetFiles(eCtx *fiber.Ctx) error { PrefixPath: jsonForm.DirectoryName, } - listObjects, err := s.state.GetObjectStorage().LoadBucketObjects(ctx, bucket, params) + objectStorage := s.state.GetObjectStorage() + listObjects, err := objectStorage.LoadBucketObjects(ctx, bucket, params) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -350,16 +470,31 @@ func (s *Server) GetFiles(eCtx *fiber.Ctx) error { // @Router /api/v1/cloud/{bucket}/file/attributes [post] func (s *Server) GetFileInfo(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) var jsonForm form.GetFileAttributesForm - err := json.Unmarshal(eCtx.Body(), &jsonForm) + err = json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - object, err := s.state.GetObjectStorage().GetObjectInfo(ctx, bucket, jsonForm.FilePath) + objectStorage := s.state.GetObjectStorage() + object, err := objectStorage.GetObjectInfo(ctx, bucket, jsonForm.FilePath) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } @@ -384,18 +519,34 @@ func (s *Server) GetFileInfo(eCtx *fiber.Ctx) error { // @Router /api/v1/cloud/{bucket}/file/share [post] func (s *Server) ShareFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) var jsonForm form.ShareFileForm - err := json.Unmarshal(eCtx.Body(), &jsonForm) + err = json.Unmarshal(eCtx.Body(), &jsonForm) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } expired := time.Second * time.Duration(jsonForm.ExpiredSecs) params := &domain.ShareObjectParams{FilePath: jsonForm.FilePath, Expired: expired} - url, err := s.state.GetObjectStorage().GenShareURL(ctx, bucket, params) + + objectStorage := s.state.GetObjectStorage() + url, err := objectStorage.GenShareURL(ctx, bucket, params) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } diff --git a/cmd/watchtower/httpserver/routes_task.go b/cmd/watchtower/httpserver/routes_task.go index 7f0aaa5..776e80c 100644 --- a/cmd/watchtower/httpserver/routes_task.go +++ b/cmd/watchtower/httpserver/routes_task.go @@ -1,13 +1,13 @@ package httpserver import ( - "strconv" + "github.com/gofiber/fiber/v2" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "golang.org/x/exp/slices" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" - "watchtower/cmd/watchtower/httpserver/form" task "watchtower/internal/support/task/domain" @@ -36,24 +36,35 @@ func (s *Server) CreateTasksGroup(group fiber.Router) { func (s *Server) LoadTasks(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") - if bucket == "" { - return eCtx.Status(fiber.StatusBadRequest).SendString("bucket is required") + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - status := eCtx.Query("status") - inputTaskStatus, err := strconv.Atoi(status) + span.SetAttributes(attribute.String("bucket", bucket)) + + status, err := ExtractTaskStatusParameter(eCtx) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString("unknown status") } + span.SetAttributes(attribute.Int("status", status)) + taskStorage := s.state.GetTaskProcessor() tasks, err := taskStorage.GetBucketTasks(ctx, bucket) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } - taskStatus := task.TaskStatus(inputTaskStatus) + taskStatus := task.TaskStatus(status) foundedTasks := slices.DeleteFunc(tasks, func(task *task.Task) bool { return task.Status != taskStatus }) @@ -84,28 +95,30 @@ func (s *Server) LoadTasks(eCtx *fiber.Ctx) error { func (s *Server) LoadTaskByID(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() - bucket := eCtx.Params("bucket") - if bucket == "" { - return eCtx.Status(fiber.StatusBadRequest).SendString("bucket is required") - } + span := trace.SpanFromContext(ctx) - taskIDParam := eCtx.Params("task_id") - if taskIDParam == "" { - return eCtx.Status(fiber.StatusBadRequest).SendString("task_id is required") + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } - taskID, err := uuid.Parse(taskIDParam) + taskID, err := ExtractTaskIDParameter(eCtx) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) } taskStorage := s.state.GetTaskProcessor() foundedTask, err := taskStorage.GetTask(ctx, bucket, taskID) if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } taskSchema := form.TaskFromDomain(*foundedTask) - return eCtx.Status(fiber.StatusOK).JSON(taskSchema) } From c88c6d08a216f501837e43db8389d78cfa1e9f74 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Mon, 16 Mar 2026 18:26:22 +0300 Subject: [PATCH 23/47] chore: updated tracer and logger mw --- cmd/watchtower/httpserver/httpserver.go | 8 +- cmd/watchtower/httpserver/mw/logger.go | 111 ++++++++++++++++-------- cmd/watchtower/httpserver/mw/tracer.go | 2 +- 3 files changed, 78 insertions(+), 43 deletions(-) diff --git a/cmd/watchtower/httpserver/httpserver.go b/cmd/watchtower/httpserver/httpserver.go index bca4a1b..db34364 100644 --- a/cmd/watchtower/httpserver/httpserver.go +++ b/cmd/watchtower/httpserver/httpserver.go @@ -99,22 +99,22 @@ func (s *Server) Shutdown(_ kernel.Ctx) error { func (s *Server) initMeterMW() { prometheus := fiberprometheus.New(telemetry.AppName) prometheus.RegisterAt(s.Server, "/metrics") - prometheus.SetSkipPaths([]string{"/swagger"}) + prometheus.SetSkipPaths([]string{"/api/swagger"}) prometheus.SetIgnoreStatusCodes([]int{401, 403, 404}) s.Server.Use(prometheus.Middleware) } func (s *Server) initLoggerMW(logConfig telemetry.LoggerConfig) { + s.Server.Use(mw.LocalLoggerMiddleware(logConfig)) + if logConfig.EnableLoki { lokiLog := telemetry.InitLokiLogger(logConfig) s.Server.Use(mw.CreateLokiLoggerMW(&lokiLog)) - } else { - s.Server.Use(mw.InitLocalLogger(logConfig)) } } func (s *Server) initTracerMW(_ telemetry.TracerConfig) { s.Server.Use(otelfiber.Middleware( - otelfiber.WithNext(mw.TracerURLSkipper), + otelfiber.WithNext(mw.TraceURLSkipper), )) } diff --git a/cmd/watchtower/httpserver/mw/logger.go b/cmd/watchtower/httpserver/mw/logger.go index 38bdd93..53614d5 100644 --- a/cmd/watchtower/httpserver/mw/logger.go +++ b/cmd/watchtower/httpserver/mw/logger.go @@ -3,40 +3,83 @@ package mw import ( "context" "encoding/json" - "fmt" "log/slog" + "os" "slices" - "strings" "time" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/google/uuid" "watchtower/internal/shared/telemetry" ) -func InitLocalLogger(config telemetry.LoggerConfig) fiber.Handler { - return logger.New( - logger.Config{ - TimeFormat: "2006/01/02 15:04:05", - Format: fmt.Sprintf( - "%s %s http-response={%s %s %s %s %s}\n", - "${time_custom}", - config.Level, - "method=${method}", - "uri=${path}", - "latency=${latency}", - "status=${status}", - "error=\"${error}\"", - ), - Next: func(eCtx *fiber.Ctx) bool { - uri := eCtx.Request().URI().String() - excludeSwagger := strings.Contains(uri, "swagger") - excludeMetrics := strings.Contains(uri, "metrics") - return excludeSwagger || excludeMetrics - }, - }, - ) +func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { + var logLevel = slog.LevelInfo + switch config.Level { + case "debug": + logLevel = slog.LevelDebug + case "info": + logLevel = slog.LevelInfo + case "warn": + logLevel = slog.LevelWarn + case "error": + logLevel = slog.LevelError + } + + handleOpts := &slog.HandlerOptions{ + Level: logLevel, + } + + textHandler := slog.NewTextHandler(os.Stdout, handleOpts) + localLogger := slog.New(textHandler) + + return func(eCtx *fiber.Ctx) error { + requestID := eCtx.Get("X-Request-ID") + if requestID == "" { + requestID = uuid.New().String() + eCtx.Set("X-Request-ID", requestID) + } + + startTime := time.Now() + + ctx := context.WithValue(eCtx.UserContext(), "request_id", requestID) + eCtx.SetUserContext(ctx) + + err := eCtx.Next() + + latency := time.Since(startTime) + + statusCode := eCtx.Response().StatusCode() + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + statusCode = fiberErr.Code + } + } + + var responseMsg = "Ok" + var level = slog.LevelInfo + if statusCode >= 300 { + level = slog.LevelError + responseMsg = string(eCtx.Response().Body()) + } + + localLogger.LogAttrs(ctx, level, "http-request", + slog.String("request_id", requestID), + slog.String("method", eCtx.Method()), + slog.String("uri", eCtx.OriginalURL()), + slog.Int("status", statusCode), + slog.String("message", responseMsg), + slog.Int("bytes_received", len(eCtx.Request().Body())), + slog.Int("bytes_sent", len(eCtx.Response().Body())), + slog.Duration("latency", latency), + slog.String("referer", eCtx.Get("Referer")), + slog.String("client_ip", eCtx.IP()), + slog.String("user_agent", eCtx.Get("User-Agent")), + ) + + return err + } } func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { @@ -54,11 +97,11 @@ func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { latency := time.Since(start) - var responseMsg string - if eCtx.Response().StatusCode() >= 200 { - responseMsg = "Ok" - } else { - responseMsg = eCtx.Response().String() + var responseMsg = "Ok" + var logLevel = slog.LevelInfo + if eCtx.Response().StatusCode() >= 300 { + logLevel = slog.LevelError + responseMsg = string(eCtx.Response().Body()) } logMessage := map[string]interface{}{ @@ -72,14 +115,6 @@ func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { } jsonMessage, _ := json.Marshal(logMessage) - var logLevel slog.Level - statusCategory := eCtx.Response().StatusCode() / 100 - if statusCategory < 3 && statusCategory >= 2 { - logLevel = slog.LevelInfo - } else { - logLevel = slog.LevelError - } - ctx, cancel := context.WithTimeout(eCtx.Context(), 5*time.Second) sll.Client.Log(ctx, logLevel, string(jsonMessage)) defer cancel() diff --git a/cmd/watchtower/httpserver/mw/tracer.go b/cmd/watchtower/httpserver/mw/tracer.go index 1442ab5..996f22e 100644 --- a/cmd/watchtower/httpserver/mw/tracer.go +++ b/cmd/watchtower/httpserver/mw/tracer.go @@ -16,7 +16,7 @@ var ( } ) -func TracerURLSkipper(eCtx *fiber.Ctx) bool { +func TraceURLSkipper(eCtx *fiber.Ctx) bool { for _, excluded := range excludedPaths { if strings.HasPrefix(eCtx.Path(), excluded) { return true From 24cae118eef02eb57620b97decd0af166b0c5641 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Mon, 16 Mar 2026 18:31:15 +0300 Subject: [PATCH 24/47] chore: updated tracer and logger mw --- cmd/watchtower/httpserver/mw/logger.go | 6 +++--- go.mod | 1 + go.sum | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/watchtower/httpserver/mw/logger.go b/cmd/watchtower/httpserver/mw/logger.go index 53614d5..189a8e8 100644 --- a/cmd/watchtower/httpserver/mw/logger.go +++ b/cmd/watchtower/httpserver/mw/logger.go @@ -8,6 +8,7 @@ import ( "slices" "time" + "github.com/Marlliton/slogpretty" "github.com/gofiber/fiber/v2" "github.com/google/uuid" @@ -27,11 +28,10 @@ func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { logLevel = slog.LevelError } - handleOpts := &slog.HandlerOptions{ + textHandler := slogpretty.New(os.Stdout, &slogpretty.Options{ Level: logLevel, - } + }) - textHandler := slog.NewTextHandler(os.Stdout, handleOpts) localLogger := slog.New(textHandler) return func(eCtx *fiber.Ctx) error { diff --git a/go.mod b/go.mod index 39e0653..c7f7a6f 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/Marlliton/slogpretty v0.1.3 // indirect github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/ansrivas/fiberprometheus/v2 v2.17.0 // indirect diff --git a/go.sum b/go.sum index 32fc04c..824cf45 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Marlliton/slogpretty v0.1.3 h1:kLYjcKtFqikoCrXVMaI2R6fBy9pcJwoBJKdkhwGgoB4= +github.com/Marlliton/slogpretty v0.1.3/go.mod h1:vEC85AhV7Obb264VOAUMIBvwE3ivRSad6djal/v2sYU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c h1:AMDVOKGaiqse4qiRXSzRgpC9DCNTHCx6zpzdtXXrKM4= From f0210c1138db7623331a61f58a1e7b7830fe87ce Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 17 Mar 2026 06:13:55 +0300 Subject: [PATCH 25/47] fix: makefile launching binary --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 13ac78f..e820dd1 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ build: go build -v -o $(SERVICE_BIN_FILE_PATH) ./cmd/watchtower run: build - $(SERVICE_BIN_FILE_PATH) -c ./configs/config.toml + $(SERVICE_BIN_FILE_PATH) -d test: go test -race ./tests/... From ea3946e56f6327c1f7c86dccdfdc4820ddf26a8a Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 17 Mar 2026 13:03:28 +0300 Subject: [PATCH 26/47] fix: config parsing --- cmd/config.go | 3 +- cmd/watchtower/httpserver/form/form.go | 5 + cmd/watchtower/httpserver/helper.go | 2 +- cmd/watchtower/httpserver/mw/logger.go | 13 +- cmd/watchtower/httpserver/routes_object.go | 144 ++++++++++++++++++++- 5 files changed, 158 insertions(+), 9 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 67a8a9a..2b90b6f 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -72,7 +72,6 @@ func InitConfig() (*Config, error) { viperInst.AddConfigPath(".") viperInst.AddConfigPath("./configs") - viperInst.AddConfigPath("../configs") if err := viperInst.ReadInConfig(); err != nil { //nolint @@ -86,7 +85,7 @@ func InitConfig() (*Config, error) { setupEnv(viperInst) config := &Config{} - if err := viper.Unmarshal(config); err != nil { + if err := viperInst.Unmarshal(config); err != nil { confErr := fmt.Errorf("failed while unmarshaling config: %w", err) return config, confErr } diff --git a/cmd/watchtower/httpserver/form/form.go b/cmd/watchtower/httpserver/form/form.go index 5af89df..11419d5 100644 --- a/cmd/watchtower/httpserver/form/form.go +++ b/cmd/watchtower/httpserver/form/form.go @@ -49,3 +49,8 @@ type CopyFileForm struct { SrcPath string `json:"src_path" example:"old-test-document.docx"` DstPath string `json:"dst_path" example:"test-document.docx"` } + +// FolderForm example +type FolderForm struct { + Prefix string `json:"prefix" example:"test-folder"` +} diff --git a/cmd/watchtower/httpserver/helper.go b/cmd/watchtower/httpserver/helper.go index fa423a0..e71035e 100644 --- a/cmd/watchtower/httpserver/helper.go +++ b/cmd/watchtower/httpserver/helper.go @@ -75,7 +75,7 @@ func ExtractExpiredDatetime(eCtx *fiber.Ctx) (*time.Time, error) { expired := eCtx.Query("expired") if expired == "" { slog.Debug("expired parameter has not been set") - return nil, nil + return &time.Time{}, nil } timeVal, err := time.Parse(time.RFC3339, expired) diff --git a/cmd/watchtower/httpserver/mw/logger.go b/cmd/watchtower/httpserver/mw/logger.go index 189a8e8..332b7df 100644 --- a/cmd/watchtower/httpserver/mw/logger.go +++ b/cmd/watchtower/httpserver/mw/logger.go @@ -15,6 +15,11 @@ import ( "watchtower/internal/shared/telemetry" ) +const ( + XRequestIDHeaderKey = "X-Request-ID" + ContextRequestIDKey = "request_id" +) + func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { var logLevel = slog.LevelInfo switch config.Level { @@ -35,15 +40,16 @@ func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { localLogger := slog.New(textHandler) return func(eCtx *fiber.Ctx) error { - requestID := eCtx.Get("X-Request-ID") + requestID := eCtx.Get(XRequestIDHeaderKey) if requestID == "" { requestID = uuid.New().String() - eCtx.Set("X-Request-ID", requestID) + eCtx.Set(XRequestIDHeaderKey, requestID) } startTime := time.Now() - ctx := context.WithValue(eCtx.UserContext(), "request_id", requestID) + //nolint + ctx := context.WithValue(eCtx.UserContext(), ContextRequestIDKey, requestID) eCtx.SetUserContext(ctx) err := eCtx.Next() @@ -52,6 +58,7 @@ func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { statusCode := eCtx.Response().StatusCode() if err != nil { + //nolint if fiberErr, ok := err.(*fiber.Error); ok { statusCode = fiberErr.Code } diff --git a/cmd/watchtower/httpserver/routes_object.go b/cmd/watchtower/httpserver/routes_object.go index dc377fd..5aa0283 100644 --- a/cmd/watchtower/httpserver/routes_object.go +++ b/cmd/watchtower/httpserver/routes_object.go @@ -6,23 +6,28 @@ import ( "fmt" "log/slog" "net/http" + "path" "time" - "watchtower/cmd/watchtower/httpserver/form" - "watchtower/internal/core/cloud/domain" - "github.com/gofiber/fiber/v2" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" + + "watchtower/cmd/watchtower/httpserver/form" + "watchtower/internal/core/cloud/domain" ) +const FolderFileKeeper = ".keeper" + func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { group.Post("/cloud/:bucket/files", s.GetFiles) group.Post("/cloud/:bucket/file/copy", s.CopyFile) group.Post("/cloud/:bucket/file/move", s.MoveFile) group.Put("/cloud/:bucket/file/upload", s.UploadFile) group.Post("/cloud/:bucket/file/download", s.DownloadFile) + group.Post("/cloud/:bucket/folder", s.CreateFolder) + group.Delete("/cloud/:bucket/folder", s.DeleteFolder) group.Delete("/cloud/:bucket/file", s.RemoveFile2) group.Delete("/cloud/:bucket/file/remove", s.RemoveFile) group.Post("/cloud/:bucket/file/attributes", s.GetFileInfo) @@ -44,6 +49,7 @@ func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { // @Failure 500 {object} form.InternalServerError "Internal server error" // @Failure 503 {object} form.ServerUnavailableError "Server does not available" // @Router /api/v1/cloud/{bucket}/file/copy [post] +// nolint func (s *Server) CopyFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() @@ -94,6 +100,7 @@ func (s *Server) CopyFile(eCtx *fiber.Ctx) error { // @Failure 500 {object} form.InternalServerError "Internal server error" // @Failure 503 {object} form.ServerUnavailableError "Server does not available" // @Router /api/v1/cloud/{bucket}/file/move [post] +// nolint func (s *Server) MoveFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() @@ -129,6 +136,137 @@ func (s *Server) MoveFile(eCtx *fiber.Ctx) error { return eCtx.Status(fiber.StatusOK).SendString("Ok") } +// CreateFolder +// @Summary Create empty folder into cloud storage +// @Description Create empty folder into cloud storage +// @ID create-folder +// @Tags files +// @Accept application/json +// @Produce json +// @Param bucket path string true "Bucket name to create folder" +// @Param jsonQuery body form.FolderForm true "Params to create folder" +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/folder [post] +func (s *Server) CreateFolder(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) + + objectStorage := s.state.GetObjectStorage() + exist, err := objectStorage.IsBucketExists(ctx, bucket) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(http.StatusBadRequest).SendString(err.Error()) + } + + if !exist { + err = fmt.Errorf("specified bucket %s does not exist", bucket) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(http.StatusNotFound).SendString(err.Error()) + } + + var jsonForm form.FolderForm + err = json.Unmarshal(eCtx.Body(), &jsonForm) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + keepFilePath := path.Join(jsonForm.Prefix, FolderFileKeeper) + params := &domain.UploadObjectParams{ + FilePath: keepFilePath, + FileData: bytes.NewBufferString(""), + Expired: nil, + } + + _, err = objectStorage.StoreObject(ctx, bucket, params) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + + return eCtx.Status(fiber.StatusCreated).SendString("Ok") +} + +// DeleteFolder +// @Summary Delete folder into cloud storage +// @Description Delete empty folder into cloud storage +// @ID delete-folder +// @Tags files +// @Accept application/json +// @Produce json +// @Param bucket path string true "Bucket name to delete folder" +// @Param jsonQuery body form.FolderForm true "Params to delete folder" +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/folder [delete] +func (s *Server) DeleteFolder(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + span.SetAttributes(attribute.String("bucket", bucket)) + + objectStorage := s.state.GetObjectStorage() + exist, err := objectStorage.IsBucketExists(ctx, bucket) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(http.StatusBadRequest).SendString(err.Error()) + } + + if !exist { + err = fmt.Errorf("specified bucket %s does not exist", bucket) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(http.StatusNotFound).SendString(err.Error()) + } + + var jsonForm form.FolderForm + err = json.Unmarshal(eCtx.Body(), &jsonForm) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + err = objectStorage.DeleteObjects(ctx, bucket, jsonForm.Prefix) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + + return eCtx.Status(fiber.StatusOK).SendString("Ok") +} + // UploadFile // @Summary Upload files to cloud // @Description Upload files to cloud From e675dcbea0a59770bf9a0e687abb6f83a6930ab3 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 17 Mar 2026 13:04:11 +0300 Subject: [PATCH 27/47] feature: impled creating and deleting folder into cloud --- internal/core/cloud/application/usecase.go | 20 +++++++++++++++- internal/core/cloud/domain/storage.go | 20 ++++++++++++++++ internal/core/cloud/infrastructure/s3/s3.go | 26 +++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/internal/core/cloud/application/usecase.go b/internal/core/cloud/application/usecase.go index 83ef6dd..87d8207 100644 --- a/internal/core/cloud/application/usecase.go +++ b/internal/core/cloud/application/usecase.go @@ -147,6 +147,25 @@ func (s *StorageUseCase) DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, return nil } +func (s *StorageUseCase) DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, prefix string) error { + ctx, span := telemetry.GlobalTracer.Start(ctx, "copy-object") + defer span.End() + + span.SetAttributes( + attribute.String("bucket", bucketID), + attribute.String("prefix", prefix), + ) + + err := s.cloudStorage.DeleteObjects(ctx, bucketID, prefix) + if err != nil { + err = fmt.Errorf("failed to remove objects %s: %w", bucketID, err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return err + } + return nil +} + func (s *StorageUseCase) MoveObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { ctx, span := telemetry.GlobalTracer.Start(ctx, "move-file") defer span.End() @@ -211,7 +230,6 @@ func (s *StorageUseCase) StoreObject( span.SetAttributes( attribute.String("bucket", bucketID), attribute.String("file-path", params.FilePath), - attribute.Int64("expired", params.Expired.Unix()), attribute.Int("data-len", params.FileData.Len()), ) diff --git a/internal/core/cloud/domain/storage.go b/internal/core/cloud/domain/storage.go index 3e49018..149f034 100644 --- a/internal/core/cloud/domain/storage.go +++ b/internal/core/cloud/domain/storage.go @@ -204,6 +204,26 @@ type IObjectManager interface { // // Handle error, but ignore "not found" as it's already gone // } DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error + + // DeleteObjects permanently removes an objects from storage. + // This operation cannot be undone. + // + // Parameters: + // - kernel.Ctx: Context for cancellation and timeout + // - bucketID: ID of the bucket containing the object + // - prefix: relative path of object to delete + // + // Returns: + // - error: ErrObjectNotFound if object doesn't exist (idempotent), + // ErrBucketNotFound if bucket doesn't exist, + // or other provider-specific errors + // + // Example: + // err := storage.DeleteObjects(ctx, "temp-files", "cache") + // if err != nil && !errors.Is(err, ErrObjectNotFound) { + // // Handle error, but ignore "not found" as it's already gone + // } + DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, prefix string) error } // IObjectWalker defines operations for listing and iterating through objects in a bucket. diff --git a/internal/core/cloud/infrastructure/s3/s3.go b/internal/core/cloud/infrastructure/s3/s3.go index 725ea25..3b63570 100644 --- a/internal/core/cloud/infrastructure/s3/s3.go +++ b/internal/core/cloud/infrastructure/s3/s3.go @@ -168,6 +168,32 @@ func (s *S3Client) CopyObject(ctx kernel.Ctx, bucketID kernel.BucketID, params * return nil } +func (s *S3Client) DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, prefix string) error { + listObjOpts := minio.ListObjectsOptions{ + Prefix: prefix, + Recursive: true, + UseV1: true, + } + objInfoCh := s.mc.ListObjects(ctx, bucketID, listObjOpts) + + removeObjOpts := minio.RemoveObjectsOptions{ + GovernanceBypass: true, + } + errCh := s.mc.RemoveObjects(ctx, bucketID, objInfoCh, removeObjOpts) + for err := range errCh { + if err.Err != nil { + slog.Warn("failed to delete object", + slog.String("bucket", bucketID), + slog.String("prefix", prefix), + slog.String("error", err.ObjectName), + slog.String("err", err.Err.Error()), + ) + } + } + + return nil +} + func (s *S3Client) DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error { opts := minio.RemoveObjectOptions{} filePath := path.Clean(objID) From bbaba2de2d5e422a67b18fe0be8ffe6e0851a28d Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 17 Mar 2026 13:04:31 +0300 Subject: [PATCH 28/47] chore: updated tests after all changes --- tests/common/mocks/object_storage.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/common/mocks/object_storage.go b/tests/common/mocks/object_storage.go index 8cb6b8b..679f265 100644 --- a/tests/common/mocks/object_storage.go +++ b/tests/common/mocks/object_storage.go @@ -2,10 +2,11 @@ package mocks import ( "net/url" - "watchtower/internal/core/cloud/domain" - "watchtower/internal/shared/kernel" "github.com/stretchr/testify/mock" + + "watchtower/internal/core/cloud/domain" + "watchtower/internal/shared/kernel" ) type MockObjectStorage struct { @@ -69,6 +70,11 @@ func (m *MockObjectStorage) DeleteObject(_ kernel.Ctx, bucketID kernel.BucketID, return args.Error(0) } +func (m *MockObjectStorage) DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, prefix string) error { + args := m.Called(bucketID, prefix) + return args.Error(0) +} + func (m *MockObjectStorage) GetBucketObjects( _ kernel.Ctx, bucketID kernel.BucketID, From d26e09a5df16601d6252b445e97dde5c40e9e37e Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 17 Mar 2026 13:04:34 +0300 Subject: [PATCH 29/47] chore: updated swagger docs after all changes --- docs/docs.go | 145 ++++++++++++++++++++++++++++++++++++++++++++-- docs/swagger.json | 145 ++++++++++++++++++++++++++++++++++++++++++++-- docs/swagger.yaml | 97 +++++++++++++++++++++++++++++-- 3 files changed, 371 insertions(+), 16 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index c14b307..fba0e5b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -89,12 +89,6 @@ const docTemplate = `{ } } }, - "400": { - "description": "Bad Request error", - "schema": { - "$ref": "#/definitions/form.BadRequestError" - } - }, "500": { "description": "Internal server error", "schema": { @@ -754,6 +748,136 @@ const docTemplate = `{ } } }, + "/api/v1/cloud/{bucket}/folder": { + "post": { + "description": "Create empty folder into cloud storage", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Create empty folder into cloud storage", + "operationId": "create-folder", + "parameters": [ + { + "type": "string", + "description": "Bucket name to create folder", + "name": "bucket", + "in": "path", + "required": true + }, + { + "description": "Params to create folder", + "name": "jsonQuery", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.FolderForm" + } + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/form.Success" + } + }, + "400": { + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" + } + }, + "503": { + "description": "Server does not available", + "schema": { + "$ref": "#/definitions/form.ServerUnavailableError" + } + } + } + }, + "delete": { + "description": "Delete empty folder into cloud storage", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Delete folder into cloud storage", + "operationId": "delete-folder", + "parameters": [ + { + "type": "string", + "description": "Bucket name to delete folder", + "name": "bucket", + "in": "path", + "required": true + }, + { + "description": "Params to delete folder", + "name": "jsonQuery", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.FolderForm" + } + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/form.Success" + } + }, + "400": { + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" + } + }, + "503": { + "description": "Server does not available", + "schema": { + "$ref": "#/definitions/form.ServerUnavailableError" + } + } + } + } + }, "/api/v1/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", @@ -944,6 +1068,15 @@ const docTemplate = `{ } } }, + "form.FolderForm": { + "type": "object", + "properties": { + "prefix": { + "type": "string", + "example": "test-folder" + } + } + }, "form.GetFileAttributesForm": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index e566c73..98bcee5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -78,12 +78,6 @@ } } }, - "400": { - "description": "Bad Request error", - "schema": { - "$ref": "#/definitions/form.BadRequestError" - } - }, "500": { "description": "Internal server error", "schema": { @@ -743,6 +737,136 @@ } } }, + "/api/v1/cloud/{bucket}/folder": { + "post": { + "description": "Create empty folder into cloud storage", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Create empty folder into cloud storage", + "operationId": "create-folder", + "parameters": [ + { + "type": "string", + "description": "Bucket name to create folder", + "name": "bucket", + "in": "path", + "required": true + }, + { + "description": "Params to create folder", + "name": "jsonQuery", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.FolderForm" + } + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/form.Success" + } + }, + "400": { + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" + } + }, + "503": { + "description": "Server does not available", + "schema": { + "$ref": "#/definitions/form.ServerUnavailableError" + } + } + } + }, + "delete": { + "description": "Delete empty folder into cloud storage", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Delete folder into cloud storage", + "operationId": "delete-folder", + "parameters": [ + { + "type": "string", + "description": "Bucket name to delete folder", + "name": "bucket", + "in": "path", + "required": true + }, + { + "description": "Params to delete folder", + "name": "jsonQuery", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/form.FolderForm" + } + } + ], + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/form.Success" + } + }, + "400": { + "description": "Bad Request error", + "schema": { + "$ref": "#/definitions/form.BadRequestError" + } + }, + "404": { + "description": "Bucket not found", + "schema": { + "$ref": "#/definitions/form.NotFoundError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/form.InternalServerError" + } + }, + "503": { + "description": "Server does not available", + "schema": { + "$ref": "#/definitions/form.ServerUnavailableError" + } + } + } + } + }, "/api/v1/tasks/{bucket}": { "get": { "description": "Load tasks (processing/unrecognized/done) of uploaded files", @@ -933,6 +1057,15 @@ } } }, + "form.FolderForm": { + "type": "object", + "properties": { + "prefix": { + "type": "string", + "example": "test-folder" + } + } + }, "form.GetFileAttributesForm": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c2c943b..15af7ce 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -38,6 +38,12 @@ definitions: example: test-file.docx type: string type: object + form.FolderForm: + properties: + prefix: + example: test-folder + type: string + type: object form.GetFileAttributesForm: properties: file_path: @@ -553,6 +559,93 @@ paths: summary: Get files list into bucket tags: - files + /api/v1/cloud/{bucket}/folder: + delete: + consumes: + - application/json + description: Delete empty folder into cloud storage + operationId: delete-folder + parameters: + - description: Bucket name to delete folder + in: path + name: bucket + required: true + type: string + - description: Params to delete folder + in: body + name: jsonQuery + required: true + schema: + $ref: '#/definitions/form.FolderForm' + produces: + - application/json + responses: + "200": + description: Ok + schema: + $ref: '#/definitions/form.Success' + "400": + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' + "503": + description: Server does not available + schema: + $ref: '#/definitions/form.ServerUnavailableError' + summary: Delete folder into cloud storage + tags: + - files + post: + consumes: + - application/json + description: Create empty folder into cloud storage + operationId: create-folder + parameters: + - description: Bucket name to create folder + in: path + name: bucket + required: true + type: string + - description: Params to create folder + in: body + name: jsonQuery + required: true + schema: + $ref: '#/definitions/form.FolderForm' + produces: + - application/json + responses: + "200": + description: Ok + schema: + $ref: '#/definitions/form.Success' + "400": + description: Bad Request error + schema: + $ref: '#/definitions/form.BadRequestError' + "404": + description: Bucket not found + schema: + $ref: '#/definitions/form.NotFoundError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/form.InternalServerError' + "503": + description: Server does not available + schema: + $ref: '#/definitions/form.ServerUnavailableError' + summary: Create empty folder into cloud storage + tags: + - files /api/v1/cloud/bucket: put: consumes: @@ -601,10 +694,6 @@ paths: items: $ref: '#/definitions/form.BucketSchema' type: array - "400": - description: Bad Request error - schema: - $ref: '#/definitions/form.BadRequestError' "500": description: Internal server error schema: From 85a6607006575e15f98d2da9c7cb7c51cc3e9d66 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 17 Mar 2026 13:11:52 +0300 Subject: [PATCH 30/47] chore: added missing URIs filtering --- cmd/watchtower/httpserver/mw/logger.go | 23 +++++++++++++++++++---- internal/shared/telemetry/logger.go | 6 +++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/cmd/watchtower/httpserver/mw/logger.go b/cmd/watchtower/httpserver/mw/logger.go index 332b7df..497c054 100644 --- a/cmd/watchtower/httpserver/mw/logger.go +++ b/cmd/watchtower/httpserver/mw/logger.go @@ -5,7 +5,7 @@ import ( "encoding/json" "log/slog" "os" - "slices" + "strings" "time" "github.com/Marlliton/slogpretty" @@ -40,6 +40,10 @@ func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { localLogger := slog.New(textHandler) return func(eCtx *fiber.Ctx) error { + if CheckFilteredURI(telemetry.FilterURI, eCtx.Path()) { + return eCtx.Next() + } + requestID := eCtx.Get(XRequestIDHeaderKey) if requestID == "" { requestID = uuid.New().String() @@ -91,7 +95,7 @@ func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { return func(eCtx *fiber.Ctx) error { - if slices.Contains(sll.FilterURI, eCtx.Path()) { + if CheckFilteredURI(sll.FilterURI, eCtx.Path()) { return eCtx.Next() } @@ -106,7 +110,8 @@ func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { var responseMsg = "Ok" var logLevel = slog.LevelInfo - if eCtx.Response().StatusCode() >= 300 { + statusCode := eCtx.Response().StatusCode() + if statusCode >= 300 { logLevel = slog.LevelError responseMsg = string(eCtx.Response().Body()) } @@ -114,7 +119,7 @@ func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { logMessage := map[string]interface{}{ "message": responseMsg, "latency": latency.String(), - "status": eCtx.Response().StatusCode(), + "status": statusCode, "method": eCtx.Method(), "uri": eCtx.Path(), "client_ip": eCtx.IP(), @@ -129,3 +134,13 @@ func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { return err } } + +func CheckFilteredURI(filteredURI []string, currURI string) bool { + for _, filtered := range filteredURI { + if strings.HasPrefix(currURI, filtered) { + return true + } + } + + return false +} diff --git a/internal/shared/telemetry/logger.go b/internal/shared/telemetry/logger.go index 151e2e5..2f04ffa 100644 --- a/internal/shared/telemetry/logger.go +++ b/internal/shared/telemetry/logger.go @@ -9,9 +9,9 @@ import ( ) var ( - filterURI = []string{ + FilterURI = []string{ "/metrics", - "/swagger/*", + "/api/swagger", } ) @@ -34,5 +34,5 @@ func InitLokiLogger(config LoggerConfig) SlogLokiLogger { With("detected_level", config.Level). With("level", config.Level) - return SlogLokiLogger{Client: logger, FilterURI: filterURI} + return SlogLokiLogger{Client: logger, FilterURI: FilterURI} } From 662ce0f7af5c8e6df23e705e1c6604a40b082bec Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Fri, 27 Mar 2026 12:11:43 +0300 Subject: [PATCH 31/47] fix: linter warnings --- cmd/watchtower/httpserver/routes_object.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/watchtower/httpserver/routes_object.go b/cmd/watchtower/httpserver/routes_object.go index 5aa0283..09ec3b4 100644 --- a/cmd/watchtower/httpserver/routes_object.go +++ b/cmd/watchtower/httpserver/routes_object.go @@ -49,7 +49,7 @@ func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { // @Failure 500 {object} form.InternalServerError "Internal server error" // @Failure 503 {object} form.ServerUnavailableError "Server does not available" // @Router /api/v1/cloud/{bucket}/file/copy [post] -// nolint +//nolint func (s *Server) CopyFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() @@ -100,7 +100,7 @@ func (s *Server) CopyFile(eCtx *fiber.Ctx) error { // @Failure 500 {object} form.InternalServerError "Internal server error" // @Failure 503 {object} form.ServerUnavailableError "Server does not available" // @Router /api/v1/cloud/{bucket}/file/move [post] -// nolint +//nolint func (s *Server) MoveFile(eCtx *fiber.Ctx) error { ctx := eCtx.UserContext() From d5200b8838fb837dfc4e19510a01c13921893f9e Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Mon, 30 Mar 2026 20:29:58 +0300 Subject: [PATCH 32/47] chore: impled graceful shutdown --- cmd/config.go | 17 +- cmd/watchtower/httpserver/httpserver.go | 81 +++++----- cmd/watchtower/httpserver/mw/logger.go | 146 ------------------ cmd/watchtower/httpserver/mw/tracer.go | 27 ---- cmd/watchtower/httpserver/routes_task.go | 6 +- cmd/watchtower/watchtower.go | 37 +++-- go.mod | 19 +-- go.sum | 38 +++++ internal/core/cloud/application/usecase.go | 28 ++-- internal/core/cloud/infrastructure/s3/s3.go | 2 + internal/process/orchestrator.go | 16 +- internal/shared/kernel/name.go | 5 + internal/shared/metrics/metrics.go | 29 ++++ internal/shared/telemetry/config.go | 17 -- internal/shared/telemetry/logger.go | 38 ----- internal/shared/telemetry/tracer.go | 66 -------- internal/shared/utils/sender.go | 6 +- internal/support/task/application/usecase.go | 14 +- .../task/infrastructure/redis/redis.go | 10 +- .../support/task/infrastructure/rmq/rmq.go | 83 +++++----- tests/common/env_app_server.go | 10 +- tests/common/usecase.go | 7 +- 22 files changed, 255 insertions(+), 447 deletions(-) delete mode 100644 cmd/watchtower/httpserver/mw/logger.go delete mode 100644 cmd/watchtower/httpserver/mw/tracer.go create mode 100644 internal/shared/kernel/name.go create mode 100644 internal/shared/metrics/metrics.go delete mode 100644 internal/shared/telemetry/config.go delete mode 100644 internal/shared/telemetry/logger.go delete mode 100644 internal/shared/telemetry/tracer.go diff --git a/cmd/config.go b/cmd/config.go index 2b90b6f..6bacfc9 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -6,12 +6,12 @@ import ( "os" "strings" + otlp_go "github.com/breadrock1/otlp-go/otlp" "github.com/spf13/viper" "watchtower/cmd/watchtower/httpserver" "watchtower/internal/core/cloud/infrastructure/s3" "watchtower/internal/process" - "watchtower/internal/shared/telemetry" "watchtower/internal/support/task/infrastructure/docparser" "watchtower/internal/support/task/infrastructure/docsearch" "watchtower/internal/support/task/infrastructure/redis" @@ -19,11 +19,11 @@ import ( ) type Config struct { - Orchestrator process.Config `mapstructure:"orchestrator"` - Otlp telemetry.OtlpConfig `mapstructure:"otlp"` - Server ServerConfig `mapstructure:"server"` - Storage StorageConfig `mapstructure:"storage"` - Task TaskConfig `mapstructure:"task"` + Otlp otlp_go.OtlpConfig `mapstructure:"otlp"` + Orchestrator process.Config `mapstructure:"orchestrator"` + Server ServerConfig `mapstructure:"server"` + Storage StorageConfig `mapstructure:"storage"` + Task TaskConfig `mapstructure:"task"` } type ServerConfig struct { @@ -73,6 +73,11 @@ func InitConfig() (*Config, error) { viperInst.AddConfigPath(".") viperInst.AddConfigPath("./configs") + if launchMode == defaultLaunchMode { + // Used to include config from integration tests + viperInst.AddConfigPath("../../configs") + } + if err := viperInst.ReadInConfig(); err != nil { //nolint if _, ok := err.(viper.ConfigFileNotFoundError); ok { diff --git a/cmd/watchtower/httpserver/httpserver.go b/cmd/watchtower/httpserver/httpserver.go index db34364..fecdd22 100644 --- a/cmd/watchtower/httpserver/httpserver.go +++ b/cmd/watchtower/httpserver/httpserver.go @@ -2,23 +2,23 @@ package httpserver import ( "fmt" + "log/slog" - "go.opentelemetry.io/otel/trace" - - "github.com/ansrivas/fiberprometheus/v2" - "github.com/gofiber/contrib/otelfiber/v2" + "github.com/breadrock1/otlp-go/otlp" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/monitor" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/swagger" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/otel/trace" - "watchtower/cmd/watchtower/httpserver/mw" + _ "watchtower/docs" "watchtower/internal/process" "watchtower/internal/shared/kernel" - "watchtower/internal/shared/telemetry" - _ "watchtower/docs" + otlppfiber "github.com/breadrock1/otlp-go/pkg/fiber" ) // Server @@ -50,32 +50,34 @@ type Server struct { Server *fiber.App } -func SetupServer( - otlpConfig telemetry.OtlpConfig, - state *process.Orchestrator, - tracer trace.Tracer, -) *Server { +func SetupServer(otlpConfig otlp_go.OtlpConfig, state *process.Orchestrator) *Server { + tracer, err := otlp_go.InitTracer(otlpConfig.Tracer) + if err != nil { + slog.Warn("failed to init tracer", slog.String("err", err.Error())) + } + serverApp := &Server{ tracer: tracer, state: state, } - serverApp.Server = fiber.New() + serverApp.Server = fiber.New( + fiber.Config{ + DisableStartupMessage: true, + }, + ) - serverApp.Server.Use(cors.New(cors.Config{})) - serverApp.Server.Use(recover.New()) - - serverApp.initMeterMW() - serverApp.initTracerMW(otlpConfig.Tracer) - serverApp.initLoggerMW(otlpConfig.Logger) + serverApp.initMiddlewares(otlpConfig) + serverApp.Server.Get("/", serverApp.Home) serverApp.Server.Get("/monitor", monitor.New()) + serverApp.Server.Get("/processing/metrics", adaptor.HTTPHandler(promhttp.Handler())) api := serverApp.Server.Group("/api") + api.Get("/swagger/*", swagger.HandlerDefault) v1Api := api.Group("/v1") - serverApp.CreateSystemGroup(v1Api) serverApp.CreateTasksGroup(v1Api) serverApp.CreateStorageBucketsGroup(v1Api) @@ -84,37 +86,32 @@ func SetupServer( return serverApp } -func (s *Server) Start(_ kernel.Ctx, config Config) error { +func (s *Server) Start(config Config) error { + slog.Info("starting http server", slog.String("address", config.Address)) if err := s.Server.Listen(config.Address); err != nil { - return fmt.Errorf("failed to start Server: %w", err) + return fmt.Errorf("failed to start server: %w", err) } return nil } -func (s *Server) Shutdown(_ kernel.Ctx) error { - return s.Server.Shutdown() +func (s *Server) Shutdown(ctx kernel.Ctx) error { + slog.Info("http server shutting down") + return s.Server.ShutdownWithContext(ctx) } -func (s *Server) initMeterMW() { - prometheus := fiberprometheus.New(telemetry.AppName) - prometheus.RegisterAt(s.Server, "/metrics") - prometheus.SetSkipPaths([]string{"/api/swagger"}) - prometheus.SetIgnoreStatusCodes([]int{401, 403, 404}) - s.Server.Use(prometheus.Middleware) -} +func (s *Server) initMiddlewares(otlpConfig otlp_go.OtlpConfig) { + s.Server.Use(cors.New(cors.Config{})) + s.Server.Use(recover.New()) -func (s *Server) initLoggerMW(logConfig telemetry.LoggerConfig) { - s.Server.Use(mw.LocalLoggerMiddleware(logConfig)) + s.Server.Use(otlppfiber.PrometheusMeterMiddleware(s.Server)) + s.Server.Use(otlppfiber.OtlpJaegerTracerMiddleware()) - if logConfig.EnableLoki { - lokiLog := telemetry.InitLokiLogger(logConfig) - s.Server.Use(mw.CreateLokiLoggerMW(&lokiLog)) - } -} + logger := otlp_go.InitLocalLogger(otlpConfig.Logger) + slog.SetDefault(logger) -func (s *Server) initTracerMW(_ telemetry.TracerConfig) { - s.Server.Use(otelfiber.Middleware( - otelfiber.WithNext(mw.TraceURLSkipper), - )) + s.Server.Use(otlppfiber.StdoutLoggerMiddleware(otlpConfig.Logger)) + if otlpConfig.Logger.EnableLoki { + s.Server.Use(otlppfiber.RemoteLokiLoggerMiddleware(otlpConfig.Logger)) + } } diff --git a/cmd/watchtower/httpserver/mw/logger.go b/cmd/watchtower/httpserver/mw/logger.go deleted file mode 100644 index 497c054..0000000 --- a/cmd/watchtower/httpserver/mw/logger.go +++ /dev/null @@ -1,146 +0,0 @@ -package mw - -import ( - "context" - "encoding/json" - "log/slog" - "os" - "strings" - "time" - - "github.com/Marlliton/slogpretty" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" - - "watchtower/internal/shared/telemetry" -) - -const ( - XRequestIDHeaderKey = "X-Request-ID" - ContextRequestIDKey = "request_id" -) - -func LocalLoggerMiddleware(config telemetry.LoggerConfig) fiber.Handler { - var logLevel = slog.LevelInfo - switch config.Level { - case "debug": - logLevel = slog.LevelDebug - case "info": - logLevel = slog.LevelInfo - case "warn": - logLevel = slog.LevelWarn - case "error": - logLevel = slog.LevelError - } - - textHandler := slogpretty.New(os.Stdout, &slogpretty.Options{ - Level: logLevel, - }) - - localLogger := slog.New(textHandler) - - return func(eCtx *fiber.Ctx) error { - if CheckFilteredURI(telemetry.FilterURI, eCtx.Path()) { - return eCtx.Next() - } - - requestID := eCtx.Get(XRequestIDHeaderKey) - if requestID == "" { - requestID = uuid.New().String() - eCtx.Set(XRequestIDHeaderKey, requestID) - } - - startTime := time.Now() - - //nolint - ctx := context.WithValue(eCtx.UserContext(), ContextRequestIDKey, requestID) - eCtx.SetUserContext(ctx) - - err := eCtx.Next() - - latency := time.Since(startTime) - - statusCode := eCtx.Response().StatusCode() - if err != nil { - //nolint - if fiberErr, ok := err.(*fiber.Error); ok { - statusCode = fiberErr.Code - } - } - - var responseMsg = "Ok" - var level = slog.LevelInfo - if statusCode >= 300 { - level = slog.LevelError - responseMsg = string(eCtx.Response().Body()) - } - - localLogger.LogAttrs(ctx, level, "http-request", - slog.String("request_id", requestID), - slog.String("method", eCtx.Method()), - slog.String("uri", eCtx.OriginalURL()), - slog.Int("status", statusCode), - slog.String("message", responseMsg), - slog.Int("bytes_received", len(eCtx.Request().Body())), - slog.Int("bytes_sent", len(eCtx.Response().Body())), - slog.Duration("latency", latency), - slog.String("referer", eCtx.Get("Referer")), - slog.String("client_ip", eCtx.IP()), - slog.String("user_agent", eCtx.Get("User-Agent")), - ) - - return err - } -} - -func CreateLokiLoggerMW(sll *telemetry.SlogLokiLogger) fiber.Handler { - return func(eCtx *fiber.Ctx) error { - if CheckFilteredURI(sll.FilterURI, eCtx.Path()) { - return eCtx.Next() - } - - start := time.Now() - - err := eCtx.Next() - if err != nil { - return err - } - - latency := time.Since(start) - - var responseMsg = "Ok" - var logLevel = slog.LevelInfo - statusCode := eCtx.Response().StatusCode() - if statusCode >= 300 { - logLevel = slog.LevelError - responseMsg = string(eCtx.Response().Body()) - } - - logMessage := map[string]interface{}{ - "message": responseMsg, - "latency": latency.String(), - "status": statusCode, - "method": eCtx.Method(), - "uri": eCtx.Path(), - "client_ip": eCtx.IP(), - "user_agent": eCtx.Request(), - } - jsonMessage, _ := json.Marshal(logMessage) - - ctx, cancel := context.WithTimeout(eCtx.Context(), 5*time.Second) - sll.Client.Log(ctx, logLevel, string(jsonMessage)) - defer cancel() - - return err - } -} - -func CheckFilteredURI(filteredURI []string, currURI string) bool { - for _, filtered := range filteredURI { - if strings.HasPrefix(currURI, filtered) { - return true - } - } - - return false -} diff --git a/cmd/watchtower/httpserver/mw/tracer.go b/cmd/watchtower/httpserver/mw/tracer.go deleted file mode 100644 index 996f22e..0000000 --- a/cmd/watchtower/httpserver/mw/tracer.go +++ /dev/null @@ -1,27 +0,0 @@ -package mw - -import ( - "strings" - - "github.com/gofiber/fiber/v2" -) - -var ( - excludedPaths = []string{ - "/health", - "/metrics", - "/favicon.ico", - "/static/", - "/api/swagger", - } -) - -func TraceURLSkipper(eCtx *fiber.Ctx) bool { - for _, excluded := range excludedPaths { - if strings.HasPrefix(eCtx.Path(), excluded) { - return true - } - } - - return eCtx.Request().Header.IsOptions() -} diff --git a/cmd/watchtower/httpserver/routes_task.go b/cmd/watchtower/httpserver/routes_task.go index 776e80c..93cb0ad 100644 --- a/cmd/watchtower/httpserver/routes_task.go +++ b/cmd/watchtower/httpserver/routes_task.go @@ -2,7 +2,6 @@ package httpserver import ( "github.com/gofiber/fiber/v2" - "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" @@ -14,8 +13,9 @@ import ( ) func (s *Server) CreateTasksGroup(group fiber.Router) { - group.Get("/tasks/:bucket", s.LoadTasks) - group.Get("/tasks/:bucket/:task_id", s.LoadTaskByID) + tasksGroup := group.Group("/tasks") + tasksGroup.Get("/:bucket", s.LoadTasks) + tasksGroup.Get("/:bucket/:task_id", s.LoadTaskByID) } // LoadTasks diff --git a/cmd/watchtower/watchtower.go b/cmd/watchtower/watchtower.go index d3078f8..0d5af78 100644 --- a/cmd/watchtower/watchtower.go +++ b/cmd/watchtower/watchtower.go @@ -7,12 +7,14 @@ import ( "os" "os/signal" "syscall" + "time" + + "github.com/breadrock1/otlp-go/otlp" "watchtower/cmd" "watchtower/cmd/watchtower/httpserver" "watchtower/internal/core/cloud/infrastructure/s3" "watchtower/internal/process" - "watchtower/internal/shared/telemetry" "watchtower/internal/support/task/infrastructure/docparser" "watchtower/internal/support/task/infrastructure/docsearch" "watchtower/internal/support/task/infrastructure/redis" @@ -22,22 +24,25 @@ import ( taskApp "watchtower/internal/support/task/application" ) +const ( + ShutdownDuration = 10 * time.Second +) + func main() { servConfig := cmd.Execute() - traceProvider, err := telemetry.InitTracer(servConfig.Otlp.Tracer) - if err != nil { - slog.Warn("failed to init tracer", slog.String("err", err.Error())) - } + logger := otlp_go.InitLocalLogger(servConfig.Otlp.Logger) + slog.SetDefault(logger) ctx := context.Background() + cCtx, cancel := context.WithCancel(ctx) taskStorage := redis.New(servConfig.Task.TaskStorage.Redis) taskQueue, err := rmq.New(servConfig.Task.TaskQueue.Rmq) if err != nil { log.Fatalf("task queue connection failed: %v", err) } - err = taskQueue.StartConsuming(ctx) + err = taskQueue.StartConsuming(cCtx) if err != nil { slog.Error("failed to launch task queue consumer", slog.String("err", err.Error())) os.Exit(1) @@ -51,16 +56,15 @@ func main() { os.Exit(1) } - cCtx, cancel := context.WithCancel(ctx) storageUseCase := cloudApp.NewStorageUseCase(objStorage) taskUseCase := taskApp.NewTaskUseCase(taskStorage, taskQueue, docParser, docStorage) orchestrator := process.NewOrchestrator(servConfig.Orchestrator, storageUseCase, taskUseCase) orchestrator.LaunchListener(cCtx) - httpServer := httpserver.SetupServer(servConfig.Otlp, orchestrator, traceProvider) + httpServer := httpserver.SetupServer(servConfig.Otlp, orchestrator) go func() { - if err := httpServer.Start(cCtx, servConfig.Server.Http); err != nil { + if err := httpServer.Start(servConfig.Server.Http); err != nil { slog.Error("http server start failed", slog.String("err", err.Error())) os.Exit(1) } @@ -69,5 +73,20 @@ func main() { ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) <-ch + + slog.Warn("received shutdown signal. shutdown server...") + cancel() + + shutdownCtx, shutdownRelease := context.WithTimeout(ctx, ShutdownDuration) + defer shutdownRelease() + + if err = httpServer.Shutdown(shutdownCtx); err != nil { + slog.Error("http server shutdown failed", slog.String("err", err.Error())) + return + } + + time.Sleep(time.Second) + + slog.Info("application has been shutdown successfully") } diff --git a/go.mod b/go.mod index c7f7a6f..b02d775 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module watchtower -go 1.25.3 +go 1.25.7 require ( github.com/google/uuid v1.6.0 @@ -18,9 +18,9 @@ require ( github.com/swaggo/swag v1.16.6 go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 go.opentelemetry.io/otel v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 - go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 golang.org/x/sync v0.20.0 @@ -33,6 +33,7 @@ require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/ansrivas/fiberprometheus/v2 v2.17.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/breadrock1/otlp-go v0.0.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -61,7 +62,7 @@ require ( github.com/gofiber/swagger v1.1.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.4 // indirect @@ -106,7 +107,7 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect @@ -117,9 +118,9 @@ require ( golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.43.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect - google.golang.org/grpc v1.75.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 824cf45..eda05ca 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/ansrivas/fiberprometheus/v2 v2.17.0 h1:p0gqs5LsSCWGoSFF44fCJkyU+XcE6T github.com/ansrivas/fiberprometheus/v2 v2.17.0/go.mod h1:giWBvbFSHOHG8N2wjYhQG23oc/2pF9v1mN8CdZs5Z2g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/breadrock1/otlp-go v0.0.2 h1:vys3XlLGHk6FO/PY0TFdAAqSSgETikGd5yLU+soYch8= +github.com/breadrock1/otlp-go v0.0.2/go.mod h1:sFVCxvuE0dA+rTEPIxi0yzdBMs5zjdzGu8CjfcB4XQg= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -121,6 +123,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -132,6 +136,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -161,6 +167,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= @@ -194,6 +202,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= @@ -204,6 +214,7 @@ github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzuk github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -297,8 +308,12 @@ go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= @@ -307,15 +322,20 @@ go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5 go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -332,6 +352,8 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= @@ -342,6 +364,8 @@ golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -358,6 +382,8 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -384,6 +410,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -398,6 +426,8 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -411,6 +441,8 @@ golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -420,10 +452,16 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/internal/core/cloud/application/usecase.go b/internal/core/cloud/application/usecase.go index 87d8207..2ec8d39 100644 --- a/internal/core/cloud/application/usecase.go +++ b/internal/core/cloud/application/usecase.go @@ -3,12 +3,12 @@ package application import ( "fmt" + "github.com/breadrock1/otlp-go/otlp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "watchtower/internal/core/cloud/domain" "watchtower/internal/shared/kernel" - "watchtower/internal/shared/telemetry" ) type StorageUseCase struct { @@ -20,7 +20,7 @@ func NewStorageUseCase(cloudStorage domain.ICloudStorage) *StorageUseCase { } func (s *StorageUseCase) GetAllBuckets(ctx kernel.Ctx) ([]domain.Bucket, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "get-buckets") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "get-buckets") defer span.End() allBuckets, err := s.cloudStorage.GetAllBuckets(ctx) @@ -35,7 +35,7 @@ func (s *StorageUseCase) GetAllBuckets(ctx kernel.Ctx) ([]domain.Bucket, error) } func (s *StorageUseCase) CreateBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "create-bucket") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "create-bucket") defer span.End() span.SetAttributes(attribute.String("bucket", bucketID)) @@ -52,7 +52,7 @@ func (s *StorageUseCase) CreateBucket(ctx kernel.Ctx, bucketID kernel.BucketID) } func (s *StorageUseCase) DeleteBucket(ctx kernel.Ctx, bucketID kernel.BucketID) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "remove-bucket") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "remove-bucket") defer span.End() span.SetAttributes(attribute.String("bucket", bucketID)) @@ -68,7 +68,7 @@ func (s *StorageUseCase) DeleteBucket(ctx kernel.Ctx, bucketID kernel.BucketID) } func (s *StorageUseCase) IsBucketExists(ctx kernel.Ctx, bucketID kernel.BucketID) (bool, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "is-bucket-exists") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "is-bucket-exists") defer span.End() span.SetAttributes(attribute.String("bucket", bucketID)) @@ -89,7 +89,7 @@ func (s *StorageUseCase) GetObjectInfo( bucketID kernel.BucketID, objID kernel.ObjectID, ) (*domain.Object, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "get-file-metadata") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "get-file-metadata") defer span.End() span.SetAttributes( @@ -109,7 +109,7 @@ func (s *StorageUseCase) GetObjectInfo( } func (s *StorageUseCase) CopyObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "copy-object") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "copy-object") defer span.End() span.SetAttributes( @@ -129,7 +129,7 @@ func (s *StorageUseCase) CopyObject(ctx kernel.Ctx, bucketID kernel.BucketID, pa } func (s *StorageUseCase) DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "copy-object") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "copy-object") defer span.End() span.SetAttributes( @@ -148,7 +148,7 @@ func (s *StorageUseCase) DeleteObject(ctx kernel.Ctx, bucketID kernel.BucketID, } func (s *StorageUseCase) DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, prefix string) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "copy-object") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "copy-object") defer span.End() span.SetAttributes( @@ -167,7 +167,7 @@ func (s *StorageUseCase) DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, } func (s *StorageUseCase) MoveObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "move-file") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "move-file") defer span.End() span.SetAttributes( @@ -200,7 +200,7 @@ func (s *StorageUseCase) LoadBucketObjects( bucketID kernel.BucketID, params *domain.GetObjectsParams, ) ([]domain.Object, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "get-bucket-objects") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "get-bucket-objects") defer span.End() span.SetAttributes( @@ -224,7 +224,7 @@ func (s *StorageUseCase) StoreObject( bucketID kernel.BucketID, params *domain.UploadObjectParams, ) (kernel.ObjectID, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "upload-object") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "upload-object") defer span.End() span.SetAttributes( @@ -249,7 +249,7 @@ func (s *StorageUseCase) GenShareURL( bucketID kernel.BucketID, params *domain.ShareObjectParams, ) (string, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "share-object") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "share-object") defer span.End() span.SetAttributes( @@ -273,7 +273,7 @@ func (s *StorageUseCase) GetObjectData( bucketID kernel.BucketID, objID kernel.ObjectID, ) (domain.ObjectData, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "download-object") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "download-object") defer span.End() span.SetAttributes( diff --git a/internal/core/cloud/infrastructure/s3/s3.go b/internal/core/cloud/infrastructure/s3/s3.go index 3b63570..2f5f992 100644 --- a/internal/core/cloud/infrastructure/s3/s3.go +++ b/internal/core/cloud/infrastructure/s3/s3.go @@ -30,6 +30,8 @@ func New(config Config) (domain.ICloudStorage, error) { return nil, fmt.Errorf("s3 connection error: %w", err) } + slog.Info("s3 connection established", slog.String("address", config.Address)) + client := &S3Client{ mc: s3Client, } diff --git a/internal/process/orchestrator.go b/internal/process/orchestrator.go index 3299d2f..f3b0e85 100644 --- a/internal/process/orchestrator.go +++ b/internal/process/orchestrator.go @@ -4,14 +4,13 @@ import ( "fmt" "log/slog" - "golang.org/x/sync/semaphore" - + "github.com/breadrock1/otlp-go/otlp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" + "golang.org/x/sync/semaphore" "watchtower/internal/core/cloud/domain" "watchtower/internal/shared/kernel" - "watchtower/internal/shared/telemetry" cloudApp "watchtower/internal/core/cloud/application" taskUC "watchtower/internal/support/task/application" @@ -37,6 +36,7 @@ func (o *Orchestrator) GetTaskProcessor() *taskUC.TaskUseCase { } func (o *Orchestrator) LaunchListener(ctx kernel.Ctx) { + slog.Info("starting orchestrator processing") go func() { consumeCh := o.taskUC.GetConsumerChannel() sem := semaphore.NewWeighted(o.config.SemaphoreSize) @@ -59,7 +59,7 @@ func (o *Orchestrator) LaunchListener(ctx kernel.Ctx) { }() case <-ctx.Done(): - slog.Info("terminating processing") + slog.Info("terminating orchestrator processing") return } } @@ -71,7 +71,7 @@ func (o *Orchestrator) UploadFile( bucketID kernel.BucketID, params *domain.UploadObjectParams, ) (*taskDomain.Task, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "upload-file") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "upload-file") defer span.End() span.SetAttributes( @@ -113,7 +113,7 @@ func (o *Orchestrator) CreateTask( slog.String("file-path", objID), ) - ctx, span := telemetry.GlobalTracer.Start(ctx, "create-and-publish-task") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "create-and-publish-task") defer span.End() span.SetAttributes( @@ -145,7 +145,7 @@ func (o *Orchestrator) CreateTask( func (o *Orchestrator) handleTask(ctx kernel.Ctx, task *taskDomain.Task) { slog.Info("processing task event", slog.String("task-id", task.ID.String())) - ctx, span := telemetry.GlobalTracer.Start(ctx, "handle-task-from-queue") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "handle-task-from-queue") defer span.End() span.SetAttributes( @@ -172,7 +172,7 @@ func (o *Orchestrator) handleTask(ctx kernel.Ctx, task *taskDomain.Task) { } func (o *Orchestrator) processTask(ctx kernel.Ctx, task *taskDomain.Task) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "task-processing") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "task-processing") defer span.End() span.SetAttributes( diff --git a/internal/shared/kernel/name.go b/internal/shared/kernel/name.go new file mode 100644 index 0000000..e38281b --- /dev/null +++ b/internal/shared/kernel/name.go @@ -0,0 +1,5 @@ +package kernel + +const ( + AppName = "watchtower" +) diff --git a/internal/shared/metrics/metrics.go b/internal/shared/metrics/metrics.go new file mode 100644 index 0000000..12dac1b --- /dev/null +++ b/internal/shared/metrics/metrics.go @@ -0,0 +1,29 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + StoredArticleCounter prometheus.Counter + StoredMatchedArticleTagsCounter prometheus.Counter + CreatedMonitoringTasksCounter prometheus.Counter +) + +func init() { + StoredArticleCounter = promauto.NewCounter(prometheus.CounterOpts{ + Name: "monitoring_stored_articles_total", + Help: "Total number of stored articles", + }) + + StoredMatchedArticleTagsCounter = promauto.NewCounter(prometheus.CounterOpts{ + Name: "monitoring_stored_matched_articles_total", + Help: "Total number of stored matched article tags", + }) + + CreatedMonitoringTasksCounter = promauto.NewCounter(prometheus.CounterOpts{ + Name: "monitoring_created_tasks_total", + Help: "Total number of created tasks", + }) +} diff --git a/internal/shared/telemetry/config.go b/internal/shared/telemetry/config.go deleted file mode 100644 index dd59c26..0000000 --- a/internal/shared/telemetry/config.go +++ /dev/null @@ -1,17 +0,0 @@ -package telemetry - -type OtlpConfig struct { - Logger LoggerConfig `yaml:"logger"` - Tracer TracerConfig `yaml:"tracer"` -} - -type LoggerConfig struct { - Level string `mapstructure:"level"` - Address string `mapstructure:"address"` - EnableLoki bool `mapstructure:"enable_loki"` -} - -type TracerConfig struct { - Address string `mapstructure:"address"` - EnableJaeger bool `mapstructure:"enable_jaeger"` -} diff --git a/internal/shared/telemetry/logger.go b/internal/shared/telemetry/logger.go deleted file mode 100644 index 2f04ffa..0000000 --- a/internal/shared/telemetry/logger.go +++ /dev/null @@ -1,38 +0,0 @@ -package telemetry - -import ( - "fmt" - "log/slog" - "time" - - slogloki "github.com/samber/slog-loki/v2" -) - -var ( - FilterURI = []string{ - "/metrics", - "/api/swagger", - } -) - -type SlogLokiLogger struct { - Client *slog.Logger - FilterURI []string -} - -func InitLokiLogger(config LoggerConfig) SlogLokiLogger { - lokiConfig := slogloki.Option{ - Endpoint: fmt.Sprintf("%s/api/prom/push", config.Address), - Level: slog.LevelInfo, - BatchWait: time.Second * 5, - BatchEntriesNumber: 10, - } - - logger := slog.New(lokiConfig.NewLokiHandler()). - With("service_name", AppName). - With("service", AppName). - With("detected_level", config.Level). - With("level", config.Level) - - return SlogLokiLogger{Client: logger, FilterURI: FilterURI} -} diff --git a/internal/shared/telemetry/tracer.go b/internal/shared/telemetry/tracer.go deleted file mode 100644 index 7395053..0000000 --- a/internal/shared/telemetry/tracer.go +++ /dev/null @@ -1,66 +0,0 @@ -package telemetry - -import ( - "context" - "fmt" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/resource" - "go.opentelemetry.io/otel/trace" - - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" -) - -const AppName = "watchtower" - -var ( - GlobalTracer trace.Tracer - TracePropagator = propagation.NewCompositeTextMapPropagator( - propagation.TraceContext{}, - propagation.Baggage{}, - ) -) - -func InitTracer(config TracerConfig) (trace.Tracer, error) { - sampler := sdktrace.AlwaysSample() - res, err := resource.Merge( - resource.Default(), - resource.NewWithAttributes( - semconv.SchemaURL, - semconv.TelemetrySDKLanguageGo, - semconv.ServiceNameKey.String(AppName), - ), - ) - if err != nil { - return nil, fmt.Errorf("failed to merge trace resources: %w", err) - } - - var traceOpts []sdktrace.TracerProviderOption - traceOpts = append(traceOpts, sdktrace.WithResource(res)) - traceOpts = append(traceOpts, sdktrace.WithSampler(sampler)) - - if config.EnableJaeger { - ctx := context.Background() - client := otlptracegrpc.NewClient( - otlptracegrpc.WithEndpoint(config.Address), - otlptracegrpc.WithInsecure(), - ) - - traceExporter, err := otlptrace.New(ctx, client) - if err != nil { - return nil, fmt.Errorf("failed to connect to otlp trace server: %w", err) - } - bsp := sdktrace.NewBatchSpanProcessor(traceExporter) - traceOpts = append(traceOpts, sdktrace.WithSpanProcessor(bsp)) - } - - tp := sdktrace.NewTracerProvider(traceOpts...) - otel.SetTextMapPropagator(TracePropagator) - GlobalTracer = tp.Tracer(AppName) - otel.SetTracerProvider(tp) - return GlobalTracer, nil -} diff --git a/internal/shared/utils/sender.go b/internal/shared/utils/sender.go index ec57d99..9ca029a 100644 --- a/internal/shared/utils/sender.go +++ b/internal/shared/utils/sender.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/breadrock1/otlp-go/otlp" "github.com/labstack/echo/v4" "go.opentelemetry.io/otel" @@ -15,7 +16,6 @@ import ( "go.opentelemetry.io/otel/propagation" "watchtower/internal/shared/kernel" - "watchtower/internal/shared/telemetry" ) func PUT(ctx kernel.Ctx, body *bytes.Buffer, url, mime string, timeout time.Duration) ([]byte, error) { @@ -41,7 +41,7 @@ func POST(ctx kernel.Ctx, body *bytes.Buffer, url, mime string, timeout time.Dur } func sendRequest(ctx kernel.Ctx, client *http.Client, req *http.Request) ([]byte, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "http-request") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "http-request") defer span.End() span.SetAttributes( @@ -80,7 +80,7 @@ func sendRequest(ctx kernel.Ctx, client *http.Client, req *http.Request) ([]byte } func extractSpanContext(ctx kernel.Ctx, resp *http.Response) kernel.Ctx { - propagator := telemetry.TracePropagator + propagator := otlp_go.TracePropagator carrier := propagation.HeaderCarrier(resp.Header) return propagator.Extract(ctx, carrier) } diff --git a/internal/support/task/application/usecase.go b/internal/support/task/application/usecase.go index c8f0e55..066b450 100644 --- a/internal/support/task/application/usecase.go +++ b/internal/support/task/application/usecase.go @@ -6,11 +6,11 @@ import ( "log/slog" "path" + "github.com/breadrock1/otlp-go/otlp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "watchtower/internal/shared/kernel" - "watchtower/internal/shared/telemetry" "watchtower/internal/support/task/application/mapping" "watchtower/internal/support/task/application/service/docstorage" "watchtower/internal/support/task/application/service/recognizer" @@ -39,7 +39,7 @@ func NewTaskUseCase( } func (p *TaskUseCase) GetBucketTasks(ctx kernel.Ctx, bucketID kernel.BucketID) ([]*domain.Task, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "get-all-bucket-tasks") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "get-all-bucket-tasks") defer span.End() span.SetAttributes(attribute.String("bucket", bucketID)) @@ -56,7 +56,7 @@ func (p *TaskUseCase) GetBucketTasks(ctx kernel.Ctx, bucketID kernel.BucketID) ( } func (p *TaskUseCase) GetTask(ctx kernel.Ctx, bucketID kernel.BucketID, taskID kernel.TaskID) (*domain.Task, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "get-task-by-id") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "get-task-by-id") defer span.End() span.SetAttributes( @@ -76,7 +76,7 @@ func (p *TaskUseCase) GetTask(ctx kernel.Ctx, bucketID kernel.BucketID, taskID k } func (p *TaskUseCase) UpdateTaskStatus(ctx kernel.Ctx, task *domain.Task) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "update-task-status") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "update-task-status") defer span.End() span.SetAttributes( @@ -95,7 +95,7 @@ func (p *TaskUseCase) UpdateTaskStatus(ctx kernel.Ctx, task *domain.Task) { } func (p *TaskUseCase) IsTaskAlreadyExists(ctx kernel.Ctx, task *domain.Task) bool { - ctx, span := telemetry.GlobalTracer.Start(ctx, "check-task-already-created") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "check-task-already-created") defer span.End() span.SetAttributes( @@ -147,7 +147,7 @@ func (p *TaskUseCase) Recognize( task *domain.Task, fileData *bytes.Buffer, ) (*recognizer.Recognized, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "recognize-object-data") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "recognize-object-data") defer span.End() span.SetAttributes( @@ -179,7 +179,7 @@ func (p *TaskUseCase) StoreDocument( task *domain.Task, recData *recognizer.Recognized, ) (docstorage.DocumentID, error) { - ctx, span := telemetry.GlobalTracer.Start(ctx, "store-document-to-index") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "store-document-to-index") defer span.End() span.SetAttributes( diff --git a/internal/support/task/infrastructure/redis/redis.go b/internal/support/task/infrastructure/redis/redis.go index 8b95014..53fc60d 100644 --- a/internal/support/task/infrastructure/redis/redis.go +++ b/internal/support/task/infrastructure/redis/redis.go @@ -6,11 +6,10 @@ import ( "log/slog" "time" + "github.com/redis/go-redis/v9" + "watchtower/internal/shared/kernel" - "watchtower/internal/shared/telemetry" "watchtower/internal/support/task/domain" - - "github.com/redis/go-redis/v9" ) type RedisClient struct { @@ -21,6 +20,9 @@ type RedisClient struct { func New(config Config) domain.ITaskStorage { redisOpts := &redis.Options{Addr: config.Address} conn := redis.NewClient(redisOpts) + + slog.Info("redis connection established", slog.String("address", config.Address)) + return &RedisClient{ config: config, rsConn: conn, @@ -105,5 +107,5 @@ func (rs *RedisClient) UpdateTask(ctx kernel.Ctx, task *domain.Task) error { } func (rs *RedisClient) generateUniqID(bucketID kernel.BucketID, taskID string) string { - return fmt.Sprintf("%s:%s:%s", telemetry.AppName, bucketID, taskID) + return fmt.Sprintf("%s:%s:%s", kernel.AppName, bucketID, taskID) } diff --git a/internal/support/task/infrastructure/rmq/rmq.go b/internal/support/task/infrastructure/rmq/rmq.go index f1e898d..d778c07 100644 --- a/internal/support/task/infrastructure/rmq/rmq.go +++ b/internal/support/task/infrastructure/rmq/rmq.go @@ -6,14 +6,14 @@ import ( "fmt" "log/slog" "time" - "watchtower/internal/shared/kernel" + "github.com/breadrock1/otlp-go/otlp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" - "watchtower/internal/shared/telemetry" + "watchtower/internal/shared/kernel" "watchtower/internal/support/task/domain" amqp "github.com/rabbitmq/amqp091-go" @@ -23,7 +23,6 @@ const ConsumerName = "watchtower-consumer" type RabbitMQClient struct { redirect chan domain.Message - done chan error config Config conn *amqp.Connection @@ -48,9 +47,10 @@ func New(config Config) (domain.ITaskQueue, error) { return nil, fmt.Errorf("failed to create rmq channel: %w", err) } + slog.Info("rmq connection established", slog.String("address", config.Address)) + rmqClient = RabbitMQClient{ make(chan domain.Message), - make(chan error), config, conn, rmqCh, @@ -64,7 +64,7 @@ func (r *RabbitMQClient) GetConsumerChannel() chan domain.Message { } func (r *RabbitMQClient) Publish(ctx kernel.Ctx, msg domain.Message) error { - ctx, span := telemetry.GlobalTracer.Start(ctx, "rmq-publish") + ctx, span := otlp_go.GlobalTracer.Start(ctx, "rmq-publish") defer span.End() headers := injectSpanContextToHeaders(ctx) @@ -105,8 +105,8 @@ func (r *RabbitMQClient) Publish(ctx kernel.Ctx, msg domain.Message) error { return nil } -func (r *RabbitMQClient) StartConsuming(_ kernel.Ctx) error { - go r.handleReconnect() +func (r *RabbitMQClient) StartConsuming(ctx kernel.Ctx) error { + go r.handleReconnect(ctx) deliveries, err := r.channel.Consume( r.config.QueueName, // name @@ -122,7 +122,7 @@ func (r *RabbitMQClient) StartConsuming(_ kernel.Ctx) error { return fmt.Errorf("rmq: consume error: %w", err) } - go r.handleMessage(deliveries, r.done) + go r.handleMessage(ctx, deliveries) return nil } @@ -136,46 +136,57 @@ func (r *RabbitMQClient) StopConsuming(_ kernel.Ctx) error { return fmt.Errorf("rmq: close connection failed: %w", err) } - // wait for handleMessage() to exit - return <-r.done + return nil } -func (r *RabbitMQClient) handleMessage(deliveries <-chan amqp.Delivery, done chan error) { - cleanup := func() { - slog.Warn("rmq: deliveries channel closed") - done <- nil - } +func (r *RabbitMQClient) handleMessage(ctx kernel.Ctx, deliveries <-chan amqp.Delivery) { + slog.Info("launching rmq consumer") + + for { + select { + case <-ctx.Done(): + if err := r.StopConsuming(ctx); err != nil { + slog.Error("failed to stop rmq consuming", slog.String("err", err.Error())) + return + } - defer cleanup() + slog.Info("rmq deliveries channel has been closed") - for delMsg := range deliveries { - ctx := extractSpanContextFromHeaders(delMsg.Headers) - span := trace.SpanFromContext(ctx) + return - consumeMsg := &Message{} - err := json.Unmarshal(delMsg.Body, consumeMsg) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - slog.Error("rmq: failed while deserialize msg", slog.String("err", err.Error())) - continue - } + case delMsg, ok := <-deliveries: + if !ok { + continue + } - span.SetName("rmq-consume") - span.SetAttributes(attribute.String("task-id", consumeMsg.EventId.String())) + spanCtx := extractSpanContextFromHeaders(delMsg.Headers) + span := trace.SpanFromContext(spanCtx) - consumeMsg.Ctx = ctx - msg := consumeMsg.ToMessage() + consumeMsg := &Message{} + err := json.Unmarshal(delMsg.Body, consumeMsg) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + slog.Error("rmq: failed while deserialize msg", slog.String("err", err.Error())) + continue + } + + span.SetName("rmq-consume") + span.SetAttributes(attribute.String("task-id", consumeMsg.EventId.String())) - r.redirect <- *msg - span.End() + consumeMsg.Ctx = spanCtx + msg := consumeMsg.ToMessage() + + r.redirect <- *msg + span.End() + } } } -func (r *RabbitMQClient) handleReconnect() { +func (r *RabbitMQClient) handleReconnect(ctx kernel.Ctx) { for { select { - case <-r.done: + case <-ctx.Done(): return case <-r.conn.NotifyClose(make(chan *amqp.Error)): @@ -218,7 +229,7 @@ func (r *RabbitMQClient) handleReconnect() { func injectSpanContextToHeaders(ctx kernel.Ctx) amqp.Table { carrier := propagation.HeaderCarrier{} - telemetry.TracePropagator.Inject(ctx, carrier) + otlp_go.TracePropagator.Inject(ctx, carrier) span := trace.SpanFromContext(ctx) sCtx := span.SpanContext() diff --git a/tests/common/env_app_server.go b/tests/common/env_app_server.go index 10009c5..3673844 100644 --- a/tests/common/env_app_server.go +++ b/tests/common/env_app_server.go @@ -1,11 +1,9 @@ package common import ( - "fmt" "watchtower/cmd" "watchtower/cmd/watchtower/httpserver" "watchtower/internal/process" - "watchtower/internal/shared/telemetry" "watchtower/tests/common/mocks" cloudApp "watchtower/internal/core/cloud/application" @@ -36,15 +34,9 @@ func InitTestAppEnvironment() *TestAppServerEnvironment { } func (e *TestAppServerEnvironment) BuildAppServer(servConfig *cmd.Config) (*httpserver.Server, error) { - tracerProvider, err := telemetry.InitTracer(servConfig.Otlp.Tracer) - telemetry.GlobalTracer = tracerProvider - if err != nil { - return nil, fmt.Errorf("failed to initialize tracer: %w", err) - } - storageUseCase := cloudApp.NewStorageUseCase(e.ObjectStorage) taskUseCase := taskApp.NewTaskUseCase(e.TaskStorage, e.TaskQueue, e.Recognizer, e.DocStorage) orchestrator := process.NewOrchestrator(servConfig.Orchestrator, storageUseCase, taskUseCase) - appServer := httpserver.SetupServer(servConfig.Otlp, orchestrator, tracerProvider) + appServer := httpserver.SetupServer(servConfig.Otlp, orchestrator) return appServer, nil } diff --git a/tests/common/usecase.go b/tests/common/usecase.go index 19e32ab..3b8e1ff 100644 --- a/tests/common/usecase.go +++ b/tests/common/usecase.go @@ -7,10 +7,11 @@ import ( "os" "time" + "github.com/breadrock1/otlp-go/otlp" "watchtower/cmd" + "watchtower/internal/core/cloud/infrastructure/s3" "watchtower/internal/process" - "watchtower/internal/shared/telemetry" "watchtower/internal/support/task/infrastructure/redis" "watchtower/internal/support/task/infrastructure/rmq" "watchtower/tests/common/mocks" @@ -42,8 +43,8 @@ func InitTestEnvironment(configFilePath string) (*TestEnvironment, error) { return nil, fmt.Errorf("failed to read config file %s: %w", configFilePath, err) } - tracerProvider, _ := telemetry.InitTracer(servConfig.Otlp.Tracer) - telemetry.GlobalTracer = tracerProvider + tracerProvider, _ := otlp_go.InitTracer(servConfig.Otlp.Tracer) + otlp_go.GlobalTracer = tracerProvider docParser := new(mocks.MockRecognizer) docStorage := new(mocks.MockDocStorage) From c8ce98bce03e12576fca99c0688285b629a81e3d Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 17:29:27 +0300 Subject: [PATCH 33/47] chore: added custom prometheus metrics --- internal/process/orchestrator.go | 26 ++++++ internal/shared/metrics/metrics.go | 80 +++++++++++++++---- internal/support/task/application/usecase.go | 19 +++++ .../support/task/infrastructure/rmq/rmq.go | 11 +++ 4 files changed, 119 insertions(+), 17 deletions(-) diff --git a/internal/process/orchestrator.go b/internal/process/orchestrator.go index f3b0e85..4640381 100644 --- a/internal/process/orchestrator.go +++ b/internal/process/orchestrator.go @@ -3,6 +3,8 @@ package process import ( "fmt" "log/slog" + "strconv" + "time" "github.com/breadrock1/otlp-go/otlp" "go.opentelemetry.io/otel/attribute" @@ -11,6 +13,7 @@ import ( "watchtower/internal/core/cloud/domain" "watchtower/internal/shared/kernel" + "watchtower/internal/shared/metrics" cloudApp "watchtower/internal/core/cloud/application" taskUC "watchtower/internal/support/task/application" @@ -52,9 +55,22 @@ func (o *Orchestrator) LaunchListener(ctx kernel.Ctx) { defer sem.Release(1) task := &cMsg.Body + + instant := time.Now() o.handleTask(ctx, task) + + elapsedTime := time.Since(instant) + statusInt := strconv.Itoa(int(task.Status)) + metrics.OrchestratorProcessingDurationSeconds. + WithLabelValues(kernel.AppName, statusInt). + Observe(elapsedTime.Seconds()) + o.taskUC.UpdateTaskStatus(ctx, task) + metrics.OrchestratorProcessingCounter. + WithLabelValues(kernel.AppName, statusInt). + Inc() + ctx.Done() }() @@ -81,6 +97,11 @@ func (o *Orchestrator) UploadFile( ) objID, err := o.storageUC.StoreObject(ctx, bucketID, params) + + metrics.UploadedFilesCounter. + WithLabelValues(kernel.AppName, strconv.FormatBool(err != nil)). + Inc() + if err != nil { err = fmt.Errorf("failed to upload file %s: %w", params.FilePath, err) span.SetStatus(codes.Error, err.Error()) @@ -89,6 +110,11 @@ func (o *Orchestrator) UploadFile( } task, err := o.CreateTask(ctx, bucketID, objID) + + metrics.CreatedProcessingTasksCounter. + WithLabelValues(kernel.AppName, strconv.FormatBool(err != nil)). + Inc() + if err != nil { err = fmt.Errorf("failed to create taskUC %s: %w", params.FilePath, err) span.SetStatus(codes.Error, err.Error()) diff --git a/internal/shared/metrics/metrics.go b/internal/shared/metrics/metrics.go index 12dac1b..70ba149 100644 --- a/internal/shared/metrics/metrics.go +++ b/internal/shared/metrics/metrics.go @@ -6,24 +6,70 @@ import ( ) var ( - StoredArticleCounter prometheus.Counter - StoredMatchedArticleTagsCounter prometheus.Counter - CreatedMonitoringTasksCounter prometheus.Counter + RmqReconnectCounter *prometheus.CounterVec + UploadedFilesCounter *prometheus.CounterVec + CreatedProcessingTasksCounter *prometheus.CounterVec + OrchestratorProcessingCounter *prometheus.CounterVec + + OrchestratorProcessingDurationSeconds *prometheus.HistogramVec + RecognizerDurationSeconds *prometheus.HistogramVec + StoreProcessedDocumentDurationSeconds *prometheus.HistogramVec ) func init() { - StoredArticleCounter = promauto.NewCounter(prometheus.CounterOpts{ - Name: "monitoring_stored_articles_total", - Help: "Total number of stored articles", - }) - - StoredMatchedArticleTagsCounter = promauto.NewCounter(prometheus.CounterOpts{ - Name: "monitoring_stored_matched_articles_total", - Help: "Total number of stored matched article tags", - }) - - CreatedMonitoringTasksCounter = promauto.NewCounter(prometheus.CounterOpts{ - Name: "monitoring_created_tasks_total", - Help: "Total number of created tasks", - }) + RmqReconnectCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "watchtower_rmq_reconnect_total", + Help: "Total number of rmq reconnects", + }, + []string{"service", "is_failed"}, + ) + + UploadedFilesCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "watchtower_upload_files_total", + Help: "Total number of uploaded files to storage", + }, + []string{"service", "is_failed"}, + ) + + CreatedProcessingTasksCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "watchtower_created_tasks_total", + Help: "Total number of created tasks of processing", + }, + []string{"service", "is_failed"}, + ) + + OrchestratorProcessingCounter = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "watchtower_orchestrator_processed_total", + Help: "Total processed documents into orchestrator", + }, + []string{"service", "status"}, + ) + + OrchestratorProcessingDurationSeconds = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "watchtower_orchestrator_processing_duration_seconds", + Help: "Latency of full document processing time in seconds", + }, + []string{"service", "status"}, + ) + + RecognizerDurationSeconds = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "watchtower_recognizer_duration_seconds", + Help: "Latency of recognizing text from document file", + }, + []string{"service", "is_failed"}, + ) + + StoreProcessedDocumentDurationSeconds = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "watchtower_store_document_duration_seconds", + Help: "Latency of storing processed document", + }, + []string{"service", "is_failed"}, + ) } diff --git a/internal/support/task/application/usecase.go b/internal/support/task/application/usecase.go index 066b450..cb76d6d 100644 --- a/internal/support/task/application/usecase.go +++ b/internal/support/task/application/usecase.go @@ -5,12 +5,15 @@ import ( "fmt" "log/slog" "path" + "strconv" + "time" "github.com/breadrock1/otlp-go/otlp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "watchtower/internal/shared/kernel" + "watchtower/internal/shared/metrics" "watchtower/internal/support/task/application/mapping" "watchtower/internal/support/task/application/service/docstorage" "watchtower/internal/support/task/application/service/recognizer" @@ -161,8 +164,16 @@ func (p *TaskUseCase) Recognize( FileData: fileData, } + instant := time.Now() + // TODO: impled retry pattern recData, err := p.recognizer.Recognize(ctx, inputFile) + + elapsedTime := time.Since(instant) + metrics.RecognizerDurationSeconds. + WithLabelValues(kernel.AppName, strconv.FormatBool(err != nil)). + Observe(elapsedTime.Seconds()) + if err != nil { task.SetStatusAndText(domain.Failed, "failed to recognize file") err = fmt.Errorf("failed to recognize file %s: %w", task.ID, err) @@ -198,7 +209,15 @@ func (p *TaskUseCase) StoreDocument( ModifiedAt: task.ModifiedAt, } + instant := time.Now() + docID, err := p.docStorage.StoreDocument(ctx, doc) + + elapsedTime := time.Since(instant) + metrics.StoreProcessedDocumentDurationSeconds. + WithLabelValues(kernel.AppName, strconv.FormatBool(err != nil)). + Observe(elapsedTime.Seconds()) + if err != nil { err = fmt.Errorf("failed to store document: %w", err) span.SetStatus(codes.Error, err.Error()) diff --git a/internal/support/task/infrastructure/rmq/rmq.go b/internal/support/task/infrastructure/rmq/rmq.go index d778c07..c69a35d 100644 --- a/internal/support/task/infrastructure/rmq/rmq.go +++ b/internal/support/task/infrastructure/rmq/rmq.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log/slog" + "strconv" "time" "github.com/breadrock1/otlp-go/otlp" @@ -14,6 +15,7 @@ import ( "go.opentelemetry.io/otel/trace" "watchtower/internal/shared/kernel" + "watchtower/internal/shared/metrics" "watchtower/internal/support/task/domain" amqp "github.com/rabbitmq/amqp091-go" @@ -215,10 +217,19 @@ func (r *RabbitMQClient) handleReconnect(ctx kernel.Ctx) { } slog.Error("rmq: failed to create channel", slog.String("err", err.Error())) + reconnectDelay = reconnectCounter * reconnectCounter time.Sleep(time.Duration(reconnectDelay) * time.Second) + + metrics.RmqReconnectCounter. + WithLabelValues(kernel.AppName, strconv.FormatBool(err != nil)). + Inc() } + metrics.RmqReconnectCounter. + WithLabelValues(kernel.AppName, strconv.FormatBool(err != nil)). + Inc() + if err != nil { slog.Error("rmq: failed to restore connection", slog.String("err", err.Error())) return From 3b4e6f2f533394ece85842a0517f76a22b20b863 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 17:30:05 +0300 Subject: [PATCH 34/47] chore: combined copying and moving objects to common route --- cmd/watchtower/httpserver/form/form.go | 7 +- cmd/watchtower/httpserver/routes_object.go | 182 +++++++++------------ internal/core/cloud/application/usecase.go | 30 +--- internal/core/cloud/domain/params.go | 11 +- 4 files changed, 93 insertions(+), 137 deletions(-) diff --git a/cmd/watchtower/httpserver/form/form.go b/cmd/watchtower/httpserver/form/form.go index 11419d5..a39fc45 100644 --- a/cmd/watchtower/httpserver/form/form.go +++ b/cmd/watchtower/httpserver/form/form.go @@ -37,6 +37,8 @@ type ShareFileForm struct { // GetFilesForm example type GetFilesForm struct { DirectoryName string `json:"directory" example:"test-folder/"` + Limit int32 `json:"limit" example:"10"` + Offset int32 `json:"offset" example:"0"` } // GetFileAttributesForm example @@ -46,8 +48,9 @@ type GetFileAttributesForm struct { // CopyFileForm example type CopyFileForm struct { - SrcPath string `json:"src_path" example:"old-test-document.docx"` - DstPath string `json:"dst_path" example:"test-document.docx"` + SrcPath string `json:"src_path" example:"old-test-document.docx"` + DstPath string `json:"dst_path" example:"test-document.docx"` + WithRemove bool `json:"with_remove" example:"true"` } // FolderForm example diff --git a/cmd/watchtower/httpserver/routes_object.go b/cmd/watchtower/httpserver/routes_object.go index 09ec3b4..6c4cecf 100644 --- a/cmd/watchtower/httpserver/routes_object.go +++ b/cmd/watchtower/httpserver/routes_object.go @@ -22,8 +22,7 @@ const FolderFileKeeper = ".keeper" func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { group.Post("/cloud/:bucket/files", s.GetFiles) - group.Post("/cloud/:bucket/file/copy", s.CopyFile) - group.Post("/cloud/:bucket/file/move", s.MoveFile) + group.Patch("/cloud/:bucket/file", s.CopyFile) group.Put("/cloud/:bucket/file/upload", s.UploadFile) group.Post("/cloud/:bucket/file/download", s.DownloadFile) group.Post("/cloud/:bucket/folder", s.CreateFolder) @@ -34,108 +33,6 @@ func (s *Server) CreateStorageObjectsGroup(group fiber.Router) { group.Post("/cloud/:bucket/file/share", s.ShareFile) } -// CopyFile -// @Summary Copy file to another location into bucket -// @Description Copy file to another location into bucket -// @ID copy-file -// @Tags files -// @Accept json -// @Produce json -// @Param bucket path string true "Bucket name of src file" -// @Param jsonQuery body form.CopyFileForm true "Params to copy file" -// @Success 200 {object} form.Success "Ok" -// @Failure 400 {object} form.BadRequestError "Bad Request error" -// @Failure 404 {object} form.NotFoundError "Bucket or file not found" -// @Failure 500 {object} form.InternalServerError "Internal server error" -// @Failure 503 {object} form.ServerUnavailableError "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/copy [post] -//nolint -func (s *Server) CopyFile(eCtx *fiber.Ctx) error { - ctx := eCtx.UserContext() - - span := trace.SpanFromContext(ctx) - - bucket, err := ExtractBucketParameter(eCtx) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) - } - - var jsonForm form.CopyFileForm - err = json.Unmarshal(eCtx.Body(), &jsonForm) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) - } - - params := &domain.CopyObjectParams{ - SourcePath: jsonForm.SrcPath, - DestinationPath: jsonForm.DstPath, - } - - err = s.state.GetObjectStorage().CopyObject(ctx, bucket, params) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) - } - - return eCtx.Status(fiber.StatusOK).SendString("Ok") -} - -// MoveFile -// @Summary Move file to another location into bucket -// @Description Move file to another location into bucket -// @ID move-file -// @Tags files -// @Accept json -// @Produce json -// @Param bucket path string true "Bucket name of src file" -// @Param jsonQuery body form.CopyFileForm true "Params to move file" -// @Success 200 {object} form.Success "Ok" -// @Failure 400 {object} form.BadRequestError "Bad Request error" -// @Failure 404 {object} form.NotFoundError "Bucket or file not found" -// @Failure 500 {object} form.InternalServerError "Internal server error" -// @Failure 503 {object} form.ServerUnavailableError "Server does not available" -// @Router /api/v1/cloud/{bucket}/file/move [post] -//nolint -func (s *Server) MoveFile(eCtx *fiber.Ctx) error { - ctx := eCtx.UserContext() - - span := trace.SpanFromContext(ctx) - - bucket, err := ExtractBucketParameter(eCtx) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) - } - - var jsonForm form.CopyFileForm - err = json.Unmarshal(eCtx.Body(), &jsonForm) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) - } - - params := &domain.CopyObjectParams{ - SourcePath: jsonForm.SrcPath, - DestinationPath: jsonForm.DstPath, - } - - err = s.state.GetObjectStorage().MoveObject(ctx, bucket, params) - if err != nil { - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) - } - - return eCtx.Status(fiber.StatusOK).SendString("Ok") -} - // CreateFolder // @Summary Create empty folder into cloud storage // @Description Create empty folder into cloud storage @@ -534,6 +431,81 @@ func (s *Server) RemoveFile2(eCtx *fiber.Ctx) error { return eCtx.Status(fiber.StatusOK).SendString("Ok") } +// CopyFile +// @Summary Copy file to another location into bucket +// @Description Copy file to another location into bucket +// @ID copy-file +// @Tags files +// @Accept json +// @Produce json +// @Param bucket path string true "Bucket name of src file" +// @Param jsonQuery body form.CopyFileForm true "Params to copy file" +// @Success 200 {object} form.Success "Ok" +// @Failure 400 {object} form.BadRequestError "Bad Request error" +// @Failure 404 {object} form.NotFoundError "Bucket or file not found" +// @Failure 500 {object} form.InternalServerError "Internal server error" +// @Failure 503 {object} form.ServerUnavailableError "Server does not available" +// @Router /api/v1/cloud/{bucket}/file [patch] +func (s *Server) CopyFile(eCtx *fiber.Ctx) error { + ctx := eCtx.UserContext() + + span := trace.SpanFromContext(ctx) + + bucket, err := ExtractBucketParameter(eCtx) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + objectStorage := s.state.GetObjectStorage() + exist, err := objectStorage.IsBucketExists(ctx, bucket) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(http.StatusBadRequest).SendString(err.Error()) + } + + if !exist { + err = fmt.Errorf("specified bucket %s does not exist", bucket) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(http.StatusNotFound).SendString(err.Error()) + } + + var jsonForm form.CopyFileForm + err = json.Unmarshal(eCtx.Body(), &jsonForm) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + params := &domain.CopyObjectParams{ + SourcePath: jsonForm.SrcPath, + DestinationPath: jsonForm.DstPath, + } + + err = objectStorage.CopyObject(ctx, bucket, params) + if err != nil { + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + + if jsonForm.WithRemove { + err = objectStorage.DeleteObject(ctx, bucket, params.SourcePath) + if err != nil { + err = fmt.Errorf("failed to delete object: %w", err) + span.SetStatus(codes.Error, err.Error()) + span.RecordError(err) + return eCtx.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + } + + return eCtx.Status(fiber.StatusOK).SendString("Ok") +} + // GetFiles // @Summary Get files list into bucket // @Description Get files list into bucket @@ -573,6 +545,8 @@ func (s *Server) GetFiles(eCtx *fiber.Ctx) error { params := &domain.GetObjectsParams{ PrefixPath: jsonForm.DirectoryName, + Limit: jsonForm.Limit, + Offset: jsonForm.Offset, } objectStorage := s.state.GetObjectStorage() diff --git a/internal/core/cloud/application/usecase.go b/internal/core/cloud/application/usecase.go index 2ec8d39..77f30e5 100644 --- a/internal/core/cloud/application/usecase.go +++ b/internal/core/cloud/application/usecase.go @@ -116,6 +116,7 @@ func (s *StorageUseCase) CopyObject(ctx kernel.Ctx, bucketID kernel.BucketID, pa attribute.String("bucket", bucketID), attribute.String("src-file-path", params.SourcePath), attribute.String("dst-file-path", params.DestinationPath), + attribute.Bool("with-removed", params.WithRemoving), ) err := s.cloudStorage.CopyObject(ctx, bucketID, params) @@ -166,35 +167,6 @@ func (s *StorageUseCase) DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, return nil } -func (s *StorageUseCase) MoveObject(ctx kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { - ctx, span := otlp_go.GlobalTracer.Start(ctx, "move-file") - defer span.End() - - span.SetAttributes( - attribute.String("bucket", bucketID), - attribute.String("src-file-path", params.SourcePath), - attribute.String("dst-file-path", params.DestinationPath), - ) - - err := s.cloudStorage.CopyObject(ctx, bucketID, params) - if err != nil { - err = fmt.Errorf("failed to move object %s to %s: %w", params.SourcePath, params.DestinationPath, err) - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return err - } - - err = s.cloudStorage.DeleteObject(ctx, bucketID, params.DestinationPath) - if err != nil { - err = fmt.Errorf("failed to delete object: %w", err) - span.SetStatus(codes.Error, err.Error()) - span.RecordError(err) - return err - } - - return nil -} - func (s *StorageUseCase) LoadBucketObjects( ctx kernel.Ctx, bucketID kernel.BucketID, diff --git a/internal/core/cloud/domain/params.go b/internal/core/cloud/domain/params.go index 84937fa..0040818 100644 --- a/internal/core/cloud/domain/params.go +++ b/internal/core/cloud/domain/params.go @@ -15,6 +15,9 @@ type CopyObjectParams struct { // DestinationPath is the full path where the object should be copied // Example: "backups/file.pdf" or "documents/copy/file.pdf" DestinationPath string + + // WithRemoving is bool flag to remove source path after copying. + WithRemoving bool } // ShareObjectParams defines parameters for generating a shareable URL for an object. @@ -36,9 +39,13 @@ type GetObjectsParams struct { // Example: "documents/2024/" to list all objects in the 2024 folder PrefixPath string - // MaxKeys limits the number of objects returned (pagination) + // Limit limits the number of objects returned (pagination) // Zero means use provider default - MaxKeys int32 + Limit int32 + + // Offset base (pagination) + // Zero means use offset from begin + Offset int32 // ContinuationToken for pagination through large result sets ContinuationToken string From 7c6a73dfdb2bbdcd034471a152fd03512a6845cd Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 17:30:15 +0300 Subject: [PATCH 35/47] chore: updated object storage mock --- tests/common/mocks/object_storage.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/common/mocks/object_storage.go b/tests/common/mocks/object_storage.go index 679f265..d6289b6 100644 --- a/tests/common/mocks/object_storage.go +++ b/tests/common/mocks/object_storage.go @@ -60,17 +60,29 @@ func (m *MockObjectStorage) StoreObject( return args.Get(0).(kernel.ObjectID), args.Error(1) } -func (m *MockObjectStorage) CopyObject(_ kernel.Ctx, bucketID kernel.BucketID, params *domain.CopyObjectParams) error { +func (m *MockObjectStorage) CopyObject( + _ kernel.Ctx, + bucketID kernel.BucketID, + params *domain.CopyObjectParams, +) error { args := m.Called(bucketID, params) return args.Error(0) } -func (m *MockObjectStorage) DeleteObject(_ kernel.Ctx, bucketID kernel.BucketID, objID kernel.ObjectID) error { +func (m *MockObjectStorage) DeleteObject( + _ kernel.Ctx, + bucketID kernel.BucketID, + objID kernel.ObjectID, +) error { args := m.Called(bucketID, objID) return args.Error(0) } -func (m *MockObjectStorage) DeleteObjects(ctx kernel.Ctx, bucketID kernel.BucketID, prefix string) error { +func (m *MockObjectStorage) DeleteObjects( + _ kernel.Ctx, + bucketID kernel.BucketID, + prefix string, +) error { args := m.Called(bucketID, prefix) return args.Error(0) } From 01fc6e69cb085a8bd3a450930dfabe3cc354de33 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 18:22:36 +0300 Subject: [PATCH 36/47] chore: added first part of tests --- tests/common/mocks/object_storage.go | 2 +- tests/routes/object_routes_test.go | 335 +++++++++++++++++++++++++++ 2 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 tests/routes/object_routes_test.go diff --git a/tests/common/mocks/object_storage.go b/tests/common/mocks/object_storage.go index d6289b6..58369d9 100644 --- a/tests/common/mocks/object_storage.go +++ b/tests/common/mocks/object_storage.go @@ -84,7 +84,7 @@ func (m *MockObjectStorage) DeleteObjects( prefix string, ) error { args := m.Called(bucketID, prefix) - return args.Error(0) + return args.Error(1) } func (m *MockObjectStorage) GetBucketObjects( diff --git a/tests/routes/object_routes_test.go b/tests/routes/object_routes_test.go new file mode 100644 index 0000000..ffba772 --- /dev/null +++ b/tests/routes/object_routes_test.go @@ -0,0 +1,335 @@ +package routes_test + +import ( + "bytes" + "crypto/md5" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + "watchtower/cmd/watchtower/httpserver/form" + "watchtower/internal/core/cloud/domain" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "watchtower/cmd" + "watchtower/tests/common" +) + +const ( + TestObjectName = "test-object-name.docx" + TestObjectPath = "./test-object.docx" + TestObjectNewPath = "./test/test-object.docx" + TestObjectContentType = "application/docx" + TestObjectSize = 1024 + TestFolderPath = "test-folder" + + IsBucketExistsMethodName = "IsBucketExist" + CopyObjectMethodName = "CopyObject" + StoreObjectMethodName = "StoreObject" + DeleteObjectMethodName = "DeleteObject" + DeleteObjectsMethodName = "DeleteObjects" +) + +var ( + TestObjectID = "test-object-id" + TestObjectChecksum = md5.New() + TestObjectCreatedAt = time.Now() + + TestObject = domain.Object{ + Name: TestObjectName, + Path: TestObjectPath, + Checksum: fmt.Sprintf("%x", TestObjectChecksum), + ContentType: TestObjectContentType, + Expired: TestObjectCreatedAt, + LastModified: TestObjectCreatedAt, + Size: TestObjectSize, + IsDirectory: false, + } + + TestCopyFileForm = form.CopyFileForm{ + SrcPath: TestObjectPath, + DstPath: TestObjectNewPath, + } + + TestCreateFolderForm = form.FolderForm{ + Prefix: TestFolderPath, + } + + MatchedStoreObjectParams = mock.MatchedBy(func(params *domain.UploadObjectParams) bool { + filePathFlag := params.FilePath == "test-object.docx/.keeper" + return filePathFlag + }) + + MatchedCopyFilesParams = mock.MatchedBy(func(params *domain.CopyObjectParams) bool { + srcPathFlag := params.SourcePath == TestObjectPath + dstPathFlag := params.DestinationPath == TestObjectNewPath + return srcPathFlag && dstPathFlag + }) +) + +// nolint +func TestObjectAPIRoutes(t *testing.T) { + servConfig, err := cmd.InitConfig() + assert.NoError(t, err, "failed to read config file") + + var copyFileTestCases = []struct { + TargetURL string + HttpMethod string + MockMethodName string + RequestPayload *form.CopyFileForm + ReturnedData interface{} + ReturnedError error + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file/copy", TestBucketName), + HttpMethod: http.MethodPost, + MockMethodName: CopyObjectMethodName, + RequestPayload: &TestCopyFileForm, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusOK, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file/copy", TestBucketName), + HttpMethod: http.MethodPost, + MockMethodName: CopyObjectMethodName, + RequestPayload: nil, + ReturnedData: nil, + ReturnedError: errors.New("invalid request"), + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusBadRequest, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file/copy", TestBucketName), + HttpMethod: http.MethodPost, + MockMethodName: CopyObjectMethodName, + RequestPayload: &TestCopyFileForm, + ReturnedData: nil, + ReturnedError: errors.New("internal error"), + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + t.Run("Copy file", func(t *testing.T) { + for index, testCase := range copyFileTestCases { + testCaseName := fmt.Sprintf("Copy file case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.ObjectStorage. + On(testCase.MockMethodName, TestBucketName, MatchedCopyFilesParams). + Return(testCase.ReturnedError) + + var buffer = bytes.NewBuffer(nil) + if testCase.RequestPayload != nil { + jsonBytes, err := json.Marshal(testCase.RequestPayload) + assert.NoError(t, err, "failed to marshal request body") + buffer = bytes.NewBuffer(jsonBytes) + } + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "failed to copy file") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.ObjectStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) + + var createFolderTestCases = []struct { + TargetURL string + HttpMethod string + RequestPayload *form.FolderForm + IsBucketExists bool + MockMethodName string + ReturnedData interface{} + ReturnedError error + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodPost, + RequestPayload: &TestCreateFolderForm, + IsBucketExists: true, + MockMethodName: StoreObjectMethodName, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusCreated, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodPost, + RequestPayload: &TestCreateFolderForm, + IsBucketExists: false, + MockMethodName: StoreObjectMethodName, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusNotFound, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodPost, + RequestPayload: nil, + IsBucketExists: true, + MockMethodName: StoreObjectMethodName, + ReturnedData: nil, + ReturnedError: errors.New("invalid folder name"), + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusBadRequest, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodPost, + RequestPayload: &TestCreateFolderForm, + IsBucketExists: true, + MockMethodName: StoreObjectMethodName, + ReturnedData: nil, + ReturnedError: errors.New("internal server error"), + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + //nolint + t.Run("Create folder", func(t *testing.T) { + for index, testCase := range createFolderTestCases { + testCaseName := fmt.Sprintf("Create folder case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.ObjectStorage. + On(IsBucketExistsMethodName, TestBucketName). + Return(testCase.IsBucketExists, nil) + + testEnv.ObjectStorage. + On(testCase.MockMethodName, TestBucketName, MatchedStoreObjectParams). + Return(TestObjectID, testCase.ReturnedError) + + var buffer = bytes.NewBuffer(nil) + if testCase.RequestPayload != nil { + jsonBytes, err := json.Marshal(testCase.RequestPayload) + assert.NoError(t, err, "failed to marshal request body") + buffer = bytes.NewBuffer(jsonBytes) + } + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "failed to create folder") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.ObjectStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) + + var deleteFolderTestCases = []struct { + TargetURL string + HttpMethod string + RequestPayload *form.FolderForm + IsBucketExists bool + MockMethodName string + ReturnedData interface{} + ReturnedError error + ExpectedCalledTimes int + ExpectedStatusCode int + }{ + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodDelete, + RequestPayload: &TestCreateFolderForm, + IsBucketExists: true, + MockMethodName: DeleteObjectsMethodName, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 1, + ExpectedStatusCode: http.StatusOK, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodDelete, + RequestPayload: &TestCreateFolderForm, + IsBucketExists: false, + MockMethodName: DeleteObjectsMethodName, + ReturnedData: nil, + ReturnedError: errors.New("bucket not found"), + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusNotFound, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodDelete, + RequestPayload: &TestCreateFolderForm, + IsBucketExists: true, + MockMethodName: DeleteObjectMethodName, + ReturnedData: nil, + ReturnedError: errors.New("folder not empty"), + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusInternalServerError, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/folder", TestBucketName), + HttpMethod: http.MethodDelete, + RequestPayload: &TestCreateFolderForm, + IsBucketExists: true, + MockMethodName: DeleteObjectMethodName, + ReturnedData: nil, + ReturnedError: errors.New("service unavailable"), + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusInternalServerError, + }, + } + + //nolint + t.Run("Delete folder", func(t *testing.T) { + for index, testCase := range deleteFolderTestCases { + testCaseName := fmt.Sprintf("Delete folder case %d", index) + t.Run(testCaseName, func(t *testing.T) { + testEnv := common.InitTestAppEnvironment() + appServer, err := testEnv.BuildAppServer(servConfig) + assert.NoError(t, err, "failed to build app server") + + testEnv.ObjectStorage. + On(IsBucketExistsMethodName, TestBucketName). + Return(testCase.IsBucketExists, nil) + + testEnv.ObjectStorage. + On(testCase.MockMethodName, TestBucketName, TestFolderPath). + Return(TestObjectID, testCase.ReturnedError) + + var buffer = bytes.NewBuffer(nil) + if testCase.RequestPayload != nil { + jsonBytes, err := json.Marshal(testCase.RequestPayload) + assert.NoError(t, err, "failed to marshal request body") + buffer = bytes.NewBuffer(jsonBytes) + } + + req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + + resp, respErr := appServer.Server.Test(req, -1) + assert.NoError(t, respErr, "failed to delete folder") + assert.Equal(t, testCase.ExpectedStatusCode, resp.StatusCode, "unexpected http status code") + + testEnv.ObjectStorage.AssertNumberOfCalls(t, testCase.MockMethodName, testCase.ExpectedCalledTimes) + }) + } + }) +} From 236302fb0948805ee3c2b44a3c49c1b96028a0a3 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 18:25:23 +0300 Subject: [PATCH 37/47] chore: updated mod file --- go.mod | 28 ++----- go.sum | 236 +++------------------------------------------------------ 2 files changed, 18 insertions(+), 246 deletions(-) diff --git a/go.mod b/go.mod index b02d775..3f068ba 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,21 @@ module watchtower go 1.25.7 require ( + github.com/breadrock1/otlp-go v0.0.2 + github.com/gofiber/fiber/v2 v2.52.12 + github.com/gofiber/swagger v1.1.1 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo-contrib v0.17.4 github.com/labstack/echo/v4 v4.13.4 github.com/minio/minio-go/v7 v7.0.94 + github.com/prometheus/client_golang v1.23.2 github.com/rabbitmq/amqp091-go v1.10.0 github.com/redis/go-redis/v9 v9.11.0 - github.com/samber/slog-loki/v2 v2.2.0 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.11.1 - github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.6 - go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 go.opentelemetry.io/otel v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 - go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 golang.org/x/sync v0.20.0 @@ -33,7 +30,6 @@ require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/ansrivas/fiberprometheus/v2 v2.17.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/breadrock1/otlp-go v0.0.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -41,7 +37,6 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -56,10 +51,7 @@ require ( github.com/go-openapi/swag/typeutils v0.25.5 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/gofiber/contrib/otelfiber v1.0.10 // indirect github.com/gofiber/contrib/otelfiber/v2 v2.2.3 // indirect - github.com/gofiber/fiber/v2 v2.52.12 // indirect - github.com/gofiber/swagger v1.1.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect @@ -79,34 +71,32 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/samber/lo v1.38.1 // indirect github.com/samber/slog-common v0.11.0 // indirect + github.com/samber/slog-loki/v2 v2.2.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/fiber-swagger v1.3.0 // indirect - github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect github.com/tinylib/msgp v1.3.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect @@ -116,13 +106,11 @@ require ( golang.org/x/net v0.52.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect - golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.43.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index eda05ca..baf5252 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,9 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Marlliton/slogpretty v0.1.3 h1:kLYjcKtFqikoCrXVMaI2R6fBy9pcJwoBJKdkhwGgoB4= github.com/Marlliton/slogpretty v0.1.3/go.mod h1:vEC85AhV7Obb264VOAUMIBvwE3ivRSad6djal/v2sYU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c h1:AMDVOKGaiqse4qiRXSzRgpC9DCNTHCx6zpzdtXXrKM4= github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c/go.mod h1:p/7Wos+jcfrnwLqqzJMZ0s323kfVtJPW+HUvAANklVQ= -github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/ansrivas/fiberprometheus/v2 v2.17.0 h1:p0gqs5LsSCWGoSFF44fCJkyU+XcE6TLRqEMu80b2iCo= @@ -27,9 +22,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -42,8 +35,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -51,63 +42,37 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= -github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= -github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= -github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= -github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= -github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= -github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= -github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= -github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= -github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= -github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= -github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= -github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= -github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= -github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= -github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= -github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofiber/contrib/otelfiber v1.0.10 h1:Bu28Pi4pfYmGfIc/9+sNaBbFwTHGY/zpSIK5jBxuRtM= -github.com/gofiber/contrib/otelfiber v1.0.10/go.mod h1:jN6AvS1HolDHTQHFURsV+7jSX96FpXYeKH6nmkq8AIw= github.com/gofiber/contrib/otelfiber/v2 v2.2.3 h1:WKW1XezHFAoohGZwnvC0R8TFJcNkabQwB5YIpdKmz00= github.com/gofiber/contrib/otelfiber/v2 v2.2.3/go.mod h1:WdQ1tYbL83IYC6oBaWvKBMVGSAYvSTRuUWTcr0wK1T4= -github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY= github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA= @@ -120,9 +85,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -131,44 +93,27 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= -github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= @@ -181,12 +126,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= @@ -194,34 +133,22 @@ github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -233,9 +160,6 @@ github.com/samber/slog-common v0.11.0 h1:JdESCaXcEwdtoTCYHKQFfHGbWN2vZJq0DDGEE/l github.com/samber/slog-common v0.11.0/go.mod h1:Qjrfhwk79XiCIhBj8+jTq1Cr0u9rlWbjawh3dWXzaHk= github.com/samber/slog-loki/v2 v2.2.0 h1:Urh35FxmWxmHjDiqIVkxT98BtzdCu974rGOsgKRg9h8= github.com/samber/slog-loki/v2 v2.2.0/go.mod h1:ooEVylnfKGIB+3zJaaG3y/YcbuGPJAaE3RY9VWXXBEI= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -254,9 +178,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -265,75 +186,40 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= -github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= -github.com/swaggo/fiber-swagger v1.3.0 h1:RMjIVDleQodNVdKuu7GRs25Eq8RVXK7MwY9f5jbobNg= -github.com/swaggo/fiber-swagger v1.3.0/go.mod h1:18MuDqBkYEiUmeM/cAAB8CI28Bi62d/mys39j1QqF9w= -github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= -github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= -github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= -github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= -github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib v1.42.0 h1:845qj52z2T/bLInfZmG8AdbTO7delSd6eGVVHcAikzw= go.opentelemetry.io/contrib v1.42.0/go.mod h1:JYdNU7Pl/2ckKMGp8/G7zeyhEbtRmy9Q8bcrtv75Znk= -go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0 h1:6YeICKmGrvgJ5th4+OMNpcuoB6q/Xs8gt0YCO7MUv1k= -go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.63.0/go.mod h1:ZEA7j2B35siNV0T00aapacNzjz4tvOlNoHp0ncCfwNQ= -go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo= -go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -344,140 +230,38 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= -google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From d6a0c811c6d248664fb1ce2f4309d4e6762211f0 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 18:34:33 +0300 Subject: [PATCH 38/47] fix: tests --- tests/routes/object_routes_test.go | 33 +++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/tests/routes/object_routes_test.go b/tests/routes/object_routes_test.go index ffba772..36582ba 100644 --- a/tests/routes/object_routes_test.go +++ b/tests/routes/object_routes_test.go @@ -61,7 +61,7 @@ var ( } MatchedStoreObjectParams = mock.MatchedBy(func(params *domain.UploadObjectParams) bool { - filePathFlag := params.FilePath == "test-object.docx/.keeper" + filePathFlag := params.FilePath == fmt.Sprintf("%s/.keeper", TestFolderPath) return filePathFlag }) @@ -82,36 +82,51 @@ func TestObjectAPIRoutes(t *testing.T) { HttpMethod string MockMethodName string RequestPayload *form.CopyFileForm + IsBucketExists bool ReturnedData interface{} ReturnedError error ExpectedCalledTimes int ExpectedStatusCode int }{ { - TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file/copy", TestBucketName), - HttpMethod: http.MethodPost, + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file", TestBucketName), + HttpMethod: http.MethodPatch, MockMethodName: CopyObjectMethodName, RequestPayload: &TestCopyFileForm, + IsBucketExists: true, ReturnedData: nil, ReturnedError: nil, ExpectedCalledTimes: 1, ExpectedStatusCode: http.StatusOK, }, { - TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file/copy", TestBucketName), - HttpMethod: http.MethodPost, + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file", TestBucketName), + HttpMethod: http.MethodPatch, + RequestPayload: &TestCopyFileForm, + IsBucketExists: false, + MockMethodName: StoreObjectMethodName, + ReturnedData: nil, + ReturnedError: nil, + ExpectedCalledTimes: 0, + ExpectedStatusCode: http.StatusNotFound, + }, + { + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file", TestBucketName), + HttpMethod: http.MethodPatch, MockMethodName: CopyObjectMethodName, RequestPayload: nil, + IsBucketExists: true, ReturnedData: nil, ReturnedError: errors.New("invalid request"), ExpectedCalledTimes: 0, ExpectedStatusCode: http.StatusBadRequest, }, { - TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file/copy", TestBucketName), - HttpMethod: http.MethodPost, + TargetURL: fmt.Sprintf("/api/v1/cloud/%s/file", TestBucketName), + HttpMethod: http.MethodPatch, MockMethodName: CopyObjectMethodName, RequestPayload: &TestCopyFileForm, + IsBucketExists: true, ReturnedData: nil, ReturnedError: errors.New("internal error"), ExpectedCalledTimes: 1, @@ -127,6 +142,10 @@ func TestObjectAPIRoutes(t *testing.T) { appServer, err := testEnv.BuildAppServer(servConfig) assert.NoError(t, err, "failed to build app server") + testEnv.ObjectStorage. + On(IsBucketExistsMethodName, TestBucketName). + Return(testCase.IsBucketExists, nil) + testEnv.ObjectStorage. On(testCase.MockMethodName, TestBucketName, MatchedCopyFilesParams). Return(testCase.ReturnedError) From d2de4e84ef76f73f00575273bdd43d0ffb17e881 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 18:40:06 +0300 Subject: [PATCH 39/47] chore: added additional configs path searching --- cmd/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/config.go b/cmd/config.go index 6bacfc9..0a8d1b7 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -72,6 +72,7 @@ func InitConfig() (*Config, error) { viperInst.AddConfigPath(".") viperInst.AddConfigPath("./configs") + viperInst.AddConfigPath("../configs") if launchMode == defaultLaunchMode { // Used to include config from integration tests From 1f8a8767bf17179cf5140f7c3d4980cbb389fc51 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 18:41:56 +0300 Subject: [PATCH 40/47] chore: updated golang version to 1.26 --- Dockerfile | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9152793..71cc625 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder RUN apk update && apk add --no-cache gcc libc-dev make diff --git a/go.mod b/go.mod index 3f068ba..20738f2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module watchtower -go 1.25.7 +go 1.26 require ( github.com/breadrock1/otlp-go v0.0.2 From 16880d7a6fb5c6046fa2f127072759a20b7e62cc Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 18:47:51 +0300 Subject: [PATCH 41/47] chore: returned back golang version to 1.26 --- Dockerfile | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 71cc625..9152793 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26-alpine AS builder +FROM golang:1.25-alpine AS builder RUN apk update && apk add --no-cache gcc libc-dev make diff --git a/go.mod b/go.mod index 20738f2..3f068ba 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module watchtower -go 1.26 +go 1.25.7 require ( github.com/breadrock1/otlp-go v0.0.2 From 7338e2200ca9d7881b9b574449f5fa7c02f4c4f1 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 18:57:37 +0300 Subject: [PATCH 42/47] fix: redis unmarshal native --- internal/support/task/infrastructure/redis/redis.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/support/task/infrastructure/redis/redis.go b/internal/support/task/infrastructure/redis/redis.go index 53fc60d..2fd1888 100644 --- a/internal/support/task/infrastructure/redis/redis.go +++ b/internal/support/task/infrastructure/redis/redis.go @@ -76,8 +76,13 @@ func (rs *RedisClient) GetTask( return nil, fmt.Errorf("redis error: %w: %w", domain.ErrExecution, cmd.Err()) } - value := &RedisValue{} - if err := cmd.Scan(value); err != nil { + data, err := cmd.Bytes() + if err != nil { + return nil, fmt.Errorf("redis payload error: %w: %w", domain.ErrExecution, cmd.Err()) + } + + var value *RedisValue + if err = json.Unmarshal(data, &value); err != nil { return nil, fmt.Errorf("deserialize error: %w: %w", domain.ErrInvalidTaskData, err) } From 0ba20de82621f084f57326ffb5c16d9c4cbd2ee4 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 19:01:08 +0300 Subject: [PATCH 43/47] chore: updated golang lint version to latest --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7be977e..5f41eb0 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,7 +30,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: - version: v2.6.0 + version: latest test: runs-on: ubuntu-latest From dec1e16be982bbceb4254e9eea31ef8b87d276b9 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 19:02:43 +0300 Subject: [PATCH 44/47] chore: updated golang lint version to latest --- .github/workflows/pull-request.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5f41eb0..0adc7f6 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -26,7 +26,9 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-go@v6 with: - go-version: stable + go-version: 1.25.7 + check-latest: 'false' + - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: From edd65435b2a91d85fcfb453eb463604a17e899a1 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 19:08:34 +0300 Subject: [PATCH 45/47] fix: lint warnings --- tests/routes/bucket_routes_test.go | 13 ++++++++++--- tests/routes/object_routes_test.go | 13 ++++++++++--- tests/routes/task_routes_test.go | 9 +++++++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/tests/routes/bucket_routes_test.go b/tests/routes/bucket_routes_test.go index 2967e99..d174a77 100644 --- a/tests/routes/bucket_routes_test.go +++ b/tests/routes/bucket_routes_test.go @@ -2,6 +2,7 @@ package routes_test import ( "bytes" + "context" "encoding/json" "fmt" "net/http" @@ -73,6 +74,8 @@ func TestBucketAPIRoutes(t *testing.T) { } t.Run("Get buckets", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range getBucketsTestCases { testCaseName := fmt.Sprintf("Get buckets case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -84,7 +87,7 @@ func TestBucketAPIRoutes(t *testing.T) { On(testCase.MockMethodName). Return(testCase.ReturnedData, testCase.ReturnedError) - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, nil) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "failed to create tag") @@ -155,6 +158,8 @@ func TestBucketAPIRoutes(t *testing.T) { } t.Run("Delete bucket", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range createBucketTestCases { testCaseName := fmt.Sprintf("Create bucket case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -177,7 +182,7 @@ func TestBucketAPIRoutes(t *testing.T) { buffer = bytes.NewBuffer(jsonBytes) } - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, buffer) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "failed to delete tag") @@ -227,6 +232,8 @@ func TestBucketAPIRoutes(t *testing.T) { } t.Run("Delete buckets", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range deleteBucketsTestCases { testCaseName := fmt.Sprintf("Delete bucket case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -242,7 +249,7 @@ func TestBucketAPIRoutes(t *testing.T) { On(testCase.MockMethodName, TestBucket.ID). Return(testCase.ReturnedError, testCase.ReturnedError) - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, nil) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "delete bucket") diff --git a/tests/routes/object_routes_test.go b/tests/routes/object_routes_test.go index 36582ba..ceebe34 100644 --- a/tests/routes/object_routes_test.go +++ b/tests/routes/object_routes_test.go @@ -2,6 +2,7 @@ package routes_test import ( "bytes" + "context" "crypto/md5" "encoding/json" "errors" @@ -135,6 +136,8 @@ func TestObjectAPIRoutes(t *testing.T) { } t.Run("Copy file", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range copyFileTestCases { testCaseName := fmt.Sprintf("Copy file case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -157,7 +160,7 @@ func TestObjectAPIRoutes(t *testing.T) { buffer = bytes.NewBuffer(jsonBytes) } - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, buffer) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "failed to copy file") @@ -227,6 +230,8 @@ func TestObjectAPIRoutes(t *testing.T) { //nolint t.Run("Create folder", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range createFolderTestCases { testCaseName := fmt.Sprintf("Create folder case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -249,7 +254,7 @@ func TestObjectAPIRoutes(t *testing.T) { buffer = bytes.NewBuffer(jsonBytes) } - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, buffer) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "failed to create folder") @@ -319,6 +324,8 @@ func TestObjectAPIRoutes(t *testing.T) { //nolint t.Run("Delete folder", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range deleteFolderTestCases { testCaseName := fmt.Sprintf("Delete folder case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -341,7 +348,7 @@ func TestObjectAPIRoutes(t *testing.T) { buffer = bytes.NewBuffer(jsonBytes) } - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, buffer) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, buffer) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "failed to delete folder") diff --git a/tests/routes/task_routes_test.go b/tests/routes/task_routes_test.go index f195a29..956444a 100644 --- a/tests/routes/task_routes_test.go +++ b/tests/routes/task_routes_test.go @@ -1,6 +1,7 @@ package routes_test import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -105,6 +106,8 @@ func TestTaskAPIRoutes(t *testing.T) { } t.Run("Load tasks", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range loadTasksTestCases { testCaseName := fmt.Sprintf("Load tasks case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -116,7 +119,7 @@ func TestTaskAPIRoutes(t *testing.T) { On(testCase.MockMethodName, matchedBucketID). Return(testCase.ReturnedData, testCase.ReturnedError) - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, nil) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "failed to load tasks") @@ -166,6 +169,8 @@ func TestTaskAPIRoutes(t *testing.T) { } t.Run("Load task by id", func(t *testing.T) { + ctx := context.Background() + for index, testCase := range loadTaskByIDTestCases { testCaseName := fmt.Sprintf("Load task by id case %d", index) t.Run(testCaseName, func(t *testing.T) { @@ -177,7 +182,7 @@ func TestTaskAPIRoutes(t *testing.T) { On(testCase.MockMethodName, matchedBucketID, matchedTaskID). Return(testCase.ReturnedData, testCase.ReturnedError) - req := httptest.NewRequest(testCase.HttpMethod, testCase.TargetURL, nil) + req := httptest.NewRequestWithContext(ctx, testCase.HttpMethod, testCase.TargetURL, nil) resp, respErr := appServer.Server.Test(req, -1) assert.NoError(t, respErr, "failed to load task by id") From aca6844251c430afe35faabef445e18bb8819957 Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 19:12:47 +0300 Subject: [PATCH 46/47] fix: lint warnings --- tests/routes/object_routes_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/routes/object_routes_test.go b/tests/routes/object_routes_test.go index ceebe34..3ee28d3 100644 --- a/tests/routes/object_routes_test.go +++ b/tests/routes/object_routes_test.go @@ -73,7 +73,7 @@ var ( }) ) -// nolint +//nolint func TestObjectAPIRoutes(t *testing.T) { servConfig, err := cmd.InitConfig() assert.NoError(t, err, "failed to read config file") From 0f683a4fe1caf4424e3ea7d60064870b3724587d Mon Sep 17 00:00:00 2001 From: breadrock1 Date: Tue, 31 Mar 2026 19:13:00 +0300 Subject: [PATCH 47/47] chore: added drone ci config file --- .drone/drone.yml | 195 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 .drone/drone.yml diff --git a/.drone/drone.yml b/.drone/drone.yml new file mode 100644 index 0000000..29cccad --- /dev/null +++ b/.drone/drone.yml @@ -0,0 +1,195 @@ +kind: pipeline +type: docker +name: watchtower-pr + +trigger: + event: + - pull_request + actions: + - synchronized + +environment: + GO_VERSION: 1.25 + +services: + - name: redis + image: redis:alpine + + - name: rabbitmq + image: rabbitmq:4-management + + - name: minio + image: minio/minio + commands: + - minio server /data + environment: + MINIO_ROOT_USER: "minio-root" + MINIO_ROOT_PASSWORD: "minio-root" + +steps: + - name: cache-built-go + image: drillster/drone-volume-cache + settings: + restore: true + mount: + - /usr/local/go + - /drone/src + volumes: + - name: go-targets + path: /cache + + - name: build + image: golang:1.25 + depends_on: + - cache-built-go + commands: + - go build watchtower/cmd/watchtower + volumes: + - name: go-targets + path: /cache + + - name: lint + image: golang:1.25 + depends_on: + - build + commands: + - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + - golangci-lint run ./... + volumes: + - name: go-targets + path: /cache + + - name: test + image: golang:1.25 + depends_on: + - build + commands: + - sleep 15 + - curl -v -u guest:guest -K /drone/src/tests/config/rmq/headers.conf --data-binary '@/drone/src/tests/config/rmq/definitions.json' http://rabbitmq:15672/api/definitions + - sleep 10 + - go test watchtower/tests + volumes: + - name: go-targets + path: /cache + +volumes: + - name: go-targets + temp: {} + + +# On merged pull request to main branch or created release branch +--- +kind: pipeline +type: docker +name: watchtower-merged + +trigger: + event: + - push + branch: + - main + - master + - release/* + +environment: + GO_VERSION: 1.25 + +services: + - name: redis + image: redis:alpine + + - name: rabbitmq + image: rabbitmq:4-management + + - name: minio + image: minio/minio + commands: + - minio server /data + environment: + MINIO_ROOT_USER: "minio-root" + MINIO_ROOT_PASSWORD: "minio-root" + +steps: + - name: cache-built-go + image: drillster/drone-volume-cache + settings: + restore: true + mount: + - /usr/local/go + - /drone/src + volumes: + - name: go-targets + path: /cache + + - name: build + image: golang:1.25 + depends_on: + - cache-built-go + commands: + - go build watchtower/cmd/watchtower + volumes: + - name: go-targets + path: /cache + + - name: lint + image: golang:1.25 + depends_on: + - build + commands: + - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + - golangci-lint run ./... + volumes: + - name: go-targets + path: /cache + + - name: test + image: golang:1.25 + depends_on: + - build + commands: + - sleep 15 + - curl -v -u guest:guest -K /drone/src/tests/config/rmq/headers.conf --data-binary '@/drone/src/tests/config/rmq/definitions.json' http://rabbitmq:15672/api/definitions + - sleep 10 + - go test watchtower/tests + volumes: + - name: go-targets + path: /cache + +volumes: + - name: go-targets + temp: {} + + +# Build release on new tag creating event +--- +kind: pipeline +type: docker +name: watchtower-release + +trigger: + event: + - tag + +environment: + REGISTRY_ADDRESS: ${REGISTRY_ADDRESS} + +steps: + - name: build-and-push-docker + image: plugins/docker + settings: + repo: git.sova.local:3000/sova-core/watchtower + insecure: true + dockerfile: Dockerfile + registry: git.sova.local:5000 + tags: + - ${DRONE_COMMIT_SHA:0:8} + - ${DRONE_TAG} + - latest + username: + from_secret: GITEA_USERNAME + password: + from_secret: GITEA_TOKEN + +volumes: + - name: go-targets + temp: {}