diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index dd2457a1..c886ef6c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,6 +32,7 @@ jobs: - "worker" - "fxcore" - "fxconfig" + - "fxcron" - "fxgenerate" - "fxhealthcheck" - "fxhttpclient" diff --git a/.github/workflows/fxcron-ci.yml b/.github/workflows/fxcron-ci.yml new file mode 100644 index 00000000..947938f6 --- /dev/null +++ b/.github/workflows/fxcron-ci.yml @@ -0,0 +1,31 @@ +name: "fxcron-ci" + +on: + push: + branches: + - "feat**" + - "fix**" + - "hotfix**" + - "chore**" + paths: + - "fxcron/**.go" + - "fxcron/go.mod" + - "fxcron/go.sum" + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main + paths: + - "fxcron/**.go" + - "fxcron/go.mod" + - "fxcron/go.sum" + +jobs: + ci: + uses: ./.github/workflows/common-ci.yml + secrets: inherit + with: + module: "fxcron" diff --git a/fxcron/.golangci.yml b/fxcron/.golangci.yml new file mode 100644 index 00000000..45a6e625 --- /dev/null +++ b/fxcron/.golangci.yml @@ -0,0 +1,63 @@ +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 + - goconst + - 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/fxcron/README.md b/fxcron/README.md new file mode 100644 index 00000000..66796bf3 --- /dev/null +++ b/fxcron/README.md @@ -0,0 +1,308 @@ +# Fx Cron Module + +[![ci](https://github.com/ankorstore/yokai/actions/workflows/fxcron-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/fxcron-ci.yml) +[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/fxcron)](https://goreportcard.com/report/github.com/ankorstore/yokai/fxcron) +[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=ghUBlFsjhR&flag=fxcron)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/fxcron) +[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai%2Ffxcron) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/fxcron)](https://pkg.go.dev/github.com/ankorstore/yokai/fxcron) + +> [Fx](https://uber-go.github.io/fx/) module for [gocron](https://github.com/go-co-op/gocron). + + +* [Installation](#installation) +* [Features](#features) +* [Documentation](#documentation) + * [Dependencies](#dependencies) + * [Loading](#loading) + * [Configuration](#configuration) + * [Cron jobs](#cron-jobs) + * [Definition](#definition) + * [Registration](#registration) + * [Override](#override) + + +## Installation + +```shell +go get github.com/ankorstore/yokai/fxcron +``` + +## Features + +This module provides the possibility to run **internal** cron jobs in your application with: + +- automatic panic recovery +- configurable cron jobs scheduling and execution options +- configurable logging, tracing and metrics for cron jobs executions + +## Documentation + +### Dependencies + +This module is intended to be used alongside: + +- 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 + +### Loading + +To load the module in your Fx application: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/go-co-op/gocron/v2" + "go.uber.org/fx" +) + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxcron.FxCronModule, // load the module + fx.Invoke(func(scheduler gocron.Scheduler) { + scheduler.Start() // start the cron jobs scheduler + }), + ).Run() +} +``` + +### 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 + cron: + scheduler: + seconds: true # to allow seconds based cron jobs expressions (impact all jobs), disabled by default + concurrency: + limit: + enabled: true # to limit concurrent cron jobs executions, disabled by default + max: 3 # concurrency limit + mode: wait # "wait" or "reschedule" + stop: + timeout: 5s # scheduler shutdown timeout for graceful cron jobs termination, 10 seconds by default + jobs: # common cron jobs options + execution: + start: + immediately: true # to start cron jobs executions immediately (by default) + at: "2023-01-01T14:00:00Z" # or a given date time (RFC3339) + limit: + enabled: true # to limit the number of per cron jobs executions, disabled by default + max: 3 # executions limit + singleton: + enabled: true # to execute the cron jobs in singleton mode, disabled by default + mode: wait # "wait" or "reschedule" + log: + enabled: true # to log cron jobs executions, disabled by default (errors will always be logged). + exclude: # to exclude by name cron jobs from logging + - foo + - bar + metrics: + collect: + enabled: true # to collect cron jobs executions metrics (executions count and duration), disabled by default + namespace: app # cron jobs metrics namespace (default app.name value) + subsystem: cron # cron jobs metrics subsystem (default cron) + buckets: 1, 1.5, 10, 15, 100 # to define custom cron jobs executions durations metric buckets (in seconds) + trace: + enabled: true # to trace cron jobs executions, disabled by default + exclude: # to exclude by name cron jobs from tracing + - foo + - bar +``` + +Notes: + +- the cron jobs executions logging will be based on the [fxlog](https://github.com/ankorstore/yokai/tree/main/fxlog) + module configuration +- the cron jobs executions tracing will be based on the [fxtrace](https://github.com/ankorstore/yokai/tree/main/fxtrace) + module configuration + +Check the [configuration files documentation](https://github.com/ankorstore/yokai/tree/main/config#configuration-files) +for more details. + +### Cron jobs + +#### Definition + +This module provides a simple [CronJob](registry.go) interface to implement for your cron jobs: + +```go +package cron + +import ( + "context" + + "github.com/ankorstore/yokai/fxcron" + "path/to/service" +) + +type SomeCron struct { + service *service.SomeService +} + +func NewSomeCron(service *service.SomeService) *SomeCron { + return &SomeCron{ + service: service, + } +} + +func (c *SomeCron) Name() string { + return "some cron job" +} + +func (c *SomeCron) Run(ctx context.Context) error { + // contextual job name and execution id + name, id := fxcron.CtxCronJobName(ctx), fxcron.CtxCronJobExecutionId(ctx) + + // contextual tracing + ctx, span := fxcron.CtxTracer(ctx).Start(ctx, "some span") + defer span.End() + + // contextual logging + fxcron.CtxLogger(ctx).Info().Msg("some log") + + // invoke autowired dependency + err := c.service.DoSomething(ctx, name, id) + + // returned errors will automatically be logged + return err +} +``` + +Notes: + +- your cron job dependencies will be autowired +- you can access from the provided context: + - the cron job name with `CtxCronJobName()` + - the cron job execution id with `CtxCronJobExecutionId()` + - the tracer with `CtxTracer()`, which will automatically add to your spans the `CronJob` name + and `CronJobExecutionID` attributes + - the logger with `CtxLogger()`, which will automatically add to your log records the `cronJob` name + and `cronJobExecutionID` fields + +#### Registration + +Once ready, you can register and schedule your cron job with `AsCronJob()`: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/go-co-op/gocron/v2" + "go.uber.org/fx" + "path/to/cron" +) + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxcron.FxCronModule, // load the module + fx.Options( + // register, autowire and schedule SomeCron to run every 2 minutes + fxcron.AsCronJob(cron.NewSomeCron, `*/2 * * * *`), + ), + ).Run() +} +``` + +You can override, per job, the common job execution options by providing your own list +of [gocron.JobOption](https://github.com/go-co-op/gocron/blob/v2/job.go), for example: + +```go +fxcron.AsCronJob(cron.NewSomeCron, `*/2 * * * *`, gocron.WithLimitedRuns(10)), +``` + +If you need cron jobs to be scheduled on the seconds level, configure the scheduler +with `modules.cron.scheduler.seconds=true`. + +It will add `seconds` field to the beginning of the scheduling expression, for example to run every 5 seconds: + +```go +fxcron.AsCronJob(cron.NewSomeCron, `*/5 * * * * *`), +``` + +Note: you can use [https://crontab.guru](https://crontab.guru) to help you with your scheduling definitions. + +### Override + +By default, the `gocron.Scheduler` is created by the [DefaultCronSchedulerFactory](factory.go). + +If needed, you can provide your own factory and override the module: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/go-co-op/gocron/v2" + "go.uber.org/fx" +) + +type CustomCronSchedulerFactory struct{} + +func NewCustomCronSchedulerFactory() fxcron.CronSchedulerFactory { + return &CustomCronSchedulerFactory{} +} + +func (f *CustomCronSchedulerFactory) Create(options ...gocron.SchedulerOption) (gocron.Scheduler, error) { + return gocron.NewScheduler(options...) +} + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxcron.FxCronModule, // load the module + fx.Decorate(NewCustomCronSchedulerFactory), // override the module with a custom factory + fx.Invoke(func(scheduler gocron.Scheduler) { // invoke the cron scheduler + // ... + }), + ).Run() +} +``` diff --git a/fxcron/context.go b/fxcron/context.go new file mode 100644 index 00000000..34f644cc --- /dev/null +++ b/fxcron/context.go @@ -0,0 +1,43 @@ +package fxcron + +import ( + "context" + + "github.com/ankorstore/yokai/log" + "github.com/ankorstore/yokai/trace" + oteltrace "go.opentelemetry.io/otel/trace" +) + +// CtxCronJobNameKey is a contextual struct key. +type CtxCronJobNameKey struct{} + +// CtxCronJobExecutionIdKey is a contextual struct key. +type CtxCronJobExecutionIdKey struct{} + +// CtxCronJobName returns the contextual cron job name. +func CtxCronJobName(ctx context.Context) string { + if name, ok := ctx.Value(CtxCronJobNameKey{}).(string); ok { + return name + } else { + return "" + } +} + +// CtxCronJobExecutionId returns the contextual cron job execution id. +func CtxCronJobExecutionId(ctx context.Context) string { + if id, ok := ctx.Value(CtxCronJobExecutionIdKey{}).(string); ok { + return id + } else { + return "" + } +} + +// CtxLogger returns the contextual logger. +func CtxLogger(ctx context.Context) *log.Logger { + return log.CtxLogger(ctx) +} + +// CtxTracer returns the contextual tracer. +func CtxTracer(ctx context.Context) oteltrace.Tracer { + return trace.CtxTracerProvider(ctx).Tracer(ModuleName) +} diff --git a/fxcron/context_test.go b/fxcron/context_test.go new file mode 100644 index 00000000..662c9d5e --- /dev/null +++ b/fxcron/context_test.go @@ -0,0 +1,82 @@ +package fxcron_test + +import ( + "context" + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/log" + "github.com/ankorstore/yokai/trace" + "github.com/ankorstore/yokai/trace/tracetest" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + oteltrace "go.opentelemetry.io/otel/trace" +) + +const testName = "some test name" +const testExecutionId = "some test execution id" + +func TestCtxCronJobName(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + assert.Equal(t, "", fxcron.CtxCronJobName(ctx)) + + ctx = context.WithValue(context.Background(), fxcron.CtxCronJobNameKey{}, testName) + + assert.Equal(t, testName, fxcron.CtxCronJobName(ctx)) +} + +func TestCtxCronJobExecutionId(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + assert.Equal(t, "", fxcron.CtxCronJobExecutionId(ctx)) + + ctx = context.WithValue(ctx, fxcron.CtxCronJobExecutionIdKey{}, testExecutionId) + + assert.Equal(t, testExecutionId, fxcron.CtxCronJobExecutionId(ctx)) +} + +func TestCtxLogger(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + logger := log.CtxLogger(ctx) + + assert.Equal(t, logger, fxcron.CtxLogger(ctx)) +} + +func TestCtxTracer(t *testing.T) { + t.Parallel() + + exporter := tracetest.NewDefaultTestTraceExporter() + tracerProvider, err := trace.NewDefaultTracerProviderFactory().Create( + trace.WithSpanProcessor(trace.NewTestSpanProcessor(exporter)), + trace.WithSpanProcessor(fxcron.NewTracerProviderCronJobAnnotator()), + ) + assert.NoError(t, err) + + ctx := context.WithValue(context.Background(), fxcron.CtxCronJobNameKey{}, testName) + ctx = context.WithValue(ctx, fxcron.CtxCronJobExecutionIdKey{}, testExecutionId) + ctx = context.WithValue(ctx, trace.CtxKey{}, tracerProvider) + + _, span := fxcron.CtxTracer(ctx).Start( + ctx, + "some span", + oteltrace.WithAttributes(attribute.String("some attribute", "some value")), + ) + span.End() + + tracetest.AssertHasTraceSpan( + t, + exporter, + "some span", + attribute.String("CronJob", testName), + attribute.String("CronJobExecutionID", testExecutionId), + attribute.String("some attribute", "some value"), + ) +} diff --git a/fxcron/define.go b/fxcron/define.go new file mode 100644 index 00000000..2ed17f29 --- /dev/null +++ b/fxcron/define.go @@ -0,0 +1,42 @@ +package fxcron + +import ( + "github.com/go-co-op/gocron/v2" +) + +// CronJobDefinition is the interface for cron job definitions. +type CronJobDefinition interface { + ReturnType() string + Expression() string + Options() []gocron.JobOption +} + +type cronJobDefinition struct { + returnType string + expression string + options []gocron.JobOption +} + +// NewCronJobDefinition returns a new [CronJobDefinition]. +func NewCronJobDefinition(returnType string, expression string, options ...gocron.JobOption) CronJobDefinition { + return &cronJobDefinition{ + returnType: returnType, + expression: expression, + options: options, + } +} + +// ReturnType returns the definition return type. +func (c *cronJobDefinition) ReturnType() string { + return c.returnType +} + +// Expression returns the definition cron expression. +func (c *cronJobDefinition) Expression() string { + return c.expression +} + +// Options returns the definition cron job options. +func (c *cronJobDefinition) Options() []gocron.JobOption { + return c.options +} diff --git a/fxcron/define_test.go b/fxcron/define_test.go new file mode 100644 index 00000000..becbb28c --- /dev/null +++ b/fxcron/define_test.go @@ -0,0 +1,20 @@ +package fxcron_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" +) + +func TestNewCronJobDefinition(t *testing.T) { + t.Parallel() + + definition := fxcron.NewCronJobDefinition("*TestCron", `* * * * *`) + + assert.Implements(t, (*fxcron.CronJobDefinition)(nil), definition) + assert.Equal(t, "*TestCron", definition.ReturnType()) + assert.Equal(t, `* * * * *`, definition.Expression()) + assert.Equal(t, []gocron.JobOption(nil), definition.Options()) +} diff --git a/fxcron/factory.go b/fxcron/factory.go new file mode 100644 index 00000000..edc023dc --- /dev/null +++ b/fxcron/factory.go @@ -0,0 +1,23 @@ +package fxcron + +import ( + "github.com/go-co-op/gocron/v2" +) + +// CronSchedulerFactory is the interface for [gocron.Scheduler] factories. +type CronSchedulerFactory interface { + Create(options ...gocron.SchedulerOption) (gocron.Scheduler, error) +} + +// DefaultCronSchedulerFactory is the default [CronSchedulerFactory] implementation. +type DefaultCronSchedulerFactory struct{} + +// NewDefaultCronSchedulerFactory returns a [DefaultCronSchedulerFactory], implementing [CronSchedulerFactory]. +func NewDefaultCronSchedulerFactory() CronSchedulerFactory { + return &DefaultCronSchedulerFactory{} +} + +// Create returns a new [gocron.Scheduler] instance for an optional list of [gocron.SchedulerOption]. +func (f *DefaultCronSchedulerFactory) Create(options ...gocron.SchedulerOption) (gocron.Scheduler, error) { + return gocron.NewScheduler(options...) +} diff --git a/fxcron/factory_test.go b/fxcron/factory_test.go new file mode 100644 index 00000000..21231fee --- /dev/null +++ b/fxcron/factory_test.go @@ -0,0 +1,29 @@ +package fxcron_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" +) + +func TestDefaultCronSchedulerFactory(t *testing.T) { + t.Parallel() + + factory := fxcron.NewDefaultCronSchedulerFactory() + + assert.IsType(t, &fxcron.DefaultCronSchedulerFactory{}, factory) + assert.Implements(t, (*fxcron.CronSchedulerFactory)(nil), factory) +} + +func TestCreate(t *testing.T) { + t.Parallel() + + factory := fxcron.NewDefaultCronSchedulerFactory() + + scheduler, err := factory.Create() + assert.NoError(t, err) + + assert.Implements(t, (*gocron.Scheduler)(nil), scheduler) +} diff --git a/fxcron/go.mod b/fxcron/go.mod new file mode 100644 index 00000000..3d970e47 --- /dev/null +++ b/fxcron/go.mod @@ -0,0 +1,78 @@ +module github.com/ankorstore/yokai/fxcron + +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/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/log v1.0.0 + github.com/ankorstore/yokai/trace v1.0.0 + github.com/go-co-op/gocron/v2 v2.2.4 + github.com/prometheus/client_golang v1.18.0 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.17.0 + go.opentelemetry.io/otel/sdk v1.16.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-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jonboulle/clockwork 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/robfig/cron/v3 v3.0.1 // indirect + github.com/rs/zerolog v1.31.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/stretchr/objx v0.5.0 // indirect + github.com/subosito/gotenv v1.6.0 // 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/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/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 + 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/fxcron/go.sum b/fxcron/go.sum new file mode 100644 index 00000000..44c1a583 --- /dev/null +++ b/fxcron/go.sum @@ -0,0 +1,560 @@ +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/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/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/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-co-op/gocron/v2 v2.2.4 h1:fL6a8/U+BJQ9UbaeqKxua8wY02w4ftKZsxPzLSNOCKk= +github.com/go-co-op/gocron/v2 v2.2.4/go.mod h1:igssOwzZkfcnu3m2kwnCf/mYj4SmhP9ecSgmYjCOHkk= +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/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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.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/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/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +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/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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +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/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.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +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/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-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/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/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.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/fxcron/info.go b/fxcron/info.go new file mode 100644 index 00000000..d856a60e --- /dev/null +++ b/fxcron/info.go @@ -0,0 +1,98 @@ +package fxcron + +import ( + "reflect" + "time" + + "github.com/go-co-op/gocron/v2" +) + +const NON_AVAILABLE = "n/a" + +// FxCronModuleInfo is a module info collector for fxcore. +type FxCronModuleInfo struct { + scheduler gocron.Scheduler + registry *CronJobRegistry +} + +// NewFxCronModuleInfo returns a new [FxCronModuleInfo]. +func NewFxCronModuleInfo(scheduler gocron.Scheduler, registry *CronJobRegistry) *FxCronModuleInfo { + return &FxCronModuleInfo{ + scheduler: scheduler, + registry: registry, + } +} + +// Name return the name of the module info. +func (i *FxCronModuleInfo) Name() string { + return ModuleName +} + +// Data return the data of the module info. +func (i *FxCronModuleInfo) Data() map[string]interface{} { + scheduledJobs := i.scheduler.Jobs() + + resolvedJobs, err := i.registry.ResolveCronJobs() + if err != nil { + return map[string]interface{}{ + "jobs": map[string]interface{}{ + "scheduled": NON_AVAILABLE, + "unscheduled": NON_AVAILABLE, + }, + } + } + + scheduledJobsData := make(map[string]interface{}) + unscheduledJobsData := make(map[string]interface{}) + + for _, resolvedJob := range resolvedJobs { + isJobScheduled := false + + for _, scheduledJob := range scheduledJobs { + if resolvedJob.Implementation().Name() == scheduledJob.Name() { + isJobScheduled = true + + scheduledJobsData[resolvedJob.Implementation().Name()] = map[string]interface{}{ + "expression": resolvedJob.Expression(), + "last_run": i.jobLastRun(scheduledJob), + "next_run": i.jobNextRun(scheduledJob), + "type": i.jobType(resolvedJob.Implementation()), + } + } + } + + if !isJobScheduled { + unscheduledJobsData[resolvedJob.Implementation().Name()] = map[string]interface{}{ + "expression": resolvedJob.Expression(), + "type": i.jobType(resolvedJob.Implementation()), + } + } + } + + return map[string]interface{}{ + "jobs": map[string]interface{}{ + "scheduled": scheduledJobsData, + "unscheduled": unscheduledJobsData, + }, + } +} + +func (i *FxCronModuleInfo) jobLastRun(job gocron.Job) string { + if run, err := job.LastRun(); err == nil { + return run.Format(time.RFC3339) + } + + return NON_AVAILABLE +} + +func (i *FxCronModuleInfo) jobNextRun(job gocron.Job) string { + if run, err := job.NextRun(); err == nil { + return run.Format(time.RFC3339) + } + + return NON_AVAILABLE +} + +func (i *FxCronModuleInfo) jobType(job CronJob) string { + return reflect.ValueOf(job).Type().String() +} diff --git a/fxcron/info_test.go b/fxcron/info_test.go new file mode 100644 index 00000000..408c2ba9 --- /dev/null +++ b/fxcron/info_test.go @@ -0,0 +1,94 @@ +package fxcron_test + +import ( + "context" + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/job" + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" +) + +func TestFxCronModuleInfo(t *testing.T) { + t.Parallel() + + cronJob := job.NewDummyCron() + cronJobExpression := `*/1 * * * * *` + cronJobOptions := []gocron.JobOption(nil) + + scheduler, err := gocron.NewScheduler() + assert.NoError(t, err) + + _, err = scheduler.NewJob( + gocron.CronJob(cronJobExpression, true), + gocron.NewTask( + func() { + err := cronJob.Run(context.Background()) + assert.NoError(t, err) + }, + ), + ) + assert.NoError(t, err) + + param := fxcron.FxCronJobRegistryParam{ + CronJobs: []fxcron.CronJob{cronJob}, + CronJobsDefinitions: []fxcron.CronJobDefinition{ + fxcron.NewCronJobDefinition(fxcron.GetType(cronJob), cronJobExpression, cronJobOptions...), + }, + } + + registry := fxcron.NewFxCronJobRegistry(param) + + info := fxcron.NewFxCronModuleInfo(scheduler, registry) + + assert.IsType(t, &fxcron.FxCronModuleInfo{}, info) + assert.Equal(t, fxcron.ModuleName, info.Name()) + + assert.Equal( + t, + map[string]interface{}{ + "jobs": map[string]interface{}{ + "scheduled": map[string]interface{}{}, + "unscheduled": map[string]interface{}{ + "dummy": map[string]interface{}{ + "expression": cronJobExpression, + "type": fxcron.GetType(cronJob), + }, + }, + }, + }, + info.Data(), + ) +} + +func TestFxCronModuleInfoError(t *testing.T) { + t.Parallel() + + cronJobExpression := `*/1 * * * * *` + + scheduler, err := gocron.NewScheduler() + assert.NoError(t, err) + + param := fxcron.FxCronJobRegistryParam{ + CronJobs: []fxcron.CronJob{}, + CronJobsDefinitions: []fxcron.CronJobDefinition{ + fxcron.NewCronJobDefinition("invalid", cronJobExpression), + }, + } + + registry := fxcron.NewFxCronJobRegistry(param) + + info := fxcron.NewFxCronModuleInfo(scheduler, registry) + + assert.Equal( + t, + map[string]interface{}{ + "jobs": map[string]interface{}{ + "scheduled": fxcron.NON_AVAILABLE, + "unscheduled": fxcron.NON_AVAILABLE, + }, + }, + info.Data(), + ) +} diff --git a/fxcron/metrics.go b/fxcron/metrics.go new file mode 100644 index 00000000..2736fc81 --- /dev/null +++ b/fxcron/metrics.go @@ -0,0 +1,140 @@ +package fxcron + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +const ( + EXECUTION_SUCCESS = "success" + EXECUTION_ERROR = "error" +) + +var defaultBuckets = []float64{ + // 1ms + 0.001, + 0.002, + 0.005, + // 10ms + 0.01, + 0.02, + 0.05, + // 100ms + 0.1, + 0.2, + 0.5, + // 1s + 1.0, + 2.0, + 5.0, + // 10s + 10.0, + 20.0, + 50.0, + // 100s + 100.0, + 200.0, + 500.0, + // 1000s + 1000.0, + 2000.0, + 5000.0, +} + +// CronJobMetrics is the metrics handler for the cron jobs. +type CronJobMetrics struct { + registered bool + namespace string + subsystem string + histogram *prometheus.HistogramVec + counter *prometheus.CounterVec +} + +// NewCronJobMetrics returns a new [CronJobMetrics] instance for provided metrics namespace and subsystem. +func NewCronJobMetrics(namespace string, subsystem string) *CronJobMetrics { + return create(namespace, subsystem, defaultBuckets) +} + +// NewCronJobMetricsWithBuckets returns a new [CronJobMetrics] instance for provided metrics namespace, subsystem and buckets. +func NewCronJobMetricsWithBuckets(namespace string, subsystem string, buckets []float64) *CronJobMetrics { + return create(namespace, subsystem, buckets) +} + +// Register allows the [CronJobMetrics] to register against a provided [prometheus.Registry]. +func (m *CronJobMetrics) Register(registry *prometheus.Registry) error { + err := registry.Register(m.histogram) + if err != nil { + return err + } + + err = registry.Register(m.counter) + if err != nil { + return err + } + + m.registered = err == nil + + return err +} + +// ObserveCronJobExecutionDuration observes the duration of a cron job execution. +func (m *CronJobMetrics) ObserveCronJobExecutionDuration(jobName string, jobDuration float64) *CronJobMetrics { + if m.registered { + m.histogram.WithLabelValues(Sanitize(jobName)).Observe(jobDuration) + } + + return m +} + +// IncrementCronJobExecutionSuccess increments the number of execution successes for a given cron job. +func (m *CronJobMetrics) IncrementCronJobExecutionSuccess(jobName string) *CronJobMetrics { + if m.registered { + m.counter.WithLabelValues(Sanitize(jobName), EXECUTION_SUCCESS).Inc() + } + + return m +} + +// IncrementCronJobExecutionError increments the number of execution errors for a given cron job. +func (m *CronJobMetrics) IncrementCronJobExecutionError(jobName string) *CronJobMetrics { + if m.registered { + m.counter.WithLabelValues(Sanitize(jobName), EXECUTION_ERROR).Inc() + } + + return m +} + +func create(namespace string, subsystem string, buckets []float64) *CronJobMetrics { + histogram := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: Sanitize(namespace), + Subsystem: Sanitize(subsystem), + Name: "job_execution_duration_seconds", + Help: "Duration of cron job executions in seconds", + Buckets: buckets, + }, + []string{ + "job", + }, + ) + + counter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: Sanitize(namespace), + Subsystem: Sanitize(subsystem), + Name: "job_execution_total", + Help: "Total number of cron job executions", + }, + []string{ + "job", + "status", + }, + ) + + return &CronJobMetrics{ + registered: false, + namespace: namespace, + subsystem: subsystem, + histogram: histogram, + counter: counter, + } +} diff --git a/fxcron/metrics_test.go b/fxcron/metrics_test.go new file mode 100644 index 00000000..0943f8c0 --- /dev/null +++ b/fxcron/metrics_test.go @@ -0,0 +1,79 @@ +package fxcron_test + +import ( + "strings" + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" +) + +func TestCronJobMetrics(t *testing.T) { + t.Parallel() + + registry := prometheus.NewPedanticRegistry() + + metrics := fxcron.NewCronJobMetrics("foo", "bar") + + err := metrics.Register(registry) + assert.NoError(t, err) + + // execution duration + expected := ` + # HELP foo_bar_job_execution_duration_seconds Duration of cron job executions in seconds + # TYPE foo_bar_job_execution_duration_seconds histogram + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.001"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.002"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.005"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.01"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.02"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.05"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.1"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.2"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="0.5"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="1"} 0 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="2"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="5"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="10"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="20"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="50"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="100"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="200"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="500"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="1000"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="2000"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="5000"} 1 + foo_bar_job_execution_duration_seconds_bucket{job="foo",le="+Inf"} 1 + foo_bar_job_execution_duration_seconds_sum{job="foo"} 1.1 + foo_bar_job_execution_duration_seconds_count{job="foo"} 1 + ` + + metrics.ObserveCronJobExecutionDuration("foo", 1.1) + + err = testutil.GatherAndCompare( + registry, + strings.NewReader(expected), + "foo_bar_job_execution_duration_seconds", + ) + assert.NoError(t, err) + + // execution counter + expected = ` + # HELP foo_bar_job_execution_total Total number of cron job executions + # TYPE foo_bar_job_execution_total counter + foo_bar_job_execution_total{job="foo",status="success"} 1 + foo_bar_job_execution_total{job="foo",status="error"} 1 + ` + + metrics.IncrementCronJobExecutionSuccess("foo") + metrics.IncrementCronJobExecutionError("foo") + + err = testutil.GatherAndCompare( + registry, + strings.NewReader(expected), + "foo_bar_job_execution_total", + ) + assert.NoError(t, err) +} diff --git a/fxcron/module.go b/fxcron/module.go new file mode 100644 index 00000000..1c048aee --- /dev/null +++ b/fxcron/module.go @@ -0,0 +1,311 @@ +package fxcron + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/generate/uuid" + "github.com/ankorstore/yokai/log" + "github.com/ankorstore/yokai/trace" + "github.com/go-co-op/gocron/v2" + "github.com/prometheus/client_golang/prometheus" + oteltrace "go.opentelemetry.io/otel/trace" + "go.uber.org/fx" +) + +const ( + ModuleName = "cron" + LogRecordFieldCronJobName = "cronJob" + LogRecordFieldCronJobExecutionId = "cronJobExecutionID" + TraceSpanAttributeCronJobName = "CronJob" + TraceSpanAttributeCronJobExecutionId = "CronJobExecutionID" +) + +// FxCronModule is the [Fx] cron module. +// +// [Fx]: https://github.com/uber-go/fx +var FxCronModule = fx.Module( + ModuleName, + fx.Provide( + NewDefaultCronSchedulerFactory, + NewFxCronJobRegistry, + NewFxCron, + fx.Annotate( + NewFxCronModuleInfo, + fx.As(new(interface{})), + fx.ResultTags(`group:"core-module-infos"`), + ), + ), +) + +// FxCronParam allows injection of the required dependencies in [NewFxCron]. +type FxCronParam struct { + fx.In + LifeCycle fx.Lifecycle + Generator uuid.UuidGenerator + TracerProvider oteltrace.TracerProvider + Factory CronSchedulerFactory + Config *config.Config + Registry *CronJobRegistry + Logger *log.Logger + MetricsRegistry *prometheus.Registry +} + +// NewFxCron returns a new [gocron.Scheduler]. +// +//nolint:cyclop,gocognit +func NewFxCron(p FxCronParam) (gocron.Scheduler, error) { + appDebug := p.Config.AppDebug() + + // logger + cronLogger := log.FromZerolog(p.Logger.ToZerolog().With().Str("system", ModuleName).Logger()) + + // tracer provider + tracerProvider := AnnotateTracerProvider(p.TracerProvider) + + // scheduler + cronSchedulerOptions, err := buildSchedulerOptions(p.Config) + if err != nil { + p.Logger.Error().Err(err).Msg("cron scheduler options creation error") + + return nil, err + } + + cronScheduler, err := p.Factory.Create(cronSchedulerOptions...) + if err != nil { + p.Logger.Error().Err(err).Msg("cron scheduler creation error") + + return nil, err + } + + // jobs logs + cronJobLogExecution := p.Config.GetBool("modules.cron.log.enabled") || appDebug + cronJobLogExclusions := p.Config.GetStringSlice("modules.cron.log.exclude") + + // jobs traces + cronJobTraceExecution := p.Config.GetBool("modules.cron.trace.enabled") + cronJobTraceExclusions := p.Config.GetStringSlice("modules.cron.trace.exclude") + + // jobs metrics + cronJobMetricsNamespace := p.Config.GetString("modules.cron.metrics.collect.namespace") + if cronJobMetricsNamespace == "" { + cronJobMetricsNamespace = p.Config.AppName() + } + + cronJobMetricsSubsystem := p.Config.GetString("modules.cron.metrics.collect.subsystem") + if cronJobMetricsSubsystem == "" { + cronJobMetricsSubsystem = ModuleName + } + + var cronJobMetrics *CronJobMetrics + if cronJobMetricsBuckets := p.Config.GetString("modules.cron.metrics.buckets"); cronJobMetricsBuckets != "" { + var buckets []float64 + + for _, s := range strings.Split(strings.ReplaceAll(cronJobMetricsBuckets, " ", ""), ",") { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + buckets = append(buckets, f) + } + } + + cronJobMetrics = NewCronJobMetricsWithBuckets(cronJobMetricsNamespace, cronJobMetricsSubsystem, buckets) + } else { + cronJobMetrics = NewCronJobMetrics(cronJobMetricsNamespace, cronJobMetricsSubsystem) + } + + if p.Config.GetBool("modules.cron.metrics.collect.enabled") { + err = cronJobMetrics.Register(p.MetricsRegistry) + if err != nil { + p.Logger.Error().Err(err).Msg("cron scheduler metrics registration error") + + return nil, err + } + } + + // jobs registration + cronJobs, err := p.Registry.ResolveCronJobs() + if err != nil { + p.Logger.Error().Err(err).Msg("cron jobs resolution error") + + return nil, err + } + + for _, cronJob := range cronJobs { + // var scoping + currentCronJob := cronJob + + currentCronJobName := currentCronJob.Implementation().Name() + currentJobOptions := append(currentCronJob.Options(), gocron.WithName(currentCronJobName)) + currentCronJobLogExecution := !Contains(cronJobLogExclusions, currentCronJobName) + currentCronJobTraceExecution := !Contains(cronJobTraceExclusions, currentCronJobName) + + _, err = cronScheduler.NewJob( + gocron.CronJob( + currentCronJob.Expression(), + p.Config.GetBool("modules.cron.scheduler.seconds"), + ), + gocron.NewTask( + func() { + currentCronJobExecutionId := p.Generator.Generate() + + currentCronJobCtx := context.WithValue(context.Background(), CtxCronJobNameKey{}, currentCronJobName) + currentCronJobCtx = context.WithValue(currentCronJobCtx, CtxCronJobExecutionIdKey{}, currentCronJobExecutionId) + currentCronJobCtx = context.WithValue(currentCronJobCtx, trace.CtxKey{}, tracerProvider) + + var currentCronJobExecutionTraceSpan oteltrace.Span + if cronJobTraceExecution && currentCronJobTraceExecution { + currentCronJobCtx, currentCronJobExecutionTraceSpan = tracerProvider. + Tracer(ModuleName). + Start(currentCronJobCtx, fmt.Sprintf("%s %s", ModuleName, currentCronJobName)) + } + + currentCronJobLogger := log.FromZerolog( + cronLogger. + ToZerolog(). + With(). + Str(LogRecordFieldCronJobName, currentCronJobName). + Str(LogRecordFieldCronJobExecutionId, currentCronJobExecutionId). + Logger(), + ) + + currentCronJobCtx = currentCronJobLogger.WithContext(currentCronJobCtx) + + defer func(s oteltrace.Span, t time.Time) { + if cronJobTraceExecution && currentCronJobTraceExecution && s != nil { + s.End() + } + + cronJobMetrics.ObserveCronJobExecutionDuration(currentCronJobName, time.Since(t).Seconds()) + + if r := recover(); r != nil { + cronJobMetrics.IncrementCronJobExecutionError(currentCronJobName) + currentCronJobLogger.Error().Str("panic", fmt.Sprintf("%v", r)).Msg("job execution panic") + } + }(currentCronJobExecutionTraceSpan, time.Now()) + + if cronJobLogExecution && currentCronJobLogExecution { + currentCronJobLogger.Info().Msg("job execution start") + } + + runErr := currentCronJob.Implementation().Run(currentCronJobCtx) + + if runErr != nil { + cronJobMetrics.IncrementCronJobExecutionError(currentCronJobName) + currentCronJobLogger.Error().Err(runErr).Msg("job execution error") + } else { + cronJobMetrics.IncrementCronJobExecutionSuccess(currentCronJobName) + + if cronJobLogExecution && currentCronJobLogExecution { + currentCronJobLogger.Info().Msg("job execution success") + } + } + }, + ), + currentJobOptions..., + ) + + if err != nil { + cronLogger.Error().Err(err).Msgf("job registration error for job %s with %s", currentCronJobName, currentCronJob.Expression()) + + return nil, err + } else { + cronLogger.Debug().Msgf("job registration success for job %s with %s", currentCronJobName, currentCronJob.Expression()) + } + } + + // lifecycles + p.LifeCycle.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + cronLogger.Debug().Msg("starting cron scheduler") + + cronScheduler.Start() + + return nil + }, + OnStop: func(ctx context.Context) error { + cronLogger.Debug().Msg("stopping cron scheduler") + + return cronScheduler.Shutdown() + }, + }) + + return cronScheduler, nil +} + +//nolint:cyclop +func buildSchedulerOptions(cfg *config.Config) ([]gocron.SchedulerOption, error) { + var options []gocron.SchedulerOption + + // location, default local + if cfgLocation := cfg.GetString("modules.cron.scheduler.location"); cfgLocation != "" { + location, err := time.LoadLocation(cfgLocation) + if err != nil { + return nil, err + } + + options = append(options, gocron.WithLocation(location)) + } + + // concurrency + if cfg.GetBool("modules.cron.scheduler.concurrency.limit.enabled") { + var mode gocron.LimitMode + if cfg.GetString("modules.cron.scheduler.concurrency.limit.mode") == "reschedule" { + mode = gocron.LimitModeReschedule + } else { + mode = gocron.LimitModeWait + } + + options = append(options, gocron.WithLimitConcurrentJobs(cfg.GetUint("modules.cron.scheduler.concurrency.limit.max"), mode)) + } + + // stop timeout, default 10s + if cfgStopTimeout := cfg.GetString("modules.cron.scheduler.stop.timeout"); cfgStopTimeout != "" { + stopTimeout, err := time.ParseDuration(cfgStopTimeout) + if err != nil { + return nil, err + } + + options = append(options, gocron.WithStopTimeout(stopTimeout)) + } + + // jobs global options + var jobsOptions []gocron.JobOption + + // jobs execution start + if cfg.GetBool("modules.cron.jobs.execution.start.immediately") { + jobsOptions = append(jobsOptions, gocron.WithStartAt(gocron.WithStartImmediately())) + } else if cfgJobsStartAt := cfg.GetString("modules.cron.jobs.execution.start.at"); cfgJobsStartAt != "" { + jobsStartAt, err := time.Parse(time.RFC3339, cfgJobsStartAt) + if err != nil { + return nil, err + } + + jobsOptions = append(jobsOptions, gocron.WithStartAt(gocron.WithStartDateTime(jobsStartAt))) + } + + // jobs execution limit + if cfg.GetBool("modules.cron.jobs.execution.limit.enabled") { + jobsOptions = append(jobsOptions, gocron.WithLimitedRuns(cfg.GetUint("modules.cron.jobs.execution.limit.max"))) + } + + // jobs execution mode + if cfg.GetBool("modules.cron.jobs.singleton.enabled") { + var mode gocron.LimitMode + if cfg.GetString("modules.cron.jobs.singleton.mode") == "reschedule" { + mode = gocron.LimitModeReschedule + } else { + mode = gocron.LimitModeWait + } + jobsOptions = append(jobsOptions, gocron.WithSingletonMode(mode)) + } + + if len(jobsOptions) > 0 { + options = append(options, gocron.WithGlobalJobOptions(jobsOptions...)) + } + + return options, nil +} diff --git a/fxcron/module_test.go b/fxcron/module_test.go new file mode 100644 index 00000000..e2dc0ca9 --- /dev/null +++ b/fxcron/module_test.go @@ -0,0 +1,396 @@ +package fxcron_test + +import ( + "strings" + "testing" + "time" + + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/job" + "github.com/ankorstore/yokai/fxcron/testdata/cron/tracker" + "github.com/ankorstore/yokai/fxcron/testdata/factory" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxgenerate/fxgeneratetest/uuid" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/log/logtest" + "github.com/ankorstore/yokai/trace/tracetest" + "github.com/go-co-op/gocron/v2" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +//nolint:maintidx +func TestModule(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("APP_ENV", "test") + t.Setenv("CRON_START_IMMEDIATELY", "true") + t.Setenv("CRON_METRICS_BUCKETS", "1,10,100") + t.Setenv("CRON_METRICS_NAMESPACE", "foo") + t.Setenv("CRON_METRICS_SUBSYSTEM", "bar") + + var cronTracker *tracker.CronExecutionTracker + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + var metricsRegistry *prometheus.Registry + + // limits to avoid flaky tests + expectedSuccessRuns := 3 + expectedErrorRuns := 2 + expectedPanicRuns := 1 + + // deterministic test cron job execution id + cronJobExecutionId := "testCronJobExecutionID" + + app := fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxcron.FxCronModule, + // deterministic test cron job execution id + fx.Provide( + fx.Annotate( + func() string { + return cronJobExecutionId + }, + fx.ResultTags(`name:"generate-test-uuid-value"`), + ), + ), + fx.Options( + // cron jobs execution tracker + fx.Provide(tracker.NewCronExecutionTracker), + // cron jobs registration + fxcron.AsCronJob(job.NewSuccessCron, `*/1 * * * * *`, gocron.WithLimitedRuns(uint(expectedSuccessRuns))), + fxcron.AsCronJob(job.NewErrorCron, `*/1 * * * * *`, gocron.WithLimitedRuns(uint(expectedErrorRuns))), + fxcron.AsCronJob(job.NewPanicCron, `*/1 * * * * *`, gocron.WithLimitedRuns(uint(expectedPanicRuns))), + ), + // deterministic generator for cron job execution id + fx.Decorate(uuid.NewFxTestUuidGeneratorFactory), + // extraction + fx.Populate(&cronTracker, &logBuffer, &traceExporter, &metricsRegistry), + // invoke scheduler + fx.Invoke(func(scheduler gocron.Scheduler) {}), + ).RequireStart() + + // 5 seconds for cron jobs to run + time.Sleep(4 * time.Second) + + app.RequireStop() + + // success cron assertions + assert.Equal(t, expectedSuccessRuns, cronTracker.JobExecutions("success")) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "success", + "cronJobExecutionID": cronJobExecutionId, + "message": "job execution start", + }) + + for i := 1; i <= expectedSuccessRuns; i++ { + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "success", + "cronJobExecutionID": cronJobExecutionId, + "message": "success cron job log from test", + "run": i, + }) + } + + logtest.AssertHasNotLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "success", + "cronJobExecutionID": cronJobExecutionId, + "message": "success cron job log from test", + "run": expectedSuccessRuns + 1, + }) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "success", + "cronJobExecutionID": cronJobExecutionId, + "message": "job execution success", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "cron success", + attribute.String("CronJob", "success"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + ) + + for i := 1; i <= expectedSuccessRuns; i++ { + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "success cron job span", + attribute.String("CronJob", "success"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + attribute.Int("Run", i), + ) + } + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "cron success", + attribute.String("CronJob", "success"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + attribute.Int("Run", expectedSuccessRuns+1), + ) + + // error cron assertions + assert.Equal(t, expectedErrorRuns, cronTracker.JobExecutions("error")) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "error", + "cronJobExecutionID": cronJobExecutionId, + "message": "job execution start", + }) + + for i := 1; i <= expectedErrorRuns; i++ { + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "error", + "cronJobExecutionID": cronJobExecutionId, + "message": "error cron job log from test", + "run": i, + }) + } + + logtest.AssertHasNotLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "error", + "cronJobExecutionID": cronJobExecutionId, + "message": "error cron job log from test", + "run": expectedErrorRuns + 1, + }) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "error", + "service": "test", + "system": "cron", + "cronJob": "error", + "cronJobExecutionID": cronJobExecutionId, + "message": "job execution error", + "error": "error cron job error", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "cron error", + attribute.String("CronJob", "error"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + ) + + for i := 1; i <= expectedErrorRuns; i++ { + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "error cron job span", + attribute.String("CronJob", "error"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + attribute.Int("Run", i), + ) + } + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "cron error", + attribute.String("CronJob", "error"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + attribute.Int("Run", expectedErrorRuns+1), + ) + + // panic cron assertions + assert.Equal(t, expectedPanicRuns, cronTracker.JobExecutions("panic")) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "panic", + "cronJobExecutionID": cronJobExecutionId, + "message": "job execution start", + }) + + for i := 1; i <= expectedPanicRuns; i++ { + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "panic", + "cronJobExecutionID": cronJobExecutionId, + "message": "panic cron job log from test", + "run": i, + }) + } + + logtest.AssertHasNotLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "cron", + "cronJob": "panic", + "cronJobExecutionID": cronJobExecutionId, + "message": "panic cron job log from test", + "run": expectedPanicRuns + 1, + }) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "error", + "service": "test", + "system": "cron", + "cronJob": "panic", + "cronJobExecutionID": cronJobExecutionId, + "message": "job execution panic", + "panic": "panic cron job panic", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "cron panic", + attribute.String("CronJob", "panic"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + ) + + tracetest.AssertHasNotTraceSpan( + t, + traceExporter, + "cron panic", + attribute.String("CronJob", "panic"), + attribute.String("CronJobExecutionID", cronJobExecutionId), + attribute.Int("Run", expectedPanicRuns), + ) + + // cron metrics assertions + expected := ` + # HELP foo_bar_job_execution_total Total number of cron job executions + # TYPE foo_bar_job_execution_total counter + foo_bar_job_execution_total{job="success",status="success"} 3 + foo_bar_job_execution_total{job="error",status="error"} 2 + foo_bar_job_execution_total{job="panic",status="error"} 1 + ` + + err := testutil.GatherAndCompare( + metricsRegistry, + strings.NewReader(expected), + "foo_bar_job_execution_total", + ) + assert.NoError(t, err) +} + +func TestModuleInfo(t *testing.T) { + startAt := time.Now().Add(5 * time.Second) + + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("APP_ENV", "test") + t.Setenv("CRON_START_IMMEDIATELY", "false") + t.Setenv("CRON_START_AT", startAt.Format(time.RFC3339)) + t.Setenv("CRON_CONCURRENCY_LIMIT_MODE", "reschedule") + t.Setenv("CRON_SINGLETON_ENABLED", "true") + t.Setenv("CRON_SINGLETON_MODE", "reschedule") + + var modulesInfo []any + + app := fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxcron.FxCronModule, + fx.Options( + // cron jobs registration + fxcron.AsCronJob(job.NewDummyCron, `*/1 * * * * *`), + ), + // extraction + fx.Populate( + fx.Annotate( + &modulesInfo, + fx.ParamTags(`group:"core-module-infos"`), + ), + ), + // invoke scheduler + fx.Invoke(func(scheduler gocron.Scheduler) {}), + ).RequireStart() + + // scheduling assertions + info, ok := modulesInfo[0].(*fxcron.FxCronModuleInfo) + if !ok { + t.Error("expected type *fxcron.FxCronModuleInfo") + } + + assert.Equal( + t, + map[string]interface{}{ + "jobs": map[string]interface{}{ + "scheduled": map[string]interface{}{ + "dummy": map[string]interface{}{ + "expression": `*/1 * * * * *`, + "last_run": time.Time{}.Format(time.RFC3339), + "next_run": startAt.Format(time.RFC3339), + "type": "*job.DummyCron", + }, + }, + "unscheduled": map[string]interface{}{}, + }, + }, + info.Data(), + ) + + app.RequireStop() +} + +func TestModuleDecoration(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("APP_ENV", "test") + t.Setenv("MODULES_CRON_METRICS_COLLECT_ENABLED", "false") + + var scheduler gocron.Scheduler + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxcron.FxCronModule, + fx.Decorate(factory.NewTestCronSchedulerFactory), + fx.Populate(&scheduler), + ).RequireStart().RequireStop() + + assert.Len(t, scheduler.Jobs(), 0) +} diff --git a/fxcron/reflect.go b/fxcron/reflect.go new file mode 100644 index 00000000..899276a5 --- /dev/null +++ b/fxcron/reflect.go @@ -0,0 +1,15 @@ +package fxcron + +import ( + "reflect" +) + +// GetType returns the type of a target. +func GetType(target any) string { + return reflect.TypeOf(target).String() +} + +// GetReturnType returns the return type of a target. +func GetReturnType(target any) string { + return reflect.TypeOf(target).Out(0).String() +} diff --git a/fxcron/reflect_test.go b/fxcron/reflect_test.go new file mode 100644 index 00000000..e4ad2122 --- /dev/null +++ b/fxcron/reflect_test.go @@ -0,0 +1,55 @@ +package fxcron_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/tracker" + "github.com/stretchr/testify/assert" +) + +func TestGetType(t *testing.T) { + t.Parallel() + + tests := []struct { + target any + expected string + }{ + {123, "int"}, + {"test", "string"}, + {tracker.NewCronExecutionTracker(), "*tracker.CronExecutionTracker"}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + + got := fxcron.GetType(tt.target) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestGetReturnType(t *testing.T) { + t.Parallel() + + tests := []struct { + target any + expected string + }{ + {func() string { return "test" }, "string"}, + {func() int { return 123 }, "int"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + + got := fxcron.GetReturnType(tt.target) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/fxcron/register.go b/fxcron/register.go new file mode 100644 index 00000000..21b4d805 --- /dev/null +++ b/fxcron/register.go @@ -0,0 +1,26 @@ +package fxcron + +import ( + "github.com/go-co-op/gocron/v2" + "go.uber.org/fx" +) + +// AsCronJob registers a cron job into Fx, with an optional list of [gocron.JobOption]. +func AsCronJob(j any, expression string, options ...gocron.JobOption) fx.Option { + return fx.Options( + fx.Provide( + fx.Annotate( + j, + fx.As(new(CronJob)), + fx.ResultTags(`group:"cron-jobs"`), + ), + ), + fx.Supply( + fx.Annotate( + NewCronJobDefinition(GetReturnType(j), expression, options...), + fx.As(new(CronJobDefinition)), + fx.ResultTags(`group:"cron-jobs-definitions"`), + ), + ), + ) +} diff --git a/fxcron/register_test.go b/fxcron/register_test.go new file mode 100644 index 00000000..c635fd4e --- /dev/null +++ b/fxcron/register_test.go @@ -0,0 +1,19 @@ +package fxcron_test + +import ( + "fmt" + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/job" + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" +) + +func TestAsCronJob(t *testing.T) { + t.Parallel() + + result := fxcron.AsCronJob(job.NewDummyCron, `* * * * *`, gocron.WithLimitedRuns(1)) + + assert.Equal(t, "fx.optionGroup", fmt.Sprintf("%T", result)) +} diff --git a/fxcron/registry.go b/fxcron/registry.go new file mode 100644 index 00000000..d1db5f7d --- /dev/null +++ b/fxcron/registry.go @@ -0,0 +1,64 @@ +package fxcron + +import ( + "context" + "fmt" + + "go.uber.org/fx" +) + +// CronJob is the interface for cron jobs. +type CronJob interface { + Name() string + Run(ctx context.Context) error +} + +// CronJobRegistry is the registry collecting cron jobs and their definitions. +type CronJobRegistry struct { + cronJobs []CronJob + cronJobDefinitions []CronJobDefinition +} + +// FxCronJobRegistryParam allows injection of the required dependencies in [NewFxCronJobRegistry]. +type FxCronJobRegistryParam struct { + fx.In + CronJobs []CronJob `group:"cron-jobs"` + CronJobsDefinitions []CronJobDefinition `group:"cron-jobs-definitions"` +} + +// NewFxCronJobRegistry returns as new [CronJobRegistry]. +func NewFxCronJobRegistry(p FxCronJobRegistryParam) *CronJobRegistry { + return &CronJobRegistry{ + cronJobs: p.CronJobs, + cronJobDefinitions: p.CronJobsDefinitions, + } +} + +// ResolveCronJobs resolves a list of [ResolvedCronJob] from their definitions. +func (r *CronJobRegistry) ResolveCronJobs() ([]*ResolvedCronJob, error) { + resolvedCronJobs := []*ResolvedCronJob{} + + for _, definition := range r.cronJobDefinitions { + implementation, err := r.lookupRegisteredCronJob(definition.ReturnType()) + if err != nil { + return nil, err + } + + resolvedCronJobs = append( + resolvedCronJobs, + NewResolvedCronJob(implementation, definition.Expression(), definition.Options()...), + ) + } + + return resolvedCronJobs, nil +} + +func (r *CronJobRegistry) lookupRegisteredCronJob(returnType string) (CronJob, error) { + for _, implementation := range r.cronJobs { + if GetType(implementation) == returnType { + return implementation, nil + } + } + + return nil, fmt.Errorf("cannot find cron job implementation for type %s", returnType) +} diff --git a/fxcron/registry_test.go b/fxcron/registry_test.go new file mode 100644 index 00000000..886ef4a1 --- /dev/null +++ b/fxcron/registry_test.go @@ -0,0 +1,71 @@ +package fxcron_test + +import ( + "context" + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/job" + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" +) + +const cronJobExpression = `* * * * *` + +func TestNewFxCronJobRegistry(t *testing.T) { + t.Parallel() + + param := fxcron.FxCronJobRegistryParam{ + CronJobs: []fxcron.CronJob{}, + CronJobsDefinitions: []fxcron.CronJobDefinition{}, + } + + registry := fxcron.NewFxCronJobRegistry(param) + + assert.IsType(t, &fxcron.CronJobRegistry{}, registry) +} + +func TestResolveCronJobsSuccess(t *testing.T) { + t.Parallel() + + cronJob := job.NewDummyCron() + cronJobOptions := []gocron.JobOption(nil) + + param := fxcron.FxCronJobRegistryParam{ + CronJobs: []fxcron.CronJob{cronJob}, + CronJobsDefinitions: []fxcron.CronJobDefinition{ + fxcron.NewCronJobDefinition(fxcron.GetType(cronJob), cronJobExpression, cronJobOptions...), + }, + } + + registry := fxcron.NewFxCronJobRegistry(param) + + resolvedCronJobs, err := registry.ResolveCronJobs() + assert.NoError(t, err) + + assert.Len(t, resolvedCronJobs, 1) + assert.Equal(t, cronJob, resolvedCronJobs[0].Implementation()) + assert.Equal(t, cronJobExpression, resolvedCronJobs[0].Expression()) + assert.Equal(t, cronJobOptions, resolvedCronJobs[0].Options()) + assert.Equal(t, cronJob.Name(), resolvedCronJobs[0].Implementation().Name()) + assert.Nil(t, resolvedCronJobs[0].Implementation().Run(context.Background())) +} + +func TestResolveCronJobsFailure(t *testing.T) { + t.Parallel() + + cronJob := job.NewDummyCron() + + param := fxcron.FxCronJobRegistryParam{ + CronJobs: []fxcron.CronJob{cronJob}, + CronJobsDefinitions: []fxcron.CronJobDefinition{ + fxcron.NewCronJobDefinition("invalid", cronJobExpression), + }, + } + + registry := fxcron.NewFxCronJobRegistry(param) + + _, err := registry.ResolveCronJobs() + assert.Error(t, err) + assert.Equal(t, "cannot find cron job implementation for type invalid", err.Error()) +} diff --git a/fxcron/resolve.go b/fxcron/resolve.go new file mode 100644 index 00000000..a30793ba --- /dev/null +++ b/fxcron/resolve.go @@ -0,0 +1,34 @@ +package fxcron + +import "github.com/go-co-op/gocron/v2" + +// ResolvedCronJob represents a resolved cron job, with its expression and execution options. +type ResolvedCronJob struct { + implementation CronJob + expression string + options []gocron.JobOption +} + +// NewResolvedCronJob returns a new [ResolvedCronJob] instance. +func NewResolvedCronJob(implementation CronJob, expression string, options ...gocron.JobOption) *ResolvedCronJob { + return &ResolvedCronJob{ + implementation: implementation, + expression: expression, + options: options, + } +} + +// Implementation returns the [ResolvedCronJob] cron job implementation. +func (r *ResolvedCronJob) Implementation() CronJob { + return r.implementation +} + +// Expression returns the [ResolvedCronJob] cron job expression. +func (r *ResolvedCronJob) Expression() string { + return r.expression +} + +// Options returns the [ResolvedCronJob] cron job execution options. +func (r *ResolvedCronJob) Options() []gocron.JobOption { + return r.options +} diff --git a/fxcron/resolve_test.go b/fxcron/resolve_test.go new file mode 100644 index 00000000..2cd8f865 --- /dev/null +++ b/fxcron/resolve_test.go @@ -0,0 +1,25 @@ +package fxcron_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/job" + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" +) + +func TestNewResolvedCronJob(t *testing.T) { + t.Parallel() + + cronJob := job.NewDummyCron() + expression := `* * * * *` + options := []gocron.JobOption(nil) + + resolvedCronJob := fxcron.NewResolvedCronJob(cronJob, expression, options...) + + assert.IsType(t, &fxcron.ResolvedCronJob{}, resolvedCronJob) + assert.Equal(t, cronJob, resolvedCronJob.Implementation()) + assert.Equal(t, expression, resolvedCronJob.Expression()) + assert.Equal(t, options, resolvedCronJob.Options()) +} diff --git a/fxcron/testdata/config/config.test.yaml b/fxcron/testdata/config/config.test.yaml new file mode 100644 index 00000000..2e19f8f2 --- /dev/null +++ b/fxcron/testdata/config/config.test.yaml @@ -0,0 +1,26 @@ +modules: + cron: + scheduler: + location: Local + concurrency: + limit: + enabled: true + mode: ${CRON_CONCURRENCY_LIMIT_MODE} + max: 3 + jobs: + singleton: + enabled: ${CRON_SINGLETON_ENABLED} + mode: ${CRON_SINGLETON_MODE} + execution: + start: + immediately: ${CRON_START_IMMEDIATELY} + at: ${CRON_START_AT} + limit: + enabled: true + max: 5 + metrics: + collect: + enabled: true + namespace: ${CRON_METRICS_NAMESPACE} + subsystem: ${CRON_METRICS_SUBSYSTEM} + buckets: ${CRON_METRICS_BUCKETS} \ No newline at end of file diff --git a/fxcron/testdata/config/config.yaml b/fxcron/testdata/config/config.yaml new file mode 100644 index 00000000..ef2c9923 --- /dev/null +++ b/fxcron/testdata/config/config.yaml @@ -0,0 +1,41 @@ +app: + name: test + version: 0.1.0 +modules: + log: + level: debug + output: test + trace: + processor: + type: test + cron: + scheduler: + seconds: true + concurrency: + limit: + enabled: false + max: 3 + mode: wait + stop: + timeout: 5s + jobs: + execution: + start: + immediately: true + limit: + enabled: false + max: 3 + singleton: + enabled: true + mode: wait + log: + enabled: true + exclude: + - foo + - bar + trace: + enabled: true + exclude: + - foo + - bar + diff --git a/fxcron/testdata/cron/job/dummy.go b/fxcron/testdata/cron/job/dummy.go new file mode 100644 index 00000000..f75e5886 --- /dev/null +++ b/fxcron/testdata/cron/job/dummy.go @@ -0,0 +1,19 @@ +package job + +import ( + "context" +) + +type DummyCron struct{} + +func NewDummyCron() *DummyCron { + return &DummyCron{} +} + +func (c *DummyCron) Name() string { + return "dummy" +} + +func (c *DummyCron) Run(ctx context.Context) error { + return nil +} diff --git a/fxcron/testdata/cron/job/error.go b/fxcron/testdata/cron/job/error.go new file mode 100644 index 00000000..a55e8757 --- /dev/null +++ b/fxcron/testdata/cron/job/error.go @@ -0,0 +1,43 @@ +package job + +import ( + "context" + "fmt" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/tracker" + "go.opentelemetry.io/otel/attribute" +) + +type ErrorCron struct { + config *config.Config + tracker *tracker.CronExecutionTracker +} + +func NewErrorCron(cfg *config.Config, trk *tracker.CronExecutionTracker) *ErrorCron { + return &ErrorCron{ + config: cfg, + tracker: trk, + } +} + +func (c *ErrorCron) Name() string { + return "error" +} + +func (c *ErrorCron) Run(ctx context.Context) error { + jobName := fxcron.CtxCronJobName(ctx) + + c.tracker.TrackJobExecution(jobName) + e := c.tracker.JobExecutions(jobName) + + ctx, span := fxcron.CtxTracer(ctx).Start(ctx, "error cron job span") + defer span.End() + + span.SetAttributes(attribute.Int("Run", e)) + + fxcron.CtxLogger(ctx).Info().Int("run", e).Msgf("error cron job log from %s", c.config.AppName()) + + return fmt.Errorf("error cron job error") +} diff --git a/fxcron/testdata/cron/job/panic.go b/fxcron/testdata/cron/job/panic.go new file mode 100644 index 00000000..6aa4e837 --- /dev/null +++ b/fxcron/testdata/cron/job/panic.go @@ -0,0 +1,36 @@ +package job + +import ( + "context" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/tracker" +) + +type PanicCron struct { + config *config.Config + tracker *tracker.CronExecutionTracker +} + +func NewPanicCron(cfg *config.Config, trk *tracker.CronExecutionTracker) *PanicCron { + return &PanicCron{ + config: cfg, + tracker: trk, + } +} + +func (c *PanicCron) Name() string { + return "panic" +} + +func (c *PanicCron) Run(ctx context.Context) error { + jobName := fxcron.CtxCronJobName(ctx) + + c.tracker.TrackJobExecution(jobName) + e := c.tracker.JobExecutions(jobName) + + fxcron.CtxLogger(ctx).Info().Int("run", e).Msgf("panic cron job log from %s", c.config.AppName()) + + panic("panic cron job panic") +} diff --git a/fxcron/testdata/cron/job/success.go b/fxcron/testdata/cron/job/success.go new file mode 100644 index 00000000..7c0a35f3 --- /dev/null +++ b/fxcron/testdata/cron/job/success.go @@ -0,0 +1,42 @@ +package job + +import ( + "context" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxcron" + "github.com/ankorstore/yokai/fxcron/testdata/cron/tracker" + "go.opentelemetry.io/otel/attribute" +) + +type SuccessCron struct { + config *config.Config + tracker *tracker.CronExecutionTracker +} + +func NewSuccessCron(cfg *config.Config, trk *tracker.CronExecutionTracker) *SuccessCron { + return &SuccessCron{ + config: cfg, + tracker: trk, + } +} + +func (c *SuccessCron) Name() string { + return "success" +} + +func (c *SuccessCron) Run(ctx context.Context) error { + jobName := fxcron.CtxCronJobName(ctx) + + c.tracker.TrackJobExecution(jobName) + e := c.tracker.JobExecutions(jobName) + + ctx, span := fxcron.CtxTracer(ctx).Start(ctx, "success cron job span") + defer span.End() + + span.SetAttributes(attribute.Int("Run", e)) + + fxcron.CtxLogger(ctx).Info().Int("run", e).Msgf("success cron job log from %s", c.config.AppName()) + + return nil +} diff --git a/fxcron/testdata/cron/tracker/tracker.go b/fxcron/testdata/cron/tracker/tracker.go new file mode 100644 index 00000000..7da5a348 --- /dev/null +++ b/fxcron/testdata/cron/tracker/tracker.go @@ -0,0 +1,40 @@ +package tracker + +import "sync" + +type CronExecutionTracker struct { + mutex sync.Mutex + executions map[string]int +} + +func NewCronExecutionTracker() *CronExecutionTracker { + return &CronExecutionTracker{ + executions: make(map[string]int), + } +} + +func (t *CronExecutionTracker) TrackJobExecution(jobName string) *CronExecutionTracker { + t.mutex.Lock() + + if executions, ok := t.executions[jobName]; ok { + t.executions[jobName] = executions + 1 + } else { + t.executions[jobName] = 1 + } + + t.mutex.Unlock() + + return t +} + +func (t *CronExecutionTracker) JobExecutions(jobName string) int { + t.mutex.Lock() + defer t.mutex.Unlock() + + jobExecutions := 0 + if executions, ok := t.executions[jobName]; ok { + jobExecutions = executions + } + + return jobExecutions +} diff --git a/fxcron/testdata/factory/factory.go b/fxcron/testdata/factory/factory.go new file mode 100644 index 00000000..2feb9233 --- /dev/null +++ b/fxcron/testdata/factory/factory.go @@ -0,0 +1,16 @@ +package factory + +import ( + "github.com/ankorstore/yokai/fxcron" + "github.com/go-co-op/gocron/v2" +) + +type TestCronSchedulerFactory struct{} + +func NewTestCronSchedulerFactory() fxcron.CronSchedulerFactory { + return &TestCronSchedulerFactory{} +} + +func (f *TestCronSchedulerFactory) Create(options ...gocron.SchedulerOption) (gocron.Scheduler, error) { + return gocron.NewScheduler() +} diff --git a/fxcron/trace.go b/fxcron/trace.go new file mode 100644 index 00000000..6384d981 --- /dev/null +++ b/fxcron/trace.go @@ -0,0 +1,56 @@ +package fxcron + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + otelsdktrace "go.opentelemetry.io/otel/sdk/trace" + oteltrace "go.opentelemetry.io/otel/trace" +) + +// AnnotateTracerProvider extends a provided [oteltrace.TracerProvider] spans with cron jobs execution attributes. +func AnnotateTracerProvider(base oteltrace.TracerProvider) oteltrace.TracerProvider { + if tp, ok := base.(*otelsdktrace.TracerProvider); ok { + tp.RegisterSpanProcessor(NewTracerProviderCronJobAnnotator()) + + return tp + } + + return base +} + +// TracerProviderCronJobAnnotator is the [oteltrace.TracerProvider] cron jobs annotator, implementing [otelsdktrace.SpanProcessor]. +type TracerProviderCronJobAnnotator struct{} + +// NewTracerProviderCronJobAnnotator returns a new [TracerProviderWorkerAnnotator]. +func NewTracerProviderCronJobAnnotator() *TracerProviderCronJobAnnotator { + return &TracerProviderCronJobAnnotator{} +} + +// OnStart adds cron job execution attributes to a given [otelsdktrace.ReadWriteSpan]. +func (a *TracerProviderCronJobAnnotator) OnStart(ctx context.Context, s otelsdktrace.ReadWriteSpan) { + name := CtxCronJobName(ctx) + if name != "" { + s.SetAttributes(attribute.String(TraceSpanAttributeCronJobName, name)) + } + + executionId := CtxCronJobExecutionId(ctx) + if executionId != "" { + s.SetAttributes(attribute.String(TraceSpanAttributeCronJobExecutionId, executionId)) + } +} + +// Shutdown is just for [otelsdktrace.SpanProcessor] compliance. +func (a *TracerProviderCronJobAnnotator) Shutdown(context.Context) error { + return nil +} + +// ForceFlush is just for [otelsdktrace.SpanProcessor] compliance. +func (a *TracerProviderCronJobAnnotator) ForceFlush(context.Context) error { + return nil +} + +// OnEnd is just for [otelsdktrace.SpanProcessor] compliance. +func (a *TracerProviderCronJobAnnotator) OnEnd(otelsdktrace.ReadOnlySpan) { + // noop +} diff --git a/fxcron/trace_test.go b/fxcron/trace_test.go new file mode 100644 index 00000000..7920b79d --- /dev/null +++ b/fxcron/trace_test.go @@ -0,0 +1,42 @@ +package fxcron_test + +import ( + "context" + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +type tracerProviderMock struct { + mock.Mock +} + +func (m *tracerProviderMock) Tracer(string, ...trace.TracerOption) trace.Tracer { + args := m.Called() + + return otel.GetTracerProvider().Tracer(args.String(0)) +} + +func TestAnnotateTracerProviderWithNonSdkTracerProvider(t *testing.T) { + t.Parallel() + + tp := new(tracerProviderMock) + + assert.Equal(t, tp, fxcron.AnnotateTracerProvider(tp)) +} + +func TestNewTracerProviderCronJobAnnotator(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + annotator := fxcron.NewTracerProviderCronJobAnnotator() + + assert.IsType(t, &fxcron.TracerProviderCronJobAnnotator{}, annotator) + assert.Nil(t, annotator.ForceFlush(ctx)) + assert.Nil(t, annotator.Shutdown(ctx)) +} diff --git a/fxcron/util.go b/fxcron/util.go new file mode 100644 index 00000000..099a2def --- /dev/null +++ b/fxcron/util.go @@ -0,0 +1,22 @@ +package fxcron + +import "strings" + +// Contains returns true if a provided string is found in a list of strings. +func Contains(list []string, item string) bool { + for _, i := range list { + if i == item { + return true + } + } + + return false +} + +// Sanitize transforms a given string to not contain spaces or dashes, and to be in lower case. +func Sanitize(str string) string { + str = strings.ReplaceAll(str, " ", "_") + str = strings.ReplaceAll(str, "-", "_") + + return strings.ToLower(str) +} diff --git a/fxcron/util_test.go b/fxcron/util_test.go new file mode 100644 index 00000000..dea96724 --- /dev/null +++ b/fxcron/util_test.go @@ -0,0 +1,35 @@ +package fxcron_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxcron" + "github.com/stretchr/testify/assert" +) + +func TestContains(t *testing.T) { + t.Parallel() + + list := []string{ + "/foo", + "/bar", + "/baz", + } + + assert.True(t, fxcron.Contains(list, "/foo")) + assert.True(t, fxcron.Contains(list, "/bar")) + assert.True(t, fxcron.Contains(list, "/baz")) + + assert.False(t, fxcron.Contains(list, "/fo")) + assert.False(t, fxcron.Contains(list, "/ba")) + assert.False(t, fxcron.Contains(list, "/invalid")) +} + +func TestSanitize(t *testing.T) { + t.Parallel() + + assert.Equal(t, "foo_bar", fxcron.Sanitize("foo-bar")) + assert.Equal(t, "foo_bar", fxcron.Sanitize("foo bar")) + assert.Equal(t, "foo_bar", fxcron.Sanitize("Foo-Bar")) + assert.Equal(t, "foo_bar", fxcron.Sanitize("Foo Bar")) +} diff --git a/release-please-config.json b/release-please-config.json index 44d997f3..851dbc2f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -56,6 +56,11 @@ "component": "fxconfig", "tag-separator": "/" }, + "fxcron": { + "release-type": "go", + "component": "fxcron", + "tag-separator": "/" + }, "fxgenerate": { "release-type": "go", "component": "fxgenerate",