From 7d64e8f16f2f0655110433aff1089752799bbd45 Mon Sep 17 00:00:00 2001 From: Jonathan VUILLEMIN Date: Fri, 12 Jan 2024 14:16:43 +0100 Subject: [PATCH] feat(fxcore): Provided module (#45) --- .github/workflows/coverage.yml | 1 + .github/workflows/fxcore-ci.yml | 31 + README.md | 1 + fxcore/.golangci.yml | 62 ++ fxcore/README.md | 327 ++++++++ fxcore/bootstrap.go | 79 ++ fxcore/bootstrap_test.go | 274 +++++++ fxcore/core.go | 38 + fxcore/core_test.go | 33 + fxcore/go.mod | 87 ++ fxcore/go.sum | 588 ++++++++++++++ fxcore/info.go | 82 ++ fxcore/info_test.go | 44 + fxcore/module.go | 478 +++++++++++ fxcore/module_test.go | 985 +++++++++++++++++++++++ fxcore/registry.go | 62 ++ fxcore/registry_test.go | 83 ++ fxcore/renderer.go | 26 + fxcore/renderer_test.go | 32 + fxcore/root.go | 15 + fxcore/root_test.go | 15 + fxcore/templates/dashboard.html | 305 +++++++ fxcore/testdata/config/config.test.yaml | 3 + fxcore/testdata/config/config.yaml | 60 ++ fxcore/testdata/probes/failure.go | 21 + fxcore/testdata/probes/success.go | 21 + fxcore/testdata/probes/test.go | 21 + fxcore/testdata/templates/dashboard.html | 1 + release-please-config.json | 5 + 29 files changed, 3780 insertions(+) create mode 100644 .github/workflows/fxcore-ci.yml create mode 100644 fxcore/.golangci.yml create mode 100644 fxcore/README.md create mode 100644 fxcore/bootstrap.go create mode 100644 fxcore/bootstrap_test.go create mode 100644 fxcore/core.go create mode 100644 fxcore/core_test.go create mode 100644 fxcore/go.mod create mode 100644 fxcore/go.sum create mode 100644 fxcore/info.go create mode 100644 fxcore/info_test.go create mode 100644 fxcore/module.go create mode 100644 fxcore/module_test.go create mode 100644 fxcore/registry.go create mode 100644 fxcore/registry_test.go create mode 100644 fxcore/renderer.go create mode 100644 fxcore/renderer_test.go create mode 100644 fxcore/root.go create mode 100644 fxcore/root_test.go create mode 100644 fxcore/templates/dashboard.html create mode 100644 fxcore/testdata/config/config.test.yaml create mode 100644 fxcore/testdata/config/config.yaml create mode 100644 fxcore/testdata/probes/failure.go create mode 100644 fxcore/testdata/probes/success.go create mode 100644 fxcore/testdata/probes/test.go create mode 100644 fxcore/testdata/templates/dashboard.html diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c845cd1..968669d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -29,6 +29,7 @@ jobs: - "log" - "orm" - "trace" + - "fxcore" - "fxconfig" - "fxgenerate" - "fxhealthcheck" diff --git a/.github/workflows/fxcore-ci.yml b/.github/workflows/fxcore-ci.yml new file mode 100644 index 0000000..463aadb --- /dev/null +++ b/.github/workflows/fxcore-ci.yml @@ -0,0 +1,31 @@ +name: "fxcore-ci" + +on: + push: + branches: + - "feat**" + - "fix**" + - "hotfix**" + - "chore**" + paths: + - "fxcore/**.go" + - "fxcore/go.mod" + - "fxcore/go.sum" + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main + paths: + - "fxcore/**.go" + - "fxcore/go.mod" + - "fxcore/go.sum" + +jobs: + ci: + uses: ./.github/workflows/common-ci.yml + secrets: inherit + with: + module: "fxcore" diff --git a/README.md b/README.md index 0fefe56..8fa1e7e 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Yokai's `Fx modules` are the plugins for your Yokai application. | Fx Module | Description | |--------------------------------|-------------------------------------------------------------------------| +| [fxcore](fxcore) | Fx core module | | [fxconfig](fxconfig) | Fx module for [config](config) | | [fxgenerate](fxgenerate) | Fx module for [generate](generate) | | [fxhealthcheck](fxhealthcheck) | Fx module for [healthcheck](healthcheck) | diff --git a/fxcore/.golangci.yml b/fxcore/.golangci.yml new file mode 100644 index 0000000..7911516 --- /dev/null +++ b/fxcore/.golangci.yml @@ -0,0 +1,62 @@ +run: + timeout: 5m + concurrency: 8 + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - decorder + - dogsled + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + - forbidigo + - forcetypeassert + - gocognit + - gocritic + - gocyclo + - godot + - godox + - gofmt + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - logrlint + - maintidx + - makezero + - misspell + - nilerr + - nilnil + - nlreturn + - nolintlint + - nosprintfhostport + - predeclared + - promlinter + - reassign + - staticcheck + - tenv + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace diff --git a/fxcore/README.md b/fxcore/README.md new file mode 100644 index 0000000..f1cc02b --- /dev/null +++ b/fxcore/README.md @@ -0,0 +1,327 @@ +# Fx Core Module + +[![ci](https://github.com/ankorstore/yokai/actions/workflows/fxcore-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/fxcore-ci.yml) +[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/fxcore)](https://goreportcard.com/report/github.com/ankorstore/yokai/fxcore) +[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=ghUBlFsjhR&flag=fxcore)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/fxcore) +[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai%2Ffxcore) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/fxcore)](https://pkg.go.dev/github.com/ankorstore/yokai/fxcore) + +> [Fx](https://uber-go.github.io/fx/) core module. + + +* [Installation](#installation) +* [Features](#features) +* [Documentation](#documentation) + * [Preloaded modules](#preloaded-modules) + * [Configuration](#configuration) + * [Bootstrap](#bootstrap) + * [Application](#application) + * [Test application](#test-application) + * [Root dir](#root-dir) + + +## Installation + +```shell +go get github.com/ankorstore/yokai/fxcore +``` + +## Features + +The fxcore module provides the foundation of your application: + +- a bootstrapper +- a dependency injection system +- a dedicated core http server +- ready to use config, health check, logger and tracer and metrics components +- a plugin system for the [Fx modules](https://github.com/ankorstore/yokai#fx-modules) + +The core http server runs automatically on a dedicated port (default `8081`), to serve: + +- the dashboard: UI to get an overview of your application +- the metrics endpoint: to expose all collected metrics from your application +- the health check endpoints: to expose all configured health check probes of your application +- the debug endpoints: to expose various information about your config, modules, build, etc. + +Whatever your type of application (httpserver, gRPC server, worker, etc.), all platform concerns are handled by this +dedicated server: + +- to avoid to expose sensitive information (health checks, metrics, debug, etc) to your users +- and most importantly to enable your application to focus on its logic + +## Documentation + +### Preloaded modules + +This core module preloads: + +- the [fxconfig](https://github.com/ankorstore/yokai/tree/main/fxconfig) module +- the [fxlog](https://github.com/ankorstore/yokai/tree/main/fxlog) module +- the [fxtrace](https://github.com/ankorstore/yokai/tree/main/fxtrace) module +- the [fxmetrics](https://github.com/ankorstore/yokai/tree/main/fxmetrics) module +- the [fxgenerate](https://github.com/ankorstore/yokai/tree/main/fxgenerate) module +- the [fxhealthcheck](https://github.com/ankorstore/yokai/tree/main/fxhealthcheck) module + +### Configuration + +Configuration reference: + +```yaml +# ./configs/config.yaml +app: + name: app + env: dev + version: 0.1.0 + debug: true +modules: + log: + level: info + output: stdout + trace: + processor: + type: stdout + core: + server: + port: 8081 # core http server port (default 8081) + errors: + obfuscate: false # to obfuscate error messages on the core http server responses + stack: false # to add error stack trace to error response of the core http server + log: + headers: # to log incoming request headers on the core http server + x-foo: foo # to log for example the header x-foo in the log field foo + x-bar: bar + exclude: # to exclude specific routes from logging + - /healthz + - /livez + - /readyz + - /metrics + level_from_response: true # to use response status code for log level (ex: 500=error) + trace: + enabled: true # to trace incoming request headers on the core http server + exclude: # to exclude specific routes from tracing + - /healthz + - /livez + - /readyz + - /metrics + metrics: + expose: true # to expose metrics route, disabled by default + path: /metrics # metrics route path (default /metrics) + collect: + enabled: true # to collect core http server metrics, disabled by default + namespace: app # core http server metrics namespace (default app.name value) + subsystem: fx-core # core http server metrics subsystem (default fx-core) + buckets: 0.1, 1, 10 # to override default request duration buckets + normalize: true # to normalize http status code (2xx, 3xx, ...) + healthcheck: + startup: + expose: true # to expose health check startup route, disabled by default + path: /healthz # health check startup route path (default /healthz) + readiness: + expose: true # to expose health check readiness route, disabled by default + path: /readyz # health check readiness route path (default /readyz) + liveness: + expose: true # to expose health check liveness route, disabled by default + path: /livez # health check liveness route path (default /livez) + debug: + config: + expose: true # to expose debug config route + path: /debug/config # debug config route path (default /debug/config) + pprof: + expose: true # to expose debug pprof route + path: /debug/pprof # debug pprof route path (default /debug/pprof) + routes: + expose: true # to expose debug routes route + path: /debug/routes # debug routes route path (default /debug/routes) + stats: + expose: true # to expose debug stats route + path: /debug/stats # debug stats route path (default /debug/stats) + build: + expose: true # to expose debug build route + path: /debug/build # debug build route path (default /debug/build) + modules: + expose: true # to expose debug modules route + path: /debug/modules/:name # debug modules route path (default /debug/modules/:name) +``` + +Notes: + +- the core http server requests logging will be based on + the [fxlog](https://github.com/ankorstore/yokai/tree/main/fxlog) module configuration +- the core http server requests tracing will be based on + the [fxtrace](https://github.com/ankorstore/yokai/tree/main/fxtrace) module configuration +- if `app.debug=true` (or env var `APP_DEBUG=true`): + - all the debug endpoints will be automatically exposed + - error responses will not be obfuscated and stack trace will be added + +Check the [configuration files documentation](https://github.com/ankorstore/yokai/tree/main/config#configuration-files) +for more details. + +### Bootstrap + +The core module provides a bootstrapper: + +- to plug in all the [Fx modules](https://github.com/ankorstore/yokai#fx-modules) required by your application +- to provide your own application modules and services +- to start your application (real or test runtime) + +#### Application + +Create an application service, for example depending on a database connection: + +```go +package service + +import ( + "gorm.io/gorm" +) + +type ExampleService struct { + db *gorm.DB +} + +func NewExampleService(db *gorm.DB) *ExampleService { + return &ExampleService{ + db: db, + } +} + +func (s *ExampleService) Ping() bool { + return s.db.Ping() // simplification +} +``` + +Create your application [Bootstrapper](bootstrap.go) with your bootstrap options: + +```go +package bootstrap + +import ( + "github.com/ankorstore/yokai/fxcore" + "github.com/ankorstore/yokai/fxorm" + "go.uber.org/fx" + "gorm.io/gorm" + "path/to/service" +) + +var Bootstrapper = fxcore.NewBootstrapper().WithOptions( + fxorm.FxOrmModule, // load the ORM module (provides *gorm.DB) + fx.Provide(service.NewExampleService), // autowire your service (*gorm.DB auto injection) +) +``` + +You can use the bootstrapper to start your application: + +```go +package main + +import ( + "context" + + "github.com/ankorstore/yokai/fxcore" + "path/to/bootstrap" +) + +func main() { + // run the application + bootstrap.Bootstrapper.RunApp() + + // or you can also run the application with a specific root context + bootstrap.Bootstrapper.WithContext(context.Background()).RunApp() + + // or you can also bootstrap and run it on your own + app := bootstrap.Bootstrapper.BootstrapApp() + app.Run() +} +``` + +#### Test application + +You can reuse your [Bootstrapper](bootstrap.go) to run your application in test mode: + +```go +package main_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/fx" + "path/to/bootstrap" + "path/to/service" +) + +func TestExampleService(t *testing.T) { + // *service.ExampleService instance to extract from your application + var svc *service.ExampleService + + // run the app in test mode and populate the service + bootstrap.Bootstrapper.RunTestApp(t, fx.Populate(&svc)) + + // assertion example + assert.True(t, svc.Ping()) +} +``` + +You can also use `BootstrapTestApp()` to bootstrap in test mode and run it on your own: + +```go +testApp := bootstrap.Bootstrapper.BootstrapTestApp(t, ...) +testApp.RequireStart().RequireStop() +``` + +Note: bootstrapping your application in test mode will set `APP_ENV=test`, automatically loading your testing +configuration. + +#### Root dir + +The core module provides the possibility to retrieve the root dir with `RootDir()`, useful for setting relative +path to templates or configs. + +```go +package bootstrap + +import ( + "github.com/ankorstore/yokai/fxcore" +) + +var RootDir string + +func init() { + RootDir = fxcore.RootDir(0) // configure number of stack frames to ascend +} +``` + +Then you can then use the global `RootDir` variable in any packages: + +```go +package main + +import ( + "fmt" + + "path/to/bootstrap" +) + +func main() { + fmt.Printf("root dir: %s", bootstrap.RootDir) +} +``` + +Or in any tests: + +```go +package main_test + +import ( + "fmt" + "testing" + + "path/to/bootstrap" +) + +func TestSomething(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", fmt.Sprintf("%s/configs", bootstrap.RootDir)) + + //... +} +``` diff --git a/fxcore/bootstrap.go b/fxcore/bootstrap.go new file mode 100644 index 0000000..f6fdb58 --- /dev/null +++ b/fxcore/bootstrap.go @@ -0,0 +1,79 @@ +package fxcore + +import ( + "context" + "testing" + + "github.com/ankorstore/yokai/fxlog" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +// Bootstrapper is the application bootstrapper, that can load a list [fx.Option] and run your application. +// +//nolint:containedctx +type Bootstrapper struct { + context context.Context + options []fx.Option +} + +// NewBootstrapper returns a new [Bootstrapper]. +func NewBootstrapper() *Bootstrapper { + return &Bootstrapper{ + context: context.Background(), + options: []fx.Option{ + FxCoreModule, + }, + } +} + +// WithContext is used to pass a parent [context.Context]. +func (b *Bootstrapper) WithContext(ctx context.Context) *Bootstrapper { + b.context = ctx + + return b +} + +// WithOptions is used to pass a list of [fx.Option]. +func (b *Bootstrapper) WithOptions(options ...fx.Option) *Bootstrapper { + b.options = append(b.options, options...) + + return b +} + +// BootstrapApp boostrap the application, accepting optional bootstrap options. +func (b *Bootstrapper) BootstrapApp(options ...fx.Option) *fx.App { + return fx.New( + fx.Supply(fx.Annotate(b.context, fx.As(new(context.Context)))), + fx.WithLogger(fxlog.NewFxEventLogger), + fx.Options(b.options...), + fx.Options(options...), + ) +} + +// BootstrapTestApp boostrap the application in test mode, accepting a testing context and optional bootstrap options. +func (b *Bootstrapper) BootstrapTestApp(tb testing.TB, options ...fx.Option) *fxtest.App { + tb.Helper() + + tb.Setenv("APP_ENV", "test") + + return fxtest.New( + tb, + fx.Supply(fx.Annotate(b.context, fx.As(new(context.Context)))), + fx.NopLogger, + fx.Options(b.options...), + fx.Options(options...), + ) +} + +// RunApp runs the application, accepting optional runtime options. +func (b *Bootstrapper) RunApp(options ...fx.Option) { + b.BootstrapApp(options...).Run() +} + +// RunTestApp runs the application in test mode, accepting a testing context and optional runtime options. +func (b *Bootstrapper) RunTestApp(tb testing.TB, options ...fx.Option) { + tb.Helper() + + b.BootstrapTestApp(tb, options...).RequireStart().RequireStop() +} diff --git a/fxcore/bootstrap_test.go b/fxcore/bootstrap_test.go new file mode 100644 index 0000000..ae9eaf2 --- /dev/null +++ b/fxcore/bootstrap_test.go @@ -0,0 +1,274 @@ +package fxcore_test + +import ( + "context" + "testing" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcore" + "github.com/ankorstore/yokai/fxcore/testdata/probes" + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/healthcheck" + "github.com/stretchr/testify/assert" + "go.uber.org/fx" +) + +type testCtxKey struct{} + +func TestBootstrapApp(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var cfg *config.Config + + app := fxcore.NewBootstrapper().BootstrapApp(fx.Populate(&cfg)) + + ctx := context.Background() + + err := app.Start(ctx) + assert.NoError(t, err) + + err = app.Stop(ctx) + assert.NoError(t, err) + + assert.Equal(t, "core-app", cfg.AppName()) + assert.Equal(t, config.AppEnvDev, cfg.AppEnv()) + assert.False(t, cfg.AppDebug()) + assert.Equal(t, "0.1.0", cfg.AppVersion()) +} + +func TestBootstrapAppWithContext(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + ctx := context.WithValue(context.Background(), testCtxKey{}, "test-value") + + var popCtx context.Context + + app := fxcore.NewBootstrapper().WithContext(ctx).BootstrapApp(fx.Populate(&popCtx)) + + err := app.Start(ctx) + assert.NoError(t, err) + + err = app.Stop(ctx) + assert.NoError(t, err) + + assert.Equal(t, "test-value", popCtx.Value(testCtxKey{})) +} + +func TestBootstrapAppWithOptions(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var checker *healthcheck.Checker + + app := fxcore. + NewBootstrapper(). + WithOptions( + fxhealthcheck.AsCheckerProbe(probes.NewSuccessProbe), + fxhealthcheck.AsCheckerProbe(probes.NewFailureProbe, healthcheck.Readiness), + ). + BootstrapApp(fx.Populate(&checker)) + + ctx := context.Background() + + err := app.Start(ctx) + assert.NoError(t, err) + + err = app.Stop(ctx) + assert.NoError(t, err) + + result := checker.Check(context.Background(), healthcheck.Readiness) + assert.False(t, result.Success) + + for probeName, probeResult := range result.ProbesResults { + if probeName == "success" { + assert.True(t, probeResult.Success) + assert.Equal(t, "success", probeResult.Message) + } + if probeName == "failure" { + assert.False(t, probeResult.Success) + assert.Equal(t, "failure", probeResult.Message) + } + } +} + +func TestRunAppWithContext(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + shutdown := func(sd fx.Shutdowner, lc fx.Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(context.Context) error { + return sd.Shutdown() + }, + }) + } + + ctx := context.WithValue(context.Background(), testCtxKey{}, "test-value") + + var popCtx context.Context + + fxcore. + NewBootstrapper(). + WithContext(ctx). + WithOptions(fx.Invoke(shutdown)). + RunApp(fx.Populate(&popCtx)) + + assert.Equal(t, "test-value", popCtx.Value(testCtxKey{})) +} + +func TestRunAppWithOptions(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + shutdown := func(sd fx.Shutdowner, lc fx.Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(context.Context) error { + return sd.Shutdown() + }, + }) + } + + var checker *healthcheck.Checker + + fxcore. + NewBootstrapper(). + WithOptions( + fxhealthcheck.AsCheckerProbe(probes.NewSuccessProbe), + fxhealthcheck.AsCheckerProbe(probes.NewFailureProbe, healthcheck.Readiness), + ). + WithOptions(fx.Invoke(shutdown)). + RunApp(fx.Populate(&checker)) + + result := checker.Check(context.Background(), healthcheck.Readiness) + assert.False(t, result.Success) + + for probeName, probeResult := range result.ProbesResults { + if probeName == "success" { + assert.True(t, probeResult.Success) + assert.Equal(t, "success", probeResult.Message) + } + if probeName == "failure" { + assert.False(t, probeResult.Success) + assert.Equal(t, "failure", probeResult.Message) + } + } +} + +func TestBootstrapTestApp(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var cfg *config.Config + + fxcore. + NewBootstrapper(). + BootstrapTestApp(t, fx.Populate(&cfg)). + RequireStart(). + RequireStop() + + assert.Equal(t, "core-app", cfg.AppName()) + assert.Equal(t, config.AppEnvTest, cfg.AppEnv()) + assert.True(t, cfg.AppDebug()) + assert.Equal(t, "0.1.0", cfg.AppVersion()) +} + +func TestBootstrapTestAppWithContext(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + ctx := context.WithValue(context.Background(), testCtxKey{}, "test-value") + + var popCtx context.Context + + fxcore. + NewBootstrapper(). + WithContext(ctx). + BootstrapTestApp(t, fx.Populate(&popCtx)). + RequireStart(). + RequireStop() + + assert.Equal(t, "test-value", popCtx.Value(testCtxKey{})) +} + +func TestBootstrapTestAppWithOptions(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var checker *healthcheck.Checker + + fxcore. + NewBootstrapper(). + WithOptions( + fxhealthcheck.AsCheckerProbe(probes.NewSuccessProbe), + fxhealthcheck.AsCheckerProbe(probes.NewFailureProbe, healthcheck.Readiness), + ). + BootstrapTestApp(t, fx.Populate(&checker)). + RequireStart(). + RequireStop() + + result := checker.Check(context.Background(), healthcheck.Readiness) + assert.False(t, result.Success) + + for probeName, probeResult := range result.ProbesResults { + if probeName == "success" { + assert.True(t, probeResult.Success) + assert.Equal(t, "success", probeResult.Message) + } + if probeName == "failure" { + assert.False(t, probeResult.Success) + assert.Equal(t, "failure", probeResult.Message) + } + } +} + +func TestRunTestApp(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var cfg *config.Config + + fxcore. + NewBootstrapper(). + RunTestApp(t, fx.Populate(&cfg)) + + assert.Equal(t, "core-app", cfg.AppName()) + assert.Equal(t, config.AppEnvTest, cfg.AppEnv()) + assert.True(t, cfg.AppDebug()) + assert.Equal(t, "0.1.0", cfg.AppVersion()) +} + +func TestRunTestAppWithContext(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + ctx := context.WithValue(context.Background(), testCtxKey{}, "test-value") + + var popCtx context.Context + + fxcore. + NewBootstrapper(). + WithContext(ctx). + RunTestApp(t, fx.Populate(&popCtx)) + + assert.Equal(t, "test-value", popCtx.Value(testCtxKey{})) +} + +func TestRunTestAppWithOptions(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var checker *healthcheck.Checker + + fxcore. + NewBootstrapper(). + WithOptions( + fxhealthcheck.AsCheckerProbe(probes.NewSuccessProbe), + fxhealthcheck.AsCheckerProbe(probes.NewFailureProbe, healthcheck.Readiness), + ). + RunTestApp(t, fx.Populate(&checker)) + + result := checker.Check(context.Background(), healthcheck.Readiness) + assert.False(t, result.Success) + + for probeName, probeResult := range result.ProbesResults { + if probeName == "success" { + assert.True(t, probeResult.Success) + assert.Equal(t, "success", probeResult.Message) + } + if probeName == "failure" { + assert.False(t, probeResult.Success) + assert.Equal(t, "failure", probeResult.Message) + } + } +} diff --git a/fxcore/core.go b/fxcore/core.go new file mode 100644 index 0000000..2071629 --- /dev/null +++ b/fxcore/core.go @@ -0,0 +1,38 @@ +package fxcore + +import ( + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/healthcheck" + "github.com/labstack/echo/v4" +) + +// Core is the core component, holding the core config, health checker and http server. +type Core struct { + config *config.Config + checker *healthcheck.Checker + httpServer *echo.Echo +} + +// NewCore returns a new [Core]. +func NewCore(config *config.Config, checker *healthcheck.Checker, httpServer *echo.Echo) *Core { + return &Core{ + config: config, + checker: checker, + httpServer: httpServer, + } +} + +// Config returns the core config. +func (c *Core) Config() *config.Config { + return c.config +} + +// Checker returns the core health checker. +func (c *Core) Checker() *healthcheck.Checker { + return c.checker +} + +// HttpServer returns the core http server. +func (c *Core) HttpServer() *echo.Echo { + return c.httpServer +} diff --git a/fxcore/core_test.go b/fxcore/core_test.go new file mode 100644 index 0000000..9821502 --- /dev/null +++ b/fxcore/core_test.go @@ -0,0 +1,33 @@ +package fxcore_test + +import ( + "testing" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcore" + "github.com/ankorstore/yokai/healthcheck" + "github.com/ankorstore/yokai/httpserver" + "github.com/stretchr/testify/assert" +) + +func TestNewCore(t *testing.T) { + t.Parallel() + + cfg, err := config.NewDefaultConfigFactory().Create( + config.WithFilePaths("./testdata/config"), + ) + assert.NoError(t, err) + + checker, err := healthcheck.NewDefaultCheckerFactory().Create() + assert.NoError(t, err) + + server, err := httpserver.NewDefaultHttpServerFactory().Create() + assert.NoError(t, err) + + core := fxcore.NewCore(cfg, checker, server) + assert.IsType(t, &fxcore.Core{}, core) + + assert.Equal(t, cfg, core.Config()) + assert.Equal(t, checker, core.Checker()) + assert.Equal(t, server, core.HttpServer()) +} diff --git a/fxcore/go.mod b/fxcore/go.mod new file mode 100644 index 0000000..17c6d34 --- /dev/null +++ b/fxcore/go.mod @@ -0,0 +1,87 @@ +module github.com/ankorstore/yokai/fxcore + +go 1.20 + +require ( + github.com/ankorstore/yokai/config v1.1.0 + github.com/ankorstore/yokai/fxconfig v1.0.0 + github.com/ankorstore/yokai/fxgenerate v1.0.0 + github.com/ankorstore/yokai/fxhealthcheck v1.0.0 + github.com/ankorstore/yokai/fxlog v1.0.0 + github.com/ankorstore/yokai/fxmetrics v1.0.0 + github.com/ankorstore/yokai/fxtrace v1.1.0 + github.com/ankorstore/yokai/generate v1.0.0 + github.com/ankorstore/yokai/healthcheck v1.0.0 + github.com/ankorstore/yokai/httpserver v1.0.0 + github.com/ankorstore/yokai/log v1.0.0 + github.com/ankorstore/yokai/trace v1.0.0 + github.com/arl/statsviz v0.6.0 + github.com/labstack/echo/v4 v4.11.1 + github.com/prometheus/client_golang v1.18.0 + github.com/rs/zerolog v1.31.0 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.17.0 + go.opentelemetry.io/otel/trace v1.17.0 + go.uber.org/fx v1.20.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/labstack/gommon v0.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.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.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.17.0 // indirect + go.opentelemetry.io/otel/sdk v1.16.0 // indirect + go.opentelemetry.io/proto/otlp v0.19.0 // indirect + go.uber.org/dig v1.17.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fxcore/go.sum b/fxcore/go.sum new file mode 100644 index 0000000..0a4a31e --- /dev/null +++ b/fxcore/go.sum @@ -0,0 +1,588 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ankorstore/yokai/config v1.1.0 h1:z6xnsVXAbWhhjcb5kqVaw0VlaGZziGc7Di1bJlt5rf0= +github.com/ankorstore/yokai/config v1.1.0/go.mod h1:yDANaMWIOfAUkAMClG22Q4bzQk91NLwWK3WbL5IFnbg= +github.com/ankorstore/yokai/fxconfig v1.0.0 h1:zaYOLfpurqFJuS/IHeAXPOzrCNuttsFjJWNOK39opR4= +github.com/ankorstore/yokai/fxconfig v1.0.0/go.mod h1:p+x6Jp8aLv1+uE1qO42KF+yahBK+VJdPP1/YReBjJ7M= +github.com/ankorstore/yokai/fxgenerate v1.0.0 h1:jLe6FVnUqTkHZINK/LmjoD3C+CaZuNlMlQ/JJp0T1Cg= +github.com/ankorstore/yokai/fxgenerate v1.0.0/go.mod h1:o6ICl0t3DRC3xUUm/z11EIA53BA8dHwZkJJaMVMgnGk= +github.com/ankorstore/yokai/fxhealthcheck v1.0.0 h1:NbH1QDj8JflsHV9tbLqZVU5vXvbEiFftzBa755o6hCg= +github.com/ankorstore/yokai/fxhealthcheck v1.0.0/go.mod h1:jEQ3pSXoyZrEOIF7asfEgxmsRssHzwavT5now+yzmq4= +github.com/ankorstore/yokai/fxlog v1.0.0 h1:ujq/XxgCK0uwKCNSt86XEYR2vqYbXZX2/lA/pQHZX4A= +github.com/ankorstore/yokai/fxlog v1.0.0/go.mod h1:juQnBYNddDVOa7Ukhw8axLYWyibDDMJAwG7MDpluKnk= +github.com/ankorstore/yokai/fxmetrics v1.0.0 h1:jA1MnIRzRqBk4JsdCcQxPZ6Jvmpd+uyoBwO7c0vUCMc= +github.com/ankorstore/yokai/fxmetrics v1.0.0/go.mod h1:No9z3tnPxAyjYiXfHcpGjZzBwYB/OSs80L7w0oiXXmM= +github.com/ankorstore/yokai/fxtrace v1.1.0 h1:UBzz5mo0kvfbp2fEaY/2Mamy4lkWoJiWe8iz2bDl+Vw= +github.com/ankorstore/yokai/fxtrace v1.1.0/go.mod h1:DP/aNn65I+LU1QoBVvCLhFVr2djFUNFnclITmUxjQmc= +github.com/ankorstore/yokai/generate v1.0.0 h1:kHpbl8cet9qklUamMqSTJy3h6aiybKMgnAK6dDI42p8= +github.com/ankorstore/yokai/generate v1.0.0/go.mod h1:7/gebXdxAOmqeDG54RcguC0a+f3JtqEKVKtSy8f2dlk= +github.com/ankorstore/yokai/healthcheck v1.0.0 h1:uX6RrchsvbxCV70dh5d6RX5LEuGIf+Pt+14waV0CzY0= +github.com/ankorstore/yokai/healthcheck v1.0.0/go.mod h1:Frz73NuG8ruLDz04vQxzf0bWhKK1Ru2Ktod+3ltaIxs= +github.com/ankorstore/yokai/httpserver v1.0.0 h1:ROCsM1L/tCSA9zcOpSwrpecQv8twbs3hYtrZ5rFkRF8= +github.com/ankorstore/yokai/httpserver v1.0.0/go.mod h1:W72H3+ok6sUY41Qj5TdhjFqyDlQ9nC4JFwKVQIT6+1A= +github.com/ankorstore/yokai/log v1.0.0 h1:9NsM0J+1O028WuNDW7vr0yeUdWDX1JKYTkuz7hiYCSs= +github.com/ankorstore/yokai/log v1.0.0/go.mod h1:lyBRVA8VkrmlNjaR2jVTH9XjV06ioolWTuDVN6wF0vk= +github.com/ankorstore/yokai/trace v1.0.0 h1:EKWXyg2W8v3xszIiB5JfiDwU2OUfSDOo8LXJMDxlSrw= +github.com/ankorstore/yokai/trace v1.0.0/go.mod h1:OhCIJouVmBD7je1dIynqR1mhMEFCBzidy16a624lwBw= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/arl/statsviz v0.6.0 h1:jbW1QJkEYQkufd//4NDYRSNBpwJNrdzPahF7ZmoGdyE= +github.com/arl/statsviz v0.6.0/go.mod h1:0toboo+YGSUXDaS4g1D5TVS4dXs7S7YYT5J/qnW2h8s= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +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/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +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/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= +github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +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/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= +go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 h1:+XWJd3jf75RXJq29mxbuXhCXFDG3S3R4vBUeSI2P7tE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0/go.mod h1:hqgzBPTf4yONMFgdZvL/bK42R/iinTyVQtiWihs3SZc= +go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= +go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= +go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/fxcore/info.go b/fxcore/info.go new file mode 100644 index 0000000..79bd041 --- /dev/null +++ b/fxcore/info.go @@ -0,0 +1,82 @@ +package fxcore + +import ( + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/log" + "github.com/ankorstore/yokai/trace" + "github.com/rs/zerolog" +) + +// FxModuleInfo is the interface to implement by modules to provide their info to fxcore. +type FxModuleInfo interface { + Name() string + Data() map[string]any +} + +// FxCoreModuleInfo is a module info collector for fxcore. +type FxCoreModuleInfo struct { + AppName string + AppEnv string + AppDebug bool + AppVersion string + LogLevel string + LogOutput string + TraceProcessor string + TraceSampler string +} + +// NewFxCoreModuleInfo returns a new [FxCoreModuleInfo]. +func NewFxCoreModuleInfo(cfg *config.Config) *FxCoreModuleInfo { + logLevel, logOutput := "", "" + if cfg.IsTestEnv() { + logLevel = zerolog.DebugLevel.String() + logOutput = log.TestOutputWriter.String() + } else { + logLevel = log.FetchLogLevel(cfg.GetString("modules.log.level")).String() + logOutput = log.FetchLogOutputWriter(cfg.GetString("modules.log.output")).String() + } + + traceProcessor := "" + traceSampler := trace.FetchSampler(cfg.GetString("modules.trace.sampler.type")).String() + if cfg.IsTestEnv() { + traceProcessor = trace.TestSpanProcessor.String() + } else { + traceProcessor = trace.FetchSpanProcessor(cfg.GetString("modules.trace.processor.type")).String() + } + + return &FxCoreModuleInfo{ + AppName: cfg.AppName(), + AppEnv: cfg.AppEnv(), + AppDebug: cfg.AppDebug(), + AppVersion: cfg.AppVersion(), + LogLevel: logLevel, + LogOutput: logOutput, + TraceProcessor: traceProcessor, + TraceSampler: traceSampler, + } +} + +// Name return the name of the module info. +func (i *FxCoreModuleInfo) Name() string { + return ModuleName +} + +// Data return the data of the module info. +func (i *FxCoreModuleInfo) Data() map[string]interface{} { + return map[string]interface{}{ + "app": map[string]interface{}{ + "name": i.AppName, + "env": i.AppEnv, + "debug": i.AppDebug, + "version": i.AppVersion, + }, + "log": map[string]interface{}{ + "level": i.LogLevel, + "output": i.LogOutput, + }, + "trace": map[string]interface{}{ + "processor": i.TraceProcessor, + "sampler": i.TraceSampler, + }, + } +} diff --git a/fxcore/info_test.go b/fxcore/info_test.go new file mode 100644 index 0000000..7de5279 --- /dev/null +++ b/fxcore/info_test.go @@ -0,0 +1,44 @@ +package fxcore_test + +import ( + "testing" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcore" + "github.com/stretchr/testify/assert" +) + +func TestNewFxCoreModuleInfo(t *testing.T) { + t.Setenv("APP_ENV", "test") + + cfg, err := config.NewDefaultConfigFactory().Create( + config.WithFilePaths("./testdata/config"), + ) + assert.NoError(t, err) + + info := fxcore.NewFxCoreModuleInfo(cfg) + assert.IsType(t, &fxcore.FxCoreModuleInfo{}, info) + assert.Implements(t, (*fxcore.FxModuleInfo)(nil), info) + + assert.Equal(t, fxcore.ModuleName, info.Name()) + assert.Equal( + t, + map[string]interface{}{ + "app": map[string]interface{}{ + "name": "core-app", + "env": "test", + "debug": true, + "version": "0.1.0", + }, + "log": map[string]interface{}{ + "level": "debug", + "output": "test", + }, + "trace": map[string]interface{}{ + "processor": "test", + "sampler": "parent-based-always-on", + }, + }, + info.Data(), + ) +} diff --git a/fxcore/module.go b/fxcore/module.go new file mode 100644 index 0000000..67f7de1 --- /dev/null +++ b/fxcore/module.go @@ -0,0 +1,478 @@ +package fxcore + +import ( + "context" + "embed" + "fmt" + "github.com/labstack/echo/v4" + "net/http" + "strconv" + "strings" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/generate/uuid" + "github.com/ankorstore/yokai/healthcheck" + "github.com/ankorstore/yokai/httpserver" + "github.com/ankorstore/yokai/httpserver/handler" + httpservermiddleware "github.com/ankorstore/yokai/httpserver/middleware" + "github.com/ankorstore/yokai/log" + "github.com/arl/statsviz" + "github.com/labstack/echo/v4/middleware" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + oteltrace "go.opentelemetry.io/otel/trace" + "go.uber.org/fx" +) + +const ( + ModuleName = "core" + DefaultPort = 8081 + DefaultMetricsPath = "/metrics" + DefaultHealthCheckStartupPath = "/healthz" + DefaultHealthCheckLivenessPath = "/livez" + DefaultHealthCheckReadinessPath = "/readyz" + DefaultDebugConfigPath = "/debug/config" + DefaultDebugPProfPath = "/debug/pprof" + DefaultDebugBuildPath = "/debug/build" + DefaultDebugRoutesPath = "/debug/routes" + DefaultDebugStatsPath = "/debug/stats" + DefaultDebugModulesPath = "/debug/modules" + ThemeLight = "light" + ThemeDark = "dark" +) + +//go:embed templates/* +var templatesFS embed.FS + +// FxCoreModule is the [Fx] core module. +// +// [Fx]: https://github.com/uber-go/fx +var FxCoreModule = fx.Module( + ModuleName, + fxgenerate.FxGenerateModule, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxhealthcheck.FxHealthcheckModule, + fx.Provide( + NewFxModuleInfoRegistry, + NewFxCore, + fx.Annotate( + NewFxCoreModuleInfo, + fx.As(new(interface{})), + fx.ResultTags(`group:"core-module-infos"`), + ), + ), + fx.Invoke(func(logger *log.Logger, core *Core) { + logger.Debug().Msg("starting core") + }), +) + +// FxCoreDashboardTheme is the theme for the core dashboard. +type FxCoreDashboardTheme struct { + Theme string `form:"theme" json:"theme"` +} + +// FxCoreParam allows injection of the required dependencies in [NewFxCore]. +// +//nolint:containedctx +type FxCoreParam struct { + fx.In + Context context.Context + LifeCycle fx.Lifecycle + Generator uuid.UuidGenerator + TracerProvider oteltrace.TracerProvider + Checker *healthcheck.Checker + Config *config.Config + Logger *log.Logger + Registry *FxModuleInfoRegistry + MetricsRegistry *prometheus.Registry +} + +// NewFxCore returns a new [Core]. +func NewFxCore(p FxCoreParam) (*Core, error) { + appDebug := p.Config.AppDebug() + + // logger + coreLogger := httpserver.NewEchoLogger( + log.FromZerolog(p.Logger.ToZerolog().With().Str("module", ModuleName).Logger()), + ) + + // server + coreServer, err := httpserver.NewDefaultHttpServerFactory().Create( + httpserver.WithDebug(appDebug), + httpserver.WithBanner(false), + httpserver.WithRecovery(true), + httpserver.WithLogger(coreLogger), + httpserver.WithRenderer( + NewDashboardRenderer(templatesFS, "templates/dashboard.html"), + ), + httpserver.WithHttpErrorHandler( + httpserver.JsonErrorHandler( + p.Config.GetBool("modules.core.server.errors.obfuscate") || !appDebug, + p.Config.GetBool("modules.core.server.errors.stack") || appDebug, + ), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to create core http server: %w", err) + } + + // middlewares + coreServer = withMiddlewares(coreServer, p) + + // handlers + coreServer, err = withHandlers(coreServer, p) + if err != nil { + return nil, fmt.Errorf("failed to register core http server handlers: %w", err) + } + + // lifecycles + p.LifeCycle.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + port := p.Config.GetInt("modules.core.server.port") + if port == 0 { + port = DefaultPort + } + + //nolint:errcheck + go coreServer.Start(fmt.Sprintf(":%d", port)) + + return nil + }, + OnStop: func(ctx context.Context) error { + return coreServer.Shutdown(ctx) + }, + }) + + return NewCore(p.Config, p.Checker, coreServer), nil +} + +func withMiddlewares(coreServer *echo.Echo, p FxCoreParam) *echo.Echo { + // CORS middleware + coreServer.Use(middleware.CORS()) + + // request id middleware + coreServer.Use(httpservermiddleware.RequestIdMiddlewareWithConfig( + httpservermiddleware.RequestIdMiddlewareConfig{ + Generator: p.Generator, + }, + )) + + // request logger middleware + requestHeadersToLog := map[string]string{ + httpservermiddleware.HeaderXRequestId: httpservermiddleware.LogFieldRequestId, + } + + for headerName, fieldName := range p.Config.GetStringMapString("modules.core.server.log.headers") { + requestHeadersToLog[headerName] = fieldName + } + + coreServer.Use(httpservermiddleware.RequestLoggerMiddlewareWithConfig( + httpservermiddleware.RequestLoggerMiddlewareConfig{ + RequestHeadersToLog: requestHeadersToLog, + RequestUriPrefixesToExclude: p.Config.GetStringSlice("modules.core.server.log.exclude"), + LogLevelFromResponseOrErrorCode: p.Config.GetBool("modules.core.server.log.level_from_response"), + }, + )) + + // request tracer middleware + if p.Config.GetBool("modules.core.server.trace.enabled") { + coreServer.Use(httpservermiddleware.RequestTracerMiddlewareWithConfig( + p.Config.AppName(), + httpservermiddleware.RequestTracerMiddlewareConfig{ + TracerProvider: p.TracerProvider, + RequestUriPrefixesToExclude: p.Config.GetStringSlice("modules.core.server.trace.exclude"), + }, + )) + } + + // request metrics middleware + if p.Config.GetBool("modules.core.server.metrics.collect.enabled") { + namespace := p.Config.GetString("modules.core.server.metrics.collect.namespace") + if namespace == "" { + namespace = p.Config.AppName() + } + + subsystem := p.Config.GetString("modules.core.server.metrics.collect.subsystem") + if subsystem == "" { + subsystem = ModuleName + } + + var buckets []float64 + if bucketsConfig := p.Config.GetString("modules.core.server.metrics.buckets"); bucketsConfig != "" { + for _, s := range strings.Split(strings.ReplaceAll(bucketsConfig, " ", ""), ",") { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + buckets = append(buckets, f) + } + } + } + + metricsMiddlewareConfig := httpservermiddleware.RequestMetricsMiddlewareConfig{ + Registry: p.MetricsRegistry, + Namespace: strings.ReplaceAll(namespace, "-", "_"), + Subsystem: strings.ReplaceAll(subsystem, "-", "_"), + Buckets: buckets, + NormalizeHTTPStatus: p.Config.GetBool("modules.core.server.metrics.normalize"), + } + + coreServer.Use(httpservermiddleware.RequestMetricsMiddlewareWithConfig(metricsMiddlewareConfig)) + } + + return coreServer +} + +//nolint:cyclop,gocognit,gocyclo,maintidx +func withHandlers(coreServer *echo.Echo, p FxCoreParam) (*echo.Echo, error) { + appDebug := p.Config.AppDebug() + + // overview + info, err := p.Registry.Find(ModuleName) + if err != nil { + return nil, err + } + + // template expositions + metricsExpose := p.Config.GetBool("modules.core.server.metrics.expose") + startupExpose := p.Config.GetBool("modules.core.server.healthcheck.startup.expose") + livenessExpose := p.Config.GetBool("modules.core.server.healthcheck.liveness.expose") + readinessExpose := p.Config.GetBool("modules.core.server.healthcheck.readiness.expose") + configExpose := p.Config.GetBool("modules.core.server.debug.config.expose") + pprofExpose := p.Config.GetBool("modules.core.server.debug.pprof.expose") + routesExpose := p.Config.GetBool("modules.core.server.debug.routes.expose") + statsExpose := p.Config.GetBool("modules.core.server.debug.stats.expose") + buildExpose := p.Config.GetBool("modules.core.server.debug.build.expose") + modulesExpose := p.Config.GetBool("modules.core.server.debug.modules.expose") + + // template paths + metricsPath := p.Config.GetString("modules.core.server.metrics.path") + startupPath := p.Config.GetString("modules.core.server.healthcheck.startup.path") + livenessPath := p.Config.GetString("modules.core.server.healthcheck.liveness.path") + readinessPath := p.Config.GetString("modules.core.server.healthcheck.readiness.path") + configPath := p.Config.GetString("modules.core.server.debug.config.path") + pprofPath := p.Config.GetString("modules.core.server.debug.pprof.path") + routesPath := p.Config.GetString("modules.core.server.debug.routes.path") + statsPath := p.Config.GetString("modules.core.server.debug.stats.path") + buildPath := p.Config.GetString("modules.core.server.debug.build.path") + modulesPath := p.Config.GetString("modules.core.server.debug.modules.path") + + // metrics + if metricsExpose { + if metricsPath == "" { + metricsPath = DefaultMetricsPath + } + + coreServer.GET(metricsPath, echo.WrapHandler(promhttp.HandlerFor(p.MetricsRegistry, promhttp.HandlerOpts{}))) + + coreServer.Logger.Debug("registered metrics handler") + } + + // healthcheck startup + if startupExpose { + if startupPath == "" { + startupPath = DefaultHealthCheckStartupPath + } + + coreServer.GET(startupPath, handler.HealthCheckHandler(p.Checker, healthcheck.Startup)) + + coreServer.Logger.Debug("registered healthcheck startup handler") + } + + // healthcheck liveness + if livenessExpose { + if livenessPath == "" { + livenessPath = DefaultHealthCheckLivenessPath + } + + coreServer.GET(livenessPath, handler.HealthCheckHandler(p.Checker, healthcheck.Liveness)) + + coreServer.Logger.Debug("registered healthcheck liveness handler") + } + + // healthcheck readiness + if readinessExpose { + if readinessPath == "" { + readinessPath = DefaultHealthCheckReadinessPath + } + + coreServer.GET(readinessPath, handler.HealthCheckHandler(p.Checker, healthcheck.Readiness)) + + coreServer.Logger.Debug("registered healthcheck readiness handler") + } + + // debug config + if configExpose || appDebug { + if configPath == "" { + configPath = DefaultDebugConfigPath + } + + coreServer.GET(configPath, handler.DebugConfigHandler(p.Config)) + + coreServer.Logger.Debug("registered debug config handler") + } + + // debug pprof + if pprofExpose || appDebug { + if pprofPath == "" { + pprofPath = DefaultDebugPProfPath + } + + pprofGroup := coreServer.Group(pprofPath) + + pprofGroup.GET("/", handler.PprofIndexHandler()) + pprofGroup.GET("/allocs", handler.PprofAllocsHandler()) + pprofGroup.GET("/block", handler.PprofBlockHandler()) + pprofGroup.GET("/cmdline", handler.PprofCmdlineHandler()) + pprofGroup.GET("/goroutine", handler.PprofGoroutineHandler()) + pprofGroup.GET("/heap", handler.PprofHeapHandler()) + pprofGroup.GET("/mutex", handler.PprofMutexHandler()) + pprofGroup.GET("/profile", handler.PprofProfileHandler()) + pprofGroup.GET("/symbol", handler.PprofSymbolHandler()) + pprofGroup.POST("/symbol", handler.PprofSymbolHandler()) + pprofGroup.GET("/threadcreate", handler.PprofThreadCreateHandler()) + pprofGroup.GET("/trace", handler.PprofTraceHandler()) + + coreServer.Logger.Debug("registered debug pprof handlers") + } + + // debug routes + if routesExpose || appDebug { + if routesPath == "" { + routesPath = DefaultDebugRoutesPath + } + + coreServer.GET(routesPath, handler.DebugRoutesHandler(coreServer)) + + coreServer.Logger.Debug("registered debug routes handler") + } + + // debug stats + if statsExpose || appDebug { + if statsPath == "" { + statsPath = DefaultDebugStatsPath + } + + mux := http.NewServeMux() + + err := statsviz.Register(mux, statsviz.Root(statsPath)) + if err != nil { + coreServer.Logger.Error("failed to register debug stats handler") + } else { + statsGroup := coreServer.Group(statsPath) + + statsGroup.GET("/", echo.WrapHandler(mux)) + statsGroup.GET("/*", echo.WrapHandler(mux)) + + coreServer.Logger.Debug("registered debug stats handler") + } + } + + // debug build + if buildExpose || appDebug { + if buildPath == "" { + buildPath = DefaultDebugBuildPath + } + + coreServer.GET(buildPath, handler.DebugBuildHandler()) + + coreServer.Logger.Debug("registered debug build handler") + } + + // debug modules + if modulesExpose || appDebug { + if modulesPath == "" { + modulesPath = DefaultDebugModulesPath + } + + coreServer.GET(fmt.Sprintf("%s/:name", modulesPath), func(c echo.Context) error { + info, err := p.Registry.Find(c.Param("name")) + if err != nil { + return echo.NewHTTPError(http.StatusNotFound, err.Error()) + } + + return c.JSON(http.StatusOK, info.Data()) + }) + + coreServer.Logger.Debug("registered debug modules handler") + } + + // theme + coreServer.POST("/theme", func(c echo.Context) error { + themeCookie := new(http.Cookie) + themeCookie.Name = "theme" + + var theme FxCoreDashboardTheme + if err = c.Bind(&theme); err != nil { + themeCookie.Value = ThemeLight + } else { + switch theme.Theme { + case ThemeDark: + themeCookie.Value = ThemeDark + case ThemeLight: + themeCookie.Value = ThemeLight + default: + themeCookie.Value = ThemeLight + } + } + + c.SetCookie(themeCookie) + + return c.Redirect(http.StatusMovedPermanently, "/") + }) + + // dashboard + coreServer.GET("/", func(c echo.Context) error { + var theme string + themeCookie, err := c.Cookie("theme") + if err == nil { + switch themeCookie.Value { + case ThemeDark: + theme = ThemeDark + case ThemeLight: + theme = ThemeLight + default: + theme = ThemeLight + } + } else { + theme = ThemeLight + } + + return c.Render(http.StatusOK, "dashboard.html", map[string]interface{}{ + "info": info, + "metricsExpose": metricsExpose, + "metricsPath": metricsPath, + "startupExpose": startupExpose, + "startupPath": startupPath, + "livenessExpose": livenessExpose, + "livenessPath": livenessPath, + "readinessExpose": readinessExpose, + "readinessPath": readinessPath, + "configExpose": configExpose || appDebug, + "configPath": configPath, + "pprofExpose": pprofExpose || appDebug, + "pprofPath": pprofPath, + "routesExpose": routesExpose || appDebug, + "routesPath": routesPath, + "statsExpose": statsExpose || appDebug, + "statsPath": statsPath, + "buildExpose": buildExpose || appDebug, + "buildPath": buildPath, + "modulesExpose": modulesExpose || appDebug, + "modulesPath": modulesPath, + "modulesNames": p.Registry.Names(), + "theme": theme, + }) + }) + + coreServer.Logger.Debug("registered debug dashboard handler") + + return coreServer, nil +} diff --git a/fxcore/module_test.go b/fxcore/module_test.go new file mode 100644 index 0000000..71a88d5 --- /dev/null +++ b/fxcore/module_test.go @@ -0,0 +1,985 @@ +package fxcore_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ankorstore/yokai/fxcore" + "github.com/ankorstore/yokai/fxcore/testdata/probes" + "github.com/ankorstore/yokai/fxhealthcheck" + "github.com/ankorstore/yokai/healthcheck" + "github.com/ankorstore/yokai/log/logtest" + "github.com/ankorstore/yokai/trace/tracetest" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.uber.org/fx" +) + +func TestModuleWithMetricsDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("METRICS_ENABLED", "false") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /metrics + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/metrics", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /metrics", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/metrics"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithMetricsEnabledAndCollected(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + var metricsRegistry *prometheus.Registry + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter, &metricsRegistry)) + + // [GET] / twice to generate some metrics + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + + core.HttpServer().ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + + req = httptest.NewRequest(http.MethodGet, "/", nil) + rec = httptest.NewRecorder() + + core.HttpServer().ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + + // [GET] /metrics to check the metrics + req = httptest.NewRequest(http.MethodGet, "/metrics", nil) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + + // assert [GET] / counter = 2 + assert.Contains(t, rec.Body.String(), `foo_bar_request_duration_seconds_bucket{handler="/",method="GET",le="1"} 2`) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "uri": "/metrics", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /metrics", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/metrics"), + semconv.HTTPStatusCode(http.StatusOK), + ) + + // assert metrics + expectedMetric := ` + # HELP foo_bar_requests_total Number of processed HTTP requests + # TYPE foo_bar_requests_total counter + foo_bar_requests_total{handler="/",method="GET",status="2xx"} 2 + foo_bar_requests_total{handler="/metrics",method="GET",status="2xx"} 1 + ` + + err := testutil.GatherAndCompare( + metricsRegistry, + strings.NewReader(expectedMetric), + "foo_bar_requests_total", + ) + assert.NoError(t, err) +} + +func TestModuleWithHealthcheckDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("STARTUP_ENABLED", "false") + t.Setenv("LIVENESS_ENABLED", "false") + t.Setenv("READINESS_ENABLED", "false") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /healthz + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/healthz", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "GET /healthz", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/healthz"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) + + // [GET] /livez + logBuffer.Reset() + traceExporter.Reset() + + req = httptest.NewRequest(http.MethodGet, "/livez", nil) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/livez", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "GET /livez", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/livez"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) + + // [GET] /readyz + logBuffer.Reset() + traceExporter.Reset() + + req = httptest.NewRequest(http.MethodGet, "/readyz", nil) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/readyz", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "GET /readyz", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/readyz"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithHealthcheckEnabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("STARTUP_ENABLED", "true") + t.Setenv("LIVENESS_ENABLED", "true") + t.Setenv("READINESS_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp( + t, + fxhealthcheck.AsCheckerProbe(probes.NewSuccessProbe), + fxhealthcheck.AsCheckerProbe(probes.NewFailureProbe, healthcheck.Liveness), + fx.Populate(&core, &logBuffer, &traceExporter), + ) + + // [GET] /healthz + req := httptest.NewRequest(http.MethodGet, "/healthz", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, + `{"success":true,"probes":{"successProbe":{"success":true,"message":"success"}}}`, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + ) + + logtest.AssertHasNotLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "method": "GET", + "uri": "/healthz", + "message": "request logger", + }) + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "GET /healthz", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/healthz"), + semconv.HTTPStatusCode(http.StatusOK), + ) + + // [GET] /livez + logBuffer.Reset() + + req = httptest.NewRequest(http.MethodGet, "/livez", nil) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusInternalServerError, rec.Code) + assert.Equal(t, + `{"success":false,"probes":{"failureProbe":{"success":false,"message":"failure"},"successProbe":{"success":true,"message":"success"}}}`, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + ) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "error", + "service": "core-app", + "module": "core", + "successProbe": "success: true, message: success", + "failureProbe": "success: false, message: failure", + "message": "healthcheck failure", + }) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "error", + "service": "core-app", + "module": "core", + "method": "GET", + "uri": "/livez", + "message": "request logger", + }) + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "GET /livez", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/livez"), + semconv.HTTPStatusCode(http.StatusInternalServerError), + ) + + // [GET] /readyz + logBuffer.Reset() + + req = httptest.NewRequest(http.MethodGet, "/readyz", nil) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, + `{"success":true,"probes":{"successProbe":{"success":true,"message":"success"}}}`, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + ) + + logtest.AssertHasNotLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "method": "GET", + "uri": "/readyz", + "message": "request logger", + }) + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "GET /readyz", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/readyz"), + semconv.HTTPStatusCode(http.StatusOK), + ) +} + +func TestModuleWithDebugConfigDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("CONFIG_ENABLED", "false") + t.Setenv("APP_DEBUG", "false") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/config + req := httptest.NewRequest(http.MethodGet, "/debug/config", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/debug/config", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/config", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/config"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithDebugConfigEnabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("CONFIG_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/config + req := httptest.NewRequest(http.MethodGet, "/debug/config", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains( + t, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + `"name":"core-app"`, + ) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "uri": "/debug/config", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/config", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/config"), + semconv.HTTPStatusCode(http.StatusOK), + ) +} + +func TestModuleWithDebugPprofDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("PPROF_ENABLED", "false") + t.Setenv("APP_DEBUG", "false") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/pprof/ + req := httptest.NewRequest(http.MethodGet, "/debug/pprof/", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/debug/pprof/", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/pprof/", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/pprof/"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithDebugPprofEnabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("PPROF_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/pprof/ + req := httptest.NewRequest(http.MethodGet, "/debug/pprof/", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains( + t, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + `/debug/pprof/`, + ) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "uri": "/debug/pprof/", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/pprof/", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/pprof/"), + semconv.HTTPStatusCode(http.StatusOK), + ) +} + +func TestModuleWithDebugRoutesDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("ROUTES_ENABLED", "false") + t.Setenv("APP_DEBUG", "false") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/routes + req := httptest.NewRequest(http.MethodGet, "/debug/routes", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/debug/routes", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/routes", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/routes"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithDebugRoutesEnabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("ROUTES_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/routes + req := httptest.NewRequest(http.MethodGet, "/debug/routes", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains( + t, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + `"path":"/debug/routes"`, + ) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "uri": "/debug/routes", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/routes", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/routes"), + semconv.HTTPStatusCode(http.StatusOK), + ) +} + +func TestModuleWithDebugStatsDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("STATS_ENABLED", "false") + t.Setenv("APP_DEBUG", "false") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/stats/ + req := httptest.NewRequest(http.MethodGet, "/debug/stats/", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/debug/stats/", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/stats/", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/stats/"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithDebugStatsEnabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("STATS_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/stats/ + req := httptest.NewRequest(http.MethodGet, "/debug/stats/", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains( + t, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + `Statsviz`, + ) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "uri": "/debug/stats/", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/stats/", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/stats/"), + semconv.HTTPStatusCode(http.StatusOK), + ) +} + +func TestModuleWithDebugBuildDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("BUILD_ENABLED", "false") + t.Setenv("APP_DEBUG", "false") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/build + req := httptest.NewRequest(http.MethodGet, "/debug/build", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/debug/build", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/build", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/build"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithDebugBuildEnabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("BUILD_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/build + req := httptest.NewRequest(http.MethodGet, "/debug/build", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains( + t, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + `"version"`, + ) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "uri": "/debug/build", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/build", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/build"), + semconv.HTTPStatusCode(http.StatusOK), + ) +} + +func TestModuleWithDebugModulesDisabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("MODULES_ENABLED", "false") + t.Setenv("APP_DEBUG", "false") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/modules/core + req := httptest.NewRequest(http.MethodGet, "/debug/modules/core", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/debug/modules/core", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/modules/core", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/modules/core"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleWithDebugModulesEnabled(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("MODULES_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] /debug/modules/core + req := httptest.NewRequest(http.MethodGet, "/debug/modules/core", nil) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains( + t, + strings.ReplaceAll(strings.ReplaceAll(rec.Body.String(), " ", ""), "\n", ""), + `"name":"core-app"`, + ) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "core-app", + "module": "core", + "uri": "/debug/modules/core", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/modules/:name", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/modules/core"), + semconv.HTTPStatusCode(http.StatusOK), + ) + + // [GET] /debug/modules/invalid + req = httptest.NewRequest(http.MethodGet, "/debug/modules/invalid", nil) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Contains(t, rec.Body.String(), `fx module info with name invalid was not found`) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "core-app", + "module": "core", + "uri": "/debug/modules/invalid", + "status": 404, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /debug/modules/:name", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/debug/modules/invalid"), + semconv.HTTPStatusCode(http.StatusNotFound), + ) +} + +func TestModuleDashboard(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("MODULES_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [GET] / with light theme cookie + cookie := &http.Cookie{Name: "theme", Value: "light"} + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(cookie) + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), `data-bs-theme="light"`) + + // [GET] / with dark theme cookie + cookie = &http.Cookie{Name: "theme", Value: "dark"} + req = httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(cookie) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), `data-bs-theme="dark"`) + + // [GET] / with invalid theme cookie + cookie = &http.Cookie{Name: "theme", Value: "invalid"} + req = httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(cookie) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), `data-bs-theme="light"`) + + // [GET] / with no theme cookie + req = httptest.NewRequest(http.MethodGet, "/", nil) + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), `data-bs-theme="light"`) +} + +//nolint:bodyclose +func TestModuleDashboardTheme(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("MODULES_ENABLED", "true") + t.Setenv("METRICS_ENABLED", "true") + t.Setenv("METRICS_COLLECT", "true") + + var core *fxcore.Core + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxcore.NewBootstrapper().RunTestApp(t, fx.Populate(&core, &logBuffer, &traceExporter)) + + // [POST] /theme to switch to dark + data := `{"theme": "dark"}` + req := httptest.NewRequest(http.MethodPost, "/theme", bytes.NewBuffer([]byte(data))) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusMovedPermanently, rec.Code) + + for _, cookie := range rec.Result().Cookies() { + if cookie.Name == "theme" { + assert.Equal(t, "dark", cookie.Value) + } + } + + // [POST] /theme to switch to light + data = `{"theme": "light"}` + req = httptest.NewRequest(http.MethodPost, "/theme", bytes.NewBuffer([]byte(data))) + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusMovedPermanently, rec.Code) + + for _, cookie := range rec.Result().Cookies() { + if cookie.Name == "theme" { + assert.Equal(t, "light", cookie.Value) + } + } + + // [POST] /theme to switch to invalid + data = `{"theme": "invalid"}` + req = httptest.NewRequest(http.MethodPost, "/theme", bytes.NewBuffer([]byte(data))) + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusMovedPermanently, rec.Code) + + for _, cookie := range rec.Result().Cookies() { + if cookie.Name == "theme" { + assert.Equal(t, "light", cookie.Value) + } + } + + // [POST] /theme to switch with invalid + data = `{"theme": "invalid}` + req = httptest.NewRequest(http.MethodPost, "/theme", bytes.NewBuffer([]byte(data))) + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + core.HttpServer().ServeHTTP(rec, req) + + assert.Equal(t, http.StatusMovedPermanently, rec.Code) + + for _, cookie := range rec.Result().Cookies() { + if cookie.Name == "theme" { + assert.Equal(t, "light", cookie.Value) + } + } +} diff --git a/fxcore/registry.go b/fxcore/registry.go new file mode 100644 index 0000000..29a8c67 --- /dev/null +++ b/fxcore/registry.go @@ -0,0 +1,62 @@ +package fxcore + +import ( + "fmt" + "sort" + + "go.uber.org/fx" +) + +// FxModuleInfoRegistry is the registry collecting info about registered modules. +type FxModuleInfoRegistry struct { + infos map[string]FxModuleInfo +} + +// FxModuleInfoRegistryParam allows injection of the required dependencies in [NewFxModuleInfoRegistry]. +type FxModuleInfoRegistryParam struct { + fx.In + Infos []any `group:"core-module-infos"` +} + +// NewFxModuleInfoRegistry returns a new [FxModuleInfoRegistry]. +func NewFxModuleInfoRegistry(p FxModuleInfoRegistryParam) *FxModuleInfoRegistry { + infos := make(map[string]FxModuleInfo) + + for _, info := range p.Infos { + if castInfo, ok := info.(FxModuleInfo); ok { + infos[castInfo.Name()] = castInfo + } + } + + return &FxModuleInfoRegistry{ + infos: infos, + } +} + +func (r *FxModuleInfoRegistry) Names() []string { + names := make([]string, len(r.infos)) + + i := 0 + for name := range r.infos { + names[i] = name + i++ + } + + sort.Strings(names) + + return names +} + +// All returns a map of all registered [FxModuleInfo]. +func (r *FxModuleInfoRegistry) All() map[string]FxModuleInfo { + return r.infos +} + +// Find returns a [FxModuleInfo] by name. +func (r *FxModuleInfoRegistry) Find(name string) (FxModuleInfo, error) { + if info, ok := r.infos[name]; ok { + return info, nil + } + + return nil, fmt.Errorf("fx module info with name %s was not found", name) +} diff --git a/fxcore/registry_test.go b/fxcore/registry_test.go new file mode 100644 index 0000000..67c52d2 --- /dev/null +++ b/fxcore/registry_test.go @@ -0,0 +1,83 @@ +package fxcore_test + +import ( + "testing" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcore" + "github.com/stretchr/testify/assert" +) + +type testModuleInfo struct{} + +func (i *testModuleInfo) Name() string { + return "test" +} + +func (i *testModuleInfo) Data() map[string]interface{} { + return map[string]interface{}{} +} + +func TestNewFxModuleInfoRegistry(t *testing.T) { + t.Parallel() + + registry, err := prepareTestFxModuleInfoRegistry() + assert.NoError(t, err) + + assert.IsType(t, &fxcore.FxModuleInfoRegistry{}, registry) +} + +func TestAll(t *testing.T) { + t.Parallel() + + registry, err := prepareTestFxModuleInfoRegistry() + assert.NoError(t, err) + + assert.Len(t, registry.All(), 2) +} + +func TestNames(t *testing.T) { + t.Parallel() + + registry, err := prepareTestFxModuleInfoRegistry() + assert.NoError(t, err) + + assert.Equal(t, []string{fxcore.ModuleName, "test"}, registry.Names()) +} + +func TestFind(t *testing.T) { + t.Parallel() + + registry, err := prepareTestFxModuleInfoRegistry() + assert.NoError(t, err) + + testInfo, err := registry.Find("test") + assert.NoError(t, err) + assert.Equal(t, "test", testInfo.Name()) + + coreInfo, err := registry.Find(fxcore.ModuleName) + assert.NoError(t, err) + assert.Equal(t, fxcore.ModuleName, coreInfo.Name()) + + invalidInfo, err := registry.Find("invalid") + assert.Error(t, err) + assert.Equal(t, "fx module info with name invalid was not found", err.Error()) + assert.Nil(t, invalidInfo) +} + +func prepareTestFxModuleInfoRegistry() (*fxcore.FxModuleInfoRegistry, error) { + cfg, err := config.NewDefaultConfigFactory().Create( + config.WithFilePaths("./testdata/config"), + ) + if err != nil { + return nil, err + } + + return fxcore.NewFxModuleInfoRegistry(fxcore.FxModuleInfoRegistryParam{ + Infos: []interface{}{ + &testModuleInfo{}, + fxcore.NewFxCoreModuleInfo(cfg), + "invalid", + }, + }), nil +} diff --git a/fxcore/renderer.go b/fxcore/renderer.go new file mode 100644 index 0000000..6e5f858 --- /dev/null +++ b/fxcore/renderer.go @@ -0,0 +1,26 @@ +package fxcore + +import ( + "embed" + "html/template" + "io" + + "github.com/labstack/echo/v4" +) + +// DashboardRenderer is the core dashboard template renderer, based on [template.Template]. +type DashboardRenderer struct { + engine *template.Template +} + +// NewDashboardRenderer returns a new DashboardRenderer. +func NewDashboardRenderer(fs embed.FS, tpl string) *DashboardRenderer { + return &DashboardRenderer{ + engine: template.Must(template.ParseFS(fs, tpl)), + } +} + +// Render renders the core dashboard template. +func (r *DashboardRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return r.engine.ExecuteTemplate(w, name, data) +} diff --git a/fxcore/renderer_test.go b/fxcore/renderer_test.go new file mode 100644 index 0000000..42b42ef --- /dev/null +++ b/fxcore/renderer_test.go @@ -0,0 +1,32 @@ +package fxcore_test + +import ( + "embed" + "strings" + "testing" + + "github.com/ankorstore/yokai/fxcore" + "github.com/stretchr/testify/assert" +) + +//go:embed testdata/templates/* +var testTemplatesFS embed.FS + +func TestDashboardRenderer(t *testing.T) { + t.Parallel() + + var builder strings.Builder + + renderer := fxcore.NewDashboardRenderer(testTemplatesFS, "testdata/templates/dashboard.html") + + err := renderer.Render( + &builder, + "dashboard.html", + map[string]interface{}{ + "value": "some test value", + }, + nil, + ) + assert.NoError(t, err) + assert.Contains(t, "Result: some test value", builder.String()) +} diff --git a/fxcore/root.go b/fxcore/root.go new file mode 100644 index 0000000..759efd1 --- /dev/null +++ b/fxcore/root.go @@ -0,0 +1,15 @@ +package fxcore + +import ( + "path/filepath" + "runtime" +) + +// RootDir returns the root dir, for a provided number of stack frames to ascend. +// +//nolint:dogsled +func RootDir(skip int) string { + _, file, _, _ := runtime.Caller(skip) + + return filepath.Join(filepath.Dir(file), "..") +} diff --git a/fxcore/root_test.go b/fxcore/root_test.go new file mode 100644 index 0000000..a5fd19e --- /dev/null +++ b/fxcore/root_test.go @@ -0,0 +1,15 @@ +package fxcore_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxcore" + "github.com/stretchr/testify/assert" +) + +func TestRootDir(t *testing.T) { + t.Parallel() + + dir := fxcore.RootDir(99) + assert.Equal(t, "..", dir) +} diff --git a/fxcore/templates/dashboard.html b/fxcore/templates/dashboard.html new file mode 100644 index 0000000..794f0dc --- /dev/null +++ b/fxcore/templates/dashboard.html @@ -0,0 +1,305 @@ + + + {{ .info.AppName }} + + + + + + + + + + + + +
+ +
+
+
+ {{ if or .buildExpose .configExpose .metricsExpose .routesExpose .pprofExpose .statsExpose }} +
+
+   Core +
+
+ {{ if .buildExpose }} + + Build + + + {{ end }} + {{ if .configExpose }} + + Config + + + {{ end }} + {{ if .metricsExpose }} + + Metrics + + + {{ end }} + {{ if .routesExpose }} + + Routes + + + {{ end }} + {{ if .pprofExpose }} + + Pprof +    + + + {{ end }} + {{ if .statsExpose }} + + Stats +    + + {{ end }} +
+
+ {{ end }} + {{ if or .startupExpose .livenessExpose .readinessExpose }} +
+
+
+   Healthcheck +
+
+ {{ if .startupExpose }} + + Startup + + + {{ end }} + {{ if .livenessExpose }} + + Liveness + + + {{ end }} + {{ if .readinessExpose }} + + Readiness + + + {{ end }} +
+
+ {{ end }} + {{ if .modulesExpose }} +
+
+
+   Modules +
+
+ {{ range $moduleName := .modulesNames }} + + {{ $moduleName }} + + + {{ else }} + +   n/a + + {{ end }} +
+
+ {{ end }} +
+
+
+
+
+
+ Loading... +
+  Loading ... +
+
+
+
+
+

+   {{ .info.AppName }} +

+ + + + + + + + + + + + + + + +
Env{{ .info.AppEnv }}
Debug{{ .info.AppDebug }}
Version{{ .info.AppVersion }}
+
+
+
+
+
+

+   Logs +

+ + + + + + + + + + + +
Level{{ .info.LogLevel }}
Output{{ .info.LogOutput }}
+
+
+
+
+
+

+   Traces +

+ + + + + + + + + + + +
Sampler{{ .info.TraceSampler }}
Processor{{ .info.TraceProcessor }}
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/fxcore/testdata/config/config.test.yaml b/fxcore/testdata/config/config.test.yaml new file mode 100644 index 0000000..3326e08 --- /dev/null +++ b/fxcore/testdata/config/config.test.yaml @@ -0,0 +1,3 @@ +app: + env: test + debug: true diff --git a/fxcore/testdata/config/config.yaml b/fxcore/testdata/config/config.yaml new file mode 100644 index 0000000..7e3b961 --- /dev/null +++ b/fxcore/testdata/config/config.yaml @@ -0,0 +1,60 @@ +app: + name: core-app + env: dev + version: 0.1.0 + debug: false +modules: + log: + level: debug + output: test + trace: + processor: + type: test + core: + server: + errors: + obfuscate: false + stack: false + log: + headers: + x-foo: foo + x-bar: bar + exclude: + - /healthz + - /livez + - /readyz + level_from_response: true + trace: + enabled: true + exclude: + - /healthz + - /livez + - /readyz + metrics: + expose: ${METRICS_ENABLED} + collect: + enabled: ${METRICS_COLLECT} + namespace: foo + subsystem: bar + buckets: 0.1, 1, 10 + normalize: true + healthcheck: + startup: + expose: ${STARTUP_ENABLED} + readiness: + expose: ${READINESS_ENABLED} + liveness: + expose: ${LIVENESS_ENABLED} + debug: + config: + expose: ${CONFIG_ENABLED} + pprof: + expose: ${PPROF_ENABLED} + routes: + expose: ${ROUTES_ENABLED} + stats: + expose: ${STATS_ENABLED} + build: + expose: ${BUILD_ENABLED} + modules: + expose: ${MODULES_ENABLED} diff --git a/fxcore/testdata/probes/failure.go b/fxcore/testdata/probes/failure.go new file mode 100644 index 0000000..fe650ff --- /dev/null +++ b/fxcore/testdata/probes/failure.go @@ -0,0 +1,21 @@ +package probes + +import ( + "context" + + "github.com/ankorstore/yokai/healthcheck" +) + +type FailureProbe struct{} + +func NewFailureProbe() *FailureProbe { + return &FailureProbe{} +} + +func (p *FailureProbe) Name() string { + return "failureProbe" +} + +func (p *FailureProbe) Check(ctx context.Context) *healthcheck.CheckerProbeResult { + return healthcheck.NewCheckerProbeResult(false, "failure") +} diff --git a/fxcore/testdata/probes/success.go b/fxcore/testdata/probes/success.go new file mode 100644 index 0000000..1e95bd7 --- /dev/null +++ b/fxcore/testdata/probes/success.go @@ -0,0 +1,21 @@ +package probes + +import ( + "context" + + "github.com/ankorstore/yokai/healthcheck" +) + +type SuccessProbe struct{} + +func NewSuccessProbe() *SuccessProbe { + return &SuccessProbe{} +} + +func (p *SuccessProbe) Name() string { + return "successProbe" +} + +func (p *SuccessProbe) Check(ctx context.Context) *healthcheck.CheckerProbeResult { + return healthcheck.NewCheckerProbeResult(true, "success") +} diff --git a/fxcore/testdata/probes/test.go b/fxcore/testdata/probes/test.go new file mode 100644 index 0000000..25d9c1c --- /dev/null +++ b/fxcore/testdata/probes/test.go @@ -0,0 +1,21 @@ +package probes + +import ( + "context" + + "github.com/ankorstore/yokai/healthcheck" +) + +type TestProbe struct{} + +func NewTestProbe() *TestProbe { + return &TestProbe{} +} + +func (p *TestProbe) Name() string { + return "testProbe" +} + +func (p *TestProbe) Check(ctx context.Context) *healthcheck.CheckerProbeResult { + return healthcheck.NewCheckerProbeResult(false, "test") +} diff --git a/fxcore/testdata/templates/dashboard.html b/fxcore/testdata/templates/dashboard.html new file mode 100644 index 0000000..5bf8400 --- /dev/null +++ b/fxcore/testdata/templates/dashboard.html @@ -0,0 +1 @@ +Result: {{index . "value"}} \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json index fee0356..0b53235 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -41,6 +41,11 @@ "component": "trace", "tag-separator": "/" }, + "fxcore": { + "release-type": "go", + "component": "fxcore", + "tag-separator": "/" + }, "fxconfig": { "release-type": "go", "component": "fxconfig",