diff --git a/go.mod b/go.mod index feae19f..cb0931a 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/samber/lo v1.52.0 github.com/spf13/cobra v1.10.1 + github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.44.0 golang.org/x/sync v0.18.0 golang.org/x/text v0.31.0 @@ -44,6 +45,7 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -56,6 +58,7 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect diff --git a/internal/actions/db/connect/connect.go b/internal/actions/db/connect/connect.go index e897454..911ffe7 100644 --- a/internal/actions/db/connect/connect.go +++ b/internal/actions/db/connect/connect.go @@ -4,9 +4,9 @@ import "time" // ConnConfig is the top-level structure of the YAML config file (~/.draft/dbconnect.yml). type ConnConfig struct { - Defaults map[string]DefaultConfig `yaml:"defaults"` - Environments map[string]EnvConfig `yaml:"environments"` - Connections map[string]ConnTypeConfig `yaml:"connections"` + Defaults map[string]DefaultConfig `yaml:"defaults"` + Environments map[string]EnvConfig `yaml:"environments"` + Connections map[string]ConnTypeConfig `yaml:"connections"` } // DefaultConfig holds per-type defaults (e.g. remote port). diff --git a/internal/actions/newservice/newservice.go b/internal/actions/newservice/newservice.go index 9451316..9c0b2e8 100644 --- a/internal/actions/newservice/newservice.go +++ b/internal/actions/newservice/newservice.go @@ -62,6 +62,7 @@ func (ns *NewService) createAllDirs() error { folders := []string{ ns.input.ServicePath + "/config/app", ns.input.ServicePath + "/config/sls", + ns.input.ServicePath + "/config/otel-layer", } return dirs.Create(folders...) @@ -105,6 +106,7 @@ func (ns *NewService) getFileList() []dtos.FileEntry { {Path: "/config/app/modules.pkl", Data: ns.tmpl.Config.App.ModulesPkl}, {Path: "/config/sls/environment.yml", Data: ns.tmpl.Config.Sls.EnvironmentYAML}, {Path: "/config/sls/resources.yml", Data: ns.tmpl.Config.Sls.ResourcesYAML}, + {Path: "/config/otel-layer/collector.yaml", Data: ns.tmpl.Config.Sls.OtelCollectorYAML}, } entries = append(entries, ns.getEntries()...) diff --git a/internal/dtos/lambda_input.go b/internal/dtos/lambda_input.go index 7df85c4..0ec5cd0 100644 --- a/internal/dtos/lambda_input.go +++ b/internal/dtos/lambda_input.go @@ -19,6 +19,7 @@ type LambdaInput struct { NextLambdaImportTag string IsLegacy bool UseDig bool + UseOtel bool ReservedConcurrency string UseIdempotency bool } diff --git a/internal/dtos/service_input.go b/internal/dtos/service_input.go index 50a5bc5..802a051 100644 --- a/internal/dtos/service_input.go +++ b/internal/dtos/service_input.go @@ -23,6 +23,7 @@ type ServiceInput struct { NextLambdaImportTag string IsLegacy bool UseDig bool + UseOtel bool ReservedConcurrency string RoleName string } diff --git a/internal/forms/newlambda/base_form.go b/internal/forms/newlambda/base_form.go index 58b6224..645539a 100644 --- a/internal/forms/newlambda/base_form.go +++ b/internal/forms/newlambda/base_form.go @@ -45,6 +45,11 @@ func baseForm(input *dtos.LambdaInput) error { if errSrv != nil { return errSrv } + + useOtel, errOtel := project.IsOtelService(input.ServicePath) + if errOtel == nil { + input.UseOtel = useOtel + } } errName := inputs.Text("Lambda Name:", diff --git a/internal/forms/newservice/newservice.go b/internal/forms/newservice/newservice.go index 1806951..d4d30a9 100644 --- a/internal/forms/newservice/newservice.go +++ b/internal/forms/newservice/newservice.go @@ -14,6 +14,7 @@ func GetForm(input *dtos.ServiceInput) error { input.PackageName = data.Meta.PackageName input.NextImportTag = data.NextImportTag input.NextLambdaImportTag = data.NextLambdaImportTag + input.UseOtel = true if err := baseForm(input); err != nil { return err diff --git a/internal/pkg/inputs/select.go b/internal/pkg/inputs/select.go index 4f7a596..e855b58 100644 --- a/internal/pkg/inputs/select.go +++ b/internal/pkg/inputs/select.go @@ -1,6 +1,10 @@ package inputs -import "github.com/charmbracelet/huh" +import ( + "sort" + + "github.com/charmbracelet/huh" +) func Select[T comparable](title string, opts ...Option[T]) error { input := huh.NewSelect[T]().Title(title) @@ -15,10 +19,15 @@ func Select[T comparable](title string, opts ...Option[T]) error { input.Description(inputOpts.description) } - selectOpts := make([]huh.Option[T], 0, len(opts)) + keys := make([]string, 0, len(inputOpts.options)) + for k := range inputOpts.options { + keys = append(keys, k) + } + sort.Strings(keys) - for optKey, optVal := range inputOpts.options { - selectOpts = append(selectOpts, huh.NewOption(optKey, optVal)) + selectOpts := make([]huh.Option[T], 0, len(inputOpts.options)) + for _, optKey := range keys { + selectOpts = append(selectOpts, huh.NewOption(optKey, inputOpts.options[optKey])) } input.Options(selectOpts...) diff --git a/internal/project/detect_otel.go b/internal/project/detect_otel.go new file mode 100644 index 0000000..1b1595e --- /dev/null +++ b/internal/project/detect_otel.go @@ -0,0 +1,18 @@ +package project + +import ( + "strings" + + "github.com/Drafteame/draft/internal/pkg/files" +) + +// IsOtelService reads the service's serverless.yml and returns true if the +// build command contains the otel build tag, indicating the service uses OpenTelemetry. +func IsOtelService(servicePath string) (bool, error) { + content, err := files.Read(servicePath + "/serverless.yml") + if err != nil { + return false, err + } + + return strings.Contains(string(content), `"lambda.norpc,otel"`), nil +} diff --git a/internal/templates/config_sls.go b/internal/templates/config_sls.go index 4be1c38..84c17d7 100644 --- a/internal/templates/config_sls.go +++ b/internal/templates/config_sls.go @@ -1,8 +1,9 @@ package templates type ConfigSls struct { - EnvironmentYAML []byte - ResourcesYAML []byte + EnvironmentYAML []byte + ResourcesYAML []byte + OtelCollectorYAML []byte } func loadConfigSls(v ConfigSlsSetter, data any) error { @@ -11,6 +12,7 @@ func loadConfigSls(v ConfigSlsSetter, data any) error { loaders := []func(*ConfigSls, any) error{ loadConfigSlsEnvironmentYAML, loadConfigSlsResourcesYAML, + loadConfigSlsOtelCollectorYAML, } for _, loader := range loaders { @@ -51,3 +53,17 @@ func loadConfigSlsResourcesYAML(v *ConfigSls, data any) error { return nil } + +func loadConfigSlsOtelCollectorYAML(v *ConfigSls, data any) error { + name := "config/otel-layer/collector.yaml" + path := "tmpl/sls/config/otel-layer/collector.yaml.tmpl" + + content, err := loadTemplate(name, path, data, sls) + if err != nil { + return err + } + + v.OtelCollectorYAML = content + + return nil +} diff --git a/internal/templates/templates_test.go b/internal/templates/templates_test.go new file mode 100644 index 0000000..2224361 --- /dev/null +++ b/internal/templates/templates_test.go @@ -0,0 +1,389 @@ +package templates_test + +import ( + "go/parser" + "go/token" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/Drafteame/draft/internal/data" + "github.com/Drafteame/draft/internal/dtos" + "github.com/Drafteame/draft/internal/templates" +) + +// baseLambdaInput returns a populated LambdaInput for template rendering tests. +func baseLambdaInput(lambdaType string, useOtel bool) dtos.LambdaInput { + return dtos.LambdaInput{ + PackageName: "github.com/Drafteame/api", + ServicePath: "services/testsvc", + ServiceName: "testsvc", + LambdaName: "testlambda", + LambdaType: lambdaType, + CustomTypePath: "custom", + FrameVersion: "v2", + QueueARN: "arn:aws:sqs:us-east-2:123456789012:test-queue", + HTTPPath: "/test", + HTTPPathAPIGateway: "/test", + HTTPPathEcho: "/test", + HTTPMethod: "GET", + CronExpression: "rate(1 hour)", + NextImportTag: data.NextImportTag, + NextLambdaImportTag: data.NextLambdaImportTag, + IsLegacy: false, + UseDig: false, + UseOtel: useOtel, + ReservedConcurrency: "medium.eventDriven", + UseIdempotency: false, + } +} + +func baseServiceInput() dtos.ServiceInput { + return dtos.ServiceInput{ + PackageName: "github.com/Drafteame/api", + ServiceName: "testsvc", + NormalizedServiceName: "testsvc", + ServicePath: "services/testsvc", + ServicePackage: "testsvc", + LambdaName: "helloworld", + LambdaType: "plain", + CustomDomain: false, + DomainPath: "", + WarmupEnabled: false, + FrameVersion: "v2", + HasSentry: false, + SentryDSN: "", + NextImportTag: data.NextImportTag, + NextLambdaImportTag: data.NextLambdaImportTag, + IsLegacy: false, + UseDig: false, + UseOtel: true, + ReservedConcurrency: "medium.http", + RoleName: "Testsvc", + } +} + +func baseDomainInput(dbType string) dtos.DomainInput { + return dtos.DomainInput{ + PackageName: "github.com/Drafteame/api", + DomainPath: "domains/testdomain", + DomainName: "testdomain", + DomainNamePascal: "Testdomain", + DomainNameLower: "testdomain", + DBPrefix: "tst", + TableName: "public.testdomains", + DBProviderFuncName: "TestDB", + DBType: dbType, + DBName: "testdb", + } +} + +// assertGoSyntax validates that content is syntactically valid Go source. +func assertGoSyntax(t *testing.T, name string, content []byte) { + t.Helper() + + fset := token.NewFileSet() + _, err := parser.ParseFile(fset, name, content, parser.AllErrors) + assert.NoError(t, err, "invalid Go syntax in %s:\n%s", name, string(content)) +} + +// assertYAMLSyntax validates that content is valid YAML. +func assertYAMLSyntax(t *testing.T, name string, content []byte) { + t.Helper() + + var out any + err := yaml.Unmarshal(content, &out) + assert.NoError(t, err, "invalid YAML in %s", name) +} + +// ============================================================================= +// Lambda templates +// ============================================================================= + +func TestLambdaTemplates_Plain(t *testing.T) { + for _, useOtel := range []bool{false, true} { + name := "xray" + if useOtel { + name = "otel" + } + + t.Run(name, func(t *testing.T) { + tmpl, err := templates.NewLambdaTemplates(baseLambdaInput("plain", useOtel)) + require.NoError(t, err) + + p := tmpl.Plain + assertGoSyntax(t, "plain/main.go", p.MainGo) + assertYAMLSyntax(t, "plain/lambda-config.yml", p.LambdaConfigYAML) + assertGoSyntax(t, "plain/handler/bootstrap.go", p.Handler.BootstrapGo) + assertGoSyntax(t, "plain/handler/worker/worker.go", p.Handler.WorkerGo) + assertGoSyntax(t, "plain/handler/worker/resources.go", p.Handler.ResourcesGo) + assertGoSyntax(t, "plain/handler/dtos/dto.go", p.Handler.DtosGo) + }) + } +} + +func TestLambdaTemplates_HTTP(t *testing.T) { + for _, useOtel := range []bool{false, true} { + name := "xray" + if useOtel { + name = "otel" + } + + t.Run(name, func(t *testing.T) { + tmpl, err := templates.NewLambdaTemplates(baseLambdaInput("http", useOtel)) + require.NoError(t, err) + + h := tmpl.HTTP + assertGoSyntax(t, "http/main.go", h.MainGo) + assertYAMLSyntax(t, "http/lambda-config.yml", h.LambdaConfigYAML) + assertGoSyntax(t, "http/handler/bootstrap.go", h.Handler.BootstrapGo) + assertGoSyntax(t, "http/handler/worker/worker.go", h.Handler.WorkerGo) + assertGoSyntax(t, "http/handler/worker/resources.go", h.Handler.ResourcesGo) + }) + } +} + +func TestLambdaTemplates_SQS(t *testing.T) { + for _, useOtel := range []bool{false, true} { + name := "xray" + if useOtel { + name = "otel" + } + + t.Run(name, func(t *testing.T) { + tmpl, err := templates.NewLambdaTemplates(baseLambdaInput("sqs", useOtel)) + require.NoError(t, err) + + s := tmpl.Sqs + assertGoSyntax(t, "sqs/main.go", s.MainGo) + assertYAMLSyntax(t, "sqs/lambda-config.yml", s.LambdaConfigYAML) + assertGoSyntax(t, "sqs/handler/bootstrap.go", s.Handler.BootstrapGo) + assertGoSyntax(t, "sqs/handler/worker/worker.go", s.Handler.WorkerGo) + assertGoSyntax(t, "sqs/handler/worker/resources.go", s.Handler.ResourcesGo) + assertGoSyntax(t, "sqs/handler/dtos/dto.go", s.Handler.DtosGo) + assertGoSyntax(t, "sqs/handler/worker/idempotency.go", s.Handler.IdempotencyGo) + assertGoSyntax(t, "sqs/handler/worker/interfaces.go", s.Handler.InterfacesGo) + }) + } +} + +func TestLambdaTemplates_Cron(t *testing.T) { + for _, useOtel := range []bool{false, true} { + name := "xray" + if useOtel { + name = "otel" + } + + t.Run(name, func(t *testing.T) { + tmpl, err := templates.NewLambdaTemplates(baseLambdaInput("cron", useOtel)) + require.NoError(t, err) + + c := tmpl.Cron + assertGoSyntax(t, "cron/main.go", c.MainGo) + assertYAMLSyntax(t, "cron/lambda-config.yml", c.LambdaConfigYAML) + assertGoSyntax(t, "cron/handler/bootstrap.go", c.Handler.BootstrapGo) + assertGoSyntax(t, "cron/handler/worker/worker.go", c.Handler.WorkerGo) + }) + } +} + +func TestLambdaTemplates_SnsSqs(t *testing.T) { + for _, useOtel := range []bool{false, true} { + name := "xray" + if useOtel { + name = "otel" + } + + t.Run(name, func(t *testing.T) { + tmpl, err := templates.NewLambdaTemplates(baseLambdaInput("snssqs", useOtel)) + require.NoError(t, err) + + ss := tmpl.SnsSqs + assertGoSyntax(t, "snssqs/main.go", ss.MainGo) + assertYAMLSyntax(t, "snssqs/lambda-config.yml", ss.LambdaConfigYAML) + assertGoSyntax(t, "snssqs/handler/bootstrap.go", ss.Handler.BootstrapGo) + assertGoSyntax(t, "snssqs/handler/worker/worker.go", ss.Handler.WorkerGo) + assertGoSyntax(t, "snssqs/handler/worker/resources.go", ss.Handler.ResourcesGo) + assertGoSyntax(t, "snssqs/handler/dtos/dto.go", ss.Handler.DtosGo) + assertGoSyntax(t, "snssqs/handler/worker/idempotency.go", ss.Handler.IdempotencyGo) + assertGoSyntax(t, "snssqs/handler/worker/interfaces.go", ss.Handler.InterfacesGo) + }) + } +} + +func TestLambdaTemplates_Custom(t *testing.T) { + for _, useOtel := range []bool{false, true} { + name := "xray" + if useOtel { + name = "otel" + } + + t.Run(name, func(t *testing.T) { + tmpl, err := templates.NewLambdaTemplates(baseLambdaInput("custom", useOtel)) + require.NoError(t, err) + + c := tmpl.Custom + assertGoSyntax(t, "custom/main.go", c.MainGo) + assertYAMLSyntax(t, "custom/lambda-config.yml", c.LambdaConfigYAML) + assertGoSyntax(t, "custom/handler/bootstrap.go", c.Handler.BootstrapGo) + assertGoSyntax(t, "custom/handler/worker/worker.go", c.Handler.WorkerGo) + assertGoSyntax(t, "custom/handler/worker/resources.go", c.Handler.ResourcesGo) + assertGoSyntax(t, "custom/handler/worker/idempotency.go", c.Handler.IdempotencyGo) + assertGoSyntax(t, "custom/handler/worker/interfaces.go", c.Handler.InterfacesGo) + assertGoSyntax(t, "custom/handler/worker/worker_setup_test.go", c.Handler.WorkerSetupTestGo) + assertGoSyntax(t, "custom/handler/worker/worker_test.go", c.Handler.WorkerTestGo) + }) + } +} + +// ============================================================================= +// Service templates +// ============================================================================= + +func TestServiceTemplates(t *testing.T) { + tmpl, err := templates.NewServiceTemplates(baseServiceInput()) + require.NoError(t, err) + + assertYAMLSyntax(t, "serverless.yml", tmpl.ServerlessYAML) + assertGoSyntax(t, "deps.go", tmpl.DepsGo) + assertYAMLSyntax(t, "config/sls/environment.yml", tmpl.Config.Sls.EnvironmentYAML) + assertYAMLSyntax(t, "config/sls/resources.yml", tmpl.Config.Sls.ResourcesYAML) + assertYAMLSyntax(t, "config/otel-layer/collector.yaml", tmpl.Config.Sls.OtelCollectorYAML) + + // Initial plain lambda generated with service + p := tmpl.Lambda.Plain + assertGoSyntax(t, "cmd/plain/helloworld/main.go", p.MainGo) + assertYAMLSyntax(t, "cmd/plain/helloworld/lambda-config.yml", p.LambdaConfigYAML) + assertGoSyntax(t, "cmd/plain/helloworld/handler/bootstrap.go", p.Handler.BootstrapGo) + assertGoSyntax(t, "cmd/plain/helloworld/handler/worker/worker.go", p.Handler.WorkerGo) + assertGoSyntax(t, "cmd/plain/helloworld/handler/worker/resources.go", p.Handler.ResourcesGo) + assertGoSyntax(t, "cmd/plain/helloworld/handler/dtos/dto.go", p.Handler.DtosGo) +} + +func TestServiceTemplates_WithCustomDomain(t *testing.T) { + input := baseServiceInput() + input.CustomDomain = true + input.DomainPath = "api/v1" + + tmpl, err := templates.NewServiceTemplates(input) + require.NoError(t, err) + + assertYAMLSyntax(t, "serverless.yml", tmpl.ServerlessYAML) +} + +func TestServiceTemplates_WithWarmup(t *testing.T) { + input := baseServiceInput() + input.WarmupEnabled = true + + tmpl, err := templates.NewServiceTemplates(input) + require.NoError(t, err) + + assertYAMLSyntax(t, "serverless.yml", tmpl.ServerlessYAML) +} + +// ============================================================================= +// Domain templates — postgres +// ============================================================================= + +func TestDomainTemplates_Postgres(t *testing.T) { + input := baseDomainInput(data.DBTypePostgres) + + d, err := templates.NewDomains(input) + require.NoError(t, err) + + t.Run("service", func(t *testing.T) { + svc := d.Service.Postgres + assertGoSyntax(t, "service/create.go", svc.CreateGo) + assertGoSyntax(t, "service/create_test.go", svc.CreateTestGo) + assertGoSyntax(t, "service/get.go", svc.GetGo) + assertGoSyntax(t, "service/get_test.go", svc.GetTestGo) + assertGoSyntax(t, "service/update.go", svc.UpdateGo) + assertGoSyntax(t, "service/update_test.go", svc.UpdateTestGo) + assertGoSyntax(t, "service/delete.go", svc.DeleteGo) + assertGoSyntax(t, "service/delete_test.go", svc.DeleteTestGo) + assertGoSyntax(t, "service/search.go", svc.SearchGo) + assertGoSyntax(t, "service/search_test.go", svc.SearchTestGo) + assertGoSyntax(t, "service/search_one.go", svc.SearchOneGo) + assertGoSyntax(t, "service/search_one_test.go", svc.SearchOneTestGo) + assertGoSyntax(t, "service/service.go", svc.ServiceGo) + assertGoSyntax(t, "service/service_test.go", svc.ServiceTestGo) + assertGoSyntax(t, "service/interfaces.go", svc.InterfacesGo) + assertGoSyntax(t, "service/provide.go", svc.ProvideGo) + }) + + t.Run("repository", func(t *testing.T) { + repo := d.Repository.Postgres + assertGoSyntax(t, "repository/create.go", repo.CreateGo) + assertGoSyntax(t, "repository/create_test.go", repo.CreateTestGo) + assertGoSyntax(t, "repository/get.go", repo.GetGo) + assertGoSyntax(t, "repository/get_test.go", repo.GetTestGo) + assertGoSyntax(t, "repository/update.go", repo.UpdateGo) + assertGoSyntax(t, "repository/update_test.go", repo.UpdateTestGo) + assertGoSyntax(t, "repository/delete.go", repo.DeleteGo) + assertGoSyntax(t, "repository/delete_test.go", repo.DeleteTestGo) + assertGoSyntax(t, "repository/search.go", repo.SearchGo) + assertGoSyntax(t, "repository/search_test.go", repo.SearchTestGo) + assertGoSyntax(t, "repository/search_one.go", repo.SearchOneGo) + assertGoSyntax(t, "repository/search_one_test.go", repo.SearchOneTestGo) + assertGoSyntax(t, "repository/repository.go", repo.RepositoryGo) + assertGoSyntax(t, "repository/repository_test.go", repo.RepositoryTestGo) + assertGoSyntax(t, "repository/interfaces.go", repo.InterfacesGo) + assertGoSyntax(t, "repository/provide.go", repo.ProvideGo) + assertGoSyntax(t, "repository/builders/search.go", repo.Builders.SearchGo) + assertGoSyntax(t, "repository/builders/search_filters.go", repo.Builders.SearchFiltersGo) + assertGoSyntax(t, "repository/builders/search_orders.go", repo.Builders.SearchOrdersGo) + assertGoSyntax(t, "repository/builders/search_pagination.go", repo.Builders.SearchPaginationGo) + assertGoSyntax(t, "repository/daos/daos.go", repo.Daos.DaosGo) + assertGoSyntax(t, "repository/daos/delete.go", repo.Daos.DeleteGo) + assertGoSyntax(t, "repository/daos/update.go", repo.Daos.UpdateGo) + }) + + t.Run("domain", func(t *testing.T) { + dom := d.Domain + assertGoSyntax(t, "domain/domain.go", dom.DomainGo) + assertGoSyntax(t, "domain/errors.go", dom.ErrorsGo) + assertGoSyntax(t, "domain/options/search.go", dom.Options.SearchGo) + assertGoSyntax(t, "domain/options/search_filters.go", dom.Options.SearchFiltersGo) + assertGoSyntax(t, "domain/options/search_orders.go", dom.Options.SearchOrdersGo) + assertGoSyntax(t, "domain/options/search_pagination.go", dom.Options.SearchPaginationGo) + assertGoSyntax(t, "domain/options/update_fields.go", dom.Options.UpdateFieldsGo) + }) + + t.Run("providers", func(t *testing.T) { + prov := d.Providers + assertGoSyntax(t, "providers/generator.go", prov.GeneratorGo) + assertGoSyntax(t, "providers/service.go", prov.ServiceGo) + assertGoSyntax(t, "providers/postgres/repository.go", prov.Postgres.RepositoryGo) + assertGoSyntax(t, "providers/generators/nanoid/tableid/provide.go", prov.GeneratorsNanoidTableid.ProvideGo) + }) +} + +// ============================================================================= +// Domain templates — dynamo +// ============================================================================= + +func TestDomainTemplates_Dynamo(t *testing.T) { + input := baseDomainInput(data.DBTypeDynamo) + + d, err := templates.NewDomains(input) + require.NoError(t, err) + + // DynamoDB domains only generate service and repository layers. + // The domain entity layer (options, errors, etc.) is postgres-only. + + t.Run("service", func(t *testing.T) { + svc := d.Service.Dynamo + assertGoSyntax(t, "service/service.go", svc.ServiceGo) + assertGoSyntax(t, "service/interfaces.go", svc.InterfacesGo) + assertGoSyntax(t, "service/provide.go", svc.ProviderGo) + }) + + t.Run("repository", func(t *testing.T) { + repo := d.Repository.Dynamo + assertGoSyntax(t, "repository/repository.go", repo.RepositoryGo) + assertGoSyntax(t, "repository/interfaces.go", repo.InterfacesGo) + assertGoSyntax(t, "repository/provide.go", repo.ProviderGo) + }) +} diff --git a/internal/templates/tmpl/domain/providers/generators/nanoid/tableid/provide.go.tmpl b/internal/templates/tmpl/domain/providers/generators/nanoid/tableid/provide.go.tmpl index 44ca966..dd0ee14 100644 --- a/internal/templates/tmpl/domain/providers/generators/nanoid/tableid/provide.go.tmpl +++ b/internal/templates/tmpl/domain/providers/generators/nanoid/tableid/provide.go.tmpl @@ -8,7 +8,6 @@ import ( "{{.PackageName}}/pkg/generators/nanoid" "{{.PackageName}}/pkg/providers" "{{.PackageName}}/projects/framev2/pkg/stage" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) const Prv{{.DomainNamePascal}} = "pkg.generators.nanoid.tableid.{{.DomainNamePascal}}" @@ -19,9 +18,6 @@ var ( ) func Provide{{.DomainNamePascal}}(ctx context.Context, opts ...providers.Option) (any, error) { - _, span := tracer.BeginSubSegment(ctx, "pkg.generators.nanoid.tableid.Provide{{.DomainNamePascal}}") - defer span.Close(nil) - if stage.IsTest() { return provideTest(Prv{{.DomainNamePascal}}, daos.DBPrefix, opts...) } diff --git a/internal/templates/tmpl/domain/repository/dynamo/provider.go.tmpl b/internal/templates/tmpl/domain/repository/dynamo/provider.go.tmpl index 250d1b9..bc5094b 100644 --- a/internal/templates/tmpl/domain/repository/dynamo/provider.go.tmpl +++ b/internal/templates/tmpl/domain/repository/dynamo/provider.go.tmpl @@ -5,9 +5,8 @@ import ( "sync" "{{.PackageName}}/pkg/providers" - "{{.PackageName}}/projects/framev2/pkg/stage" dynamov2 "{{.PackageName}}/pkg/providers/dynamo/v2" - "{{.PackageName}}/projects/framev2/pkg/tracer" + "{{.PackageName}}/projects/framev2/pkg/stage" ) const PrvRepository = "domains.{{ .DomainNameLower }}.repository" @@ -18,9 +17,6 @@ var ( ) func Provide(ctx context.Context, opt ...providers.Option) (any, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.Provide") - defer span.Close(nil) - if stage.IsTest() { return provideTest(ctx, opt...) } @@ -28,7 +24,7 @@ func Provide(ctx context.Context, opt ...providers.Option) (any, error) { var err error once.Do(func() { - i, errCreate := createInstance(newCtx, opt...) + i, errCreate := createInstance(ctx, opt...) if errCreate != nil { err = errCreate return @@ -38,7 +34,6 @@ func Provide(ctx context.Context, opt ...providers.Option) (any, error) { }) if err != nil { - _ = span.AddError(err) return nil, err } @@ -66,12 +61,8 @@ func provideTest(ctx context.Context, opt ...providers.Option) (any, error) { } func createInstance(ctx context.Context, opts ...providers.Option) (*Repository, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.createInstance") - defer span.Close(nil) - - dynamoDriver, err := providers.As[Dynamo](newCtx, dynamov2.Provide, opts...) + dynamoDriver, err := providers.As[Dynamo](ctx, dynamov2.Provide, opts...) if err != nil { - _ = span.AddError(err) return nil, err } diff --git a/internal/templates/tmpl/domain/repository/postgres/create.go.tmpl b/internal/templates/tmpl/domain/repository/postgres/create.go.tmpl index 3e242c7..24dcf44 100644 --- a/internal/templates/tmpl/domain/repository/postgres/create.go.tmpl +++ b/internal/templates/tmpl/domain/repository/postgres/create.go.tmpl @@ -7,19 +7,13 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/repository/daos" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) func (r *Repository) Create(ctx context.Context, entity domain.{{.DomainNamePascal}}) (domain.{{.DomainNamePascal}}, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.Create") - defer span.Close(nil) - if entity.ID == "" { id, err := r.genID.Generate() if err != nil { - err = errors.Join(domain.ErrUnexpected, err) - _ = span.AddError(err) - return domain.{{.DomainNamePascal}}{}, err + return domain.{{.DomainNamePascal}}{}, errors.Join(domain.ErrUnexpected, err) } entity.ID = id @@ -27,17 +21,13 @@ func (r *Repository) Create(ctx context.Context, entity domain.{{.DomainNamePasc dao := daos.FromModel(entity) - if dbErr := r.driver.GetClient().WithContext(newCtx).Create(&dao).Error; dbErr != nil { + if dbErr := r.driver.GetClient().WithContext(ctx).Create(&dao).Error; dbErr != nil { if strings.Contains(dbErr.Error(), "duplicate key") { - err := errors.Join(domain.ErrDuplicatedRecord, dbErr) - _ = span.AddError(err) - return domain.{{.DomainNamePascal}}{}, err + return domain.{{.DomainNamePascal}}{}, errors.Join(domain.ErrDuplicatedRecord, dbErr) } - err := errors.Join(domain.ErrUnexpected, dbErr) - _ = span.AddError(err) - return domain.{{.DomainNamePascal}}{}, err + return domain.{{.DomainNamePascal}}{}, errors.Join(domain.ErrUnexpected, dbErr) } - return r.Get(newCtx, entity.ID) + return r.Get(ctx, entity.ID) } diff --git a/internal/templates/tmpl/domain/repository/postgres/delete.go.tmpl b/internal/templates/tmpl/domain/repository/postgres/delete.go.tmpl index 3fc78ac..f9272ec 100644 --- a/internal/templates/tmpl/domain/repository/postgres/delete.go.tmpl +++ b/internal/templates/tmpl/domain/repository/postgres/delete.go.tmpl @@ -6,16 +6,11 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/repository/daos" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) func (r *Repository) Delete(ctx context.Context, id string) (domain.{{.DomainNamePascal}}, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.Delete") - defer span.Close(nil) - - entity, err := r.Get(newCtx, id) + entity, err := r.Get(ctx, id) if err != nil { - _ = span.AddError(err) return domain.{{.DomainNamePascal}}{}, err } @@ -26,10 +21,8 @@ func (r *Repository) Delete(ctx context.Context, id string) (domain.{{.DomainNam DeletedAt: &now, } - if err := r.driver.GetClient().Model(&daos.{{.DomainNamePascal}}{}).WithContext(newCtx).Where("id = ?", id).Updates(update).Error; err != nil { - dbErr := errors.Join(domain.ErrDeleteRecord, err) - _ = span.AddError(dbErr) - return domain.{{.DomainNamePascal}}{}, dbErr + if err := r.driver.GetClient().Model(&daos.{{.DomainNamePascal}}{}).WithContext(ctx).Where("id = ?", id).Updates(update).Error; err != nil { + return domain.{{.DomainNamePascal}}{}, errors.Join(domain.ErrDeleteRecord, err) } entity.DeletedAt = &now diff --git a/internal/templates/tmpl/domain/repository/postgres/get.go.tmpl b/internal/templates/tmpl/domain/repository/postgres/get.go.tmpl index 3624329..290c641 100644 --- a/internal/templates/tmpl/domain/repository/postgres/get.go.tmpl +++ b/internal/templates/tmpl/domain/repository/postgres/get.go.tmpl @@ -8,18 +8,14 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/repository/daos" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) func (r *Repository) Get(ctx context.Context, id string) (domain.{{.DomainNamePascal}}, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.Get") - defer span.Close(nil) - gormDB := r.driver.GetClient() dao := daos.{{.DomainNamePascal}}{} - err := gormDB.WithContext(newCtx). + err := gormDB.WithContext(ctx). Model(daos.{{.DomainNamePascal}}{}). Where("id = ? AND deleted_at IS NULL", id). First(&dao). @@ -27,14 +23,10 @@ func (r *Repository) Get(ctx context.Context, id string) (domain.{{.DomainNamePa if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - dbErr := errors.Join(domain.ErrRecordNotFound, err) - _ = span.AddError(dbErr) - return domain.{{.DomainNamePascal}}{}, dbErr + return domain.{{.DomainNamePascal}}{}, errors.Join(domain.ErrRecordNotFound, err) } - dbErr := errors.Join(domain.ErrUnexpected, err) - _ = span.AddError(dbErr) - return domain.{{.DomainNamePascal}}{}, dbErr + return domain.{{.DomainNamePascal}}{}, errors.Join(domain.ErrUnexpected, err) } return daos.ToModel(dao), nil diff --git a/internal/templates/tmpl/domain/repository/postgres/provide.go.tmpl b/internal/templates/tmpl/domain/repository/postgres/provide.go.tmpl index 11b530c..14c379e 100644 --- a/internal/templates/tmpl/domain/repository/postgres/provide.go.tmpl +++ b/internal/templates/tmpl/domain/repository/postgres/provide.go.tmpl @@ -10,7 +10,6 @@ import ( prvpostgres "{{.PackageName}}/pkg/providers/postgres" "{{.PackageName}}/projects/framev2/pkg/drivers/gorm" "{{.PackageName}}/projects/framev2/pkg/stage" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) const PrvRepo = "domains.{{ .DomainNameLower }}.repository" @@ -21,9 +20,6 @@ var ( ) func Provide(ctx context.Context, opt ...providers.Option) (any, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.Provide") - defer span.Close(nil) - if stage.IsTest() { return provideTest(ctx, opt...) } @@ -31,7 +27,7 @@ func Provide(ctx context.Context, opt ...providers.Option) (any, error) { var err error once.Do(func() { - i, errCreate := createInstance(newCtx, opt...) + i, errCreate := createInstance(ctx, opt...) if errCreate != nil { err = errCreate return @@ -41,7 +37,6 @@ func Provide(ctx context.Context, opt ...providers.Option) (any, error) { }) if err != nil { - _ = span.AddError(err) return nil, err } @@ -69,15 +64,11 @@ func provideTest(ctx context.Context, opt ...providers.Option) (any, error) { } func createInstance(ctx context.Context, opts ...providers.Option) (any, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.createInstance") - defer span.Close(nil) - - driver := providers.MustAs[gorm.Driver](newCtx, prvpostgres.Provide{{.DBProviderFuncName}}, opts...) + driver := providers.MustAs[gorm.Driver](ctx, prvpostgres.Provide{{.DBProviderFuncName}}, opts...) - clock := providers.MustAs[Clock](newCtx, prvdatetime.ProvideClock, opts...) + clock := providers.MustAs[Clock](ctx, prvdatetime.ProvideClock, opts...) - generatorID := providers.MustAs[GeneratorID](newCtx, prvgenerator.Provide{{.DomainNamePascal}}, opts...) + generatorID := providers.MustAs[GeneratorID](ctx, prvgenerator.Provide{{.DomainNamePascal}}, opts...) return New(driver, clock, generatorID), nil } - diff --git a/internal/templates/tmpl/domain/repository/postgres/search.go.tmpl b/internal/templates/tmpl/domain/repository/postgres/search.go.tmpl index 66eec30..9611d56 100644 --- a/internal/templates/tmpl/domain/repository/postgres/search.go.tmpl +++ b/internal/templates/tmpl/domain/repository/postgres/search.go.tmpl @@ -7,31 +7,25 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain/options" "{{.PackageName}}/{{ .DomainPath }}/repository/builders" "{{.PackageName}}/{{ .DomainPath }}/repository/daos" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) // Search finds records for the search options. func (r *Repository) Search(ctx context.Context, opts options.SearchOptions) ([]domain.{{.DomainNamePascal}}, int64, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.Search") - defer span.Close(nil) - var daoModels []daos.{{.DomainNamePascal}} client := r.driver.GetClient().Model(&daoModels) client = builders.SearchOptions(client, opts) - client = client.WithContext(newCtx) + client = client.WithContext(ctx) err := client.Find(&daoModels).Error if err != nil { - _ = span.AddError(err) return nil, 0, err } var total int64 if opts.Pagination.Limit != 1 { if err := client.Offset(-1).Count(&total).Error; err != nil { - _ = span.AddError(err) return nil, 0, err } } diff --git a/internal/templates/tmpl/domain/repository/postgres/search_one.go.tmpl b/internal/templates/tmpl/domain/repository/postgres/search_one.go.tmpl index 43aff3b..4f2a401 100644 --- a/internal/templates/tmpl/domain/repository/postgres/search_one.go.tmpl +++ b/internal/templates/tmpl/domain/repository/postgres/search_one.go.tmpl @@ -5,28 +5,21 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/domain/options" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) // SearchOne finds the first record for the search options. func (r *Repository) SearchOne(ctx context.Context, filters options.SearchFilters) (domain.{{.DomainNamePascal}}, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.SearchOne") - defer span.Close(nil) - opts := options.NewSearchOptions(). SetFilters(filters). Limit(1) - list, _, err := r.Search(newCtx, opts) + list, _, err := r.Search(ctx, opts) if err != nil { - _ = span.AddError(err) return domain.{{.DomainNamePascal}}{}, err } if len(list) == 0 { - err := domain.ErrRecordNotFound - _ = span.AddError(err) - return domain.{{.DomainNamePascal}}{}, err + return domain.{{.DomainNamePascal}}{}, domain.ErrRecordNotFound } return list[0], nil diff --git a/internal/templates/tmpl/domain/repository/postgres/update.go.tmpl b/internal/templates/tmpl/domain/repository/postgres/update.go.tmpl index 31e9fc3..1d68343 100644 --- a/internal/templates/tmpl/domain/repository/postgres/update.go.tmpl +++ b/internal/templates/tmpl/domain/repository/postgres/update.go.tmpl @@ -6,16 +6,11 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/domain/options" "{{.PackageName}}/{{ .DomainPath }}/repository/daos" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) func (r *Repository) Update(ctx context.Context, id string, fields options.UpdateFields) (domain.{{.DomainNamePascal}}, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.repository.Update") - defer span.Close(nil) - - entity, err := r.Get(newCtx, id) + entity, err := r.Get(ctx, id) if err != nil { - _ = span.AddError(err) return domain.{{.DomainNamePascal}}{}, err } @@ -23,10 +18,9 @@ func (r *Repository) Update(ctx context.Context, id string, fields options.Updat dao := daos.FromModel(entity) - if err := r.driver.GetClient().Model(&dao).WithContext(newCtx).Updates(updated).Error; err != nil { - _ = span.AddError(err) + if err := r.driver.GetClient().Model(&dao).WithContext(ctx).Updates(updated).Error; err != nil { return domain.{{.DomainNamePascal}}{}, err } - return r.Get(newCtx, id) + return r.Get(ctx, id) } diff --git a/internal/templates/tmpl/domain/service/dynamo/provider.go.tmpl b/internal/templates/tmpl/domain/service/dynamo/provider.go.tmpl index 68a65b9..efd43ed 100644 --- a/internal/templates/tmpl/domain/service/dynamo/provider.go.tmpl +++ b/internal/templates/tmpl/domain/service/dynamo/provider.go.tmpl @@ -7,7 +7,6 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/repository" "{{.PackageName}}/pkg/providers" "{{.PackageName}}/projects/framev2/pkg/stage" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) const PrvService = "domains.{{ .DomainNameLower }}.service" @@ -18,9 +17,6 @@ var ( ) func Provide(ctx context.Context, opt ...providers.Option) (any, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.service.Provide") - defer span.Close(nil) - if stage.IsTest() { return provideTestService(ctx, opt...) } @@ -28,7 +24,7 @@ func Provide(ctx context.Context, opt ...providers.Option) (any, error) { var err error once.Do(func() { - i, errCreate := createInstance(newCtx, opt...) + i, errCreate := createInstance(ctx, opt...) if errCreate != nil { err = errCreate return @@ -38,7 +34,6 @@ func Provide(ctx context.Context, opt ...providers.Option) (any, error) { }) if err != nil { - _ = span.AddError(err) return nil, err } @@ -66,10 +61,7 @@ func provideTestService(ctx context.Context, opt ...providers.Option) (any, erro } func createInstance(ctx context.Context, opts ...providers.Option) (*Service, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.service.createInstance") - defer span.Close(nil) - - repo := providers.MustAs[Repository](newCtx, repository.Provide, opts...) + repo := providers.MustAs[Repository](ctx, repository.Provide, opts...) return New(repo), nil } diff --git a/internal/templates/tmpl/domain/service/postgres/create.go.tmpl b/internal/templates/tmpl/domain/service/postgres/create.go.tmpl index 21b98dc..858b958 100644 --- a/internal/templates/tmpl/domain/service/postgres/create.go.tmpl +++ b/internal/templates/tmpl/domain/service/postgres/create.go.tmpl @@ -4,16 +4,21 @@ import ( "context" "{{.PackageName}}/{{ .DomainPath }}/domain" - "{{.PackageName}}/projects/framev2/pkg/tracer" + oteltracer "{{.PackageName}}/pkg/otel/tracer" ) func (s *Service) Create(ctx context.Context, m domain.{{.DomainNamePascal}}) (domain.{{.DomainNamePascal}}, error) { - newCtx, seg := tracer.BeginSubSegment(ctx, "domains.{{.DomainNameLower}}.service.Create") - defer seg.Close(nil) + ctx, span := oteltracer.StartSpan(ctx, oteltracer.ServiceSpanName("{{.DomainNameLower}}", "create"), + oteltracer.WithAttributes( + oteltracer.Domain("{{.DomainNameLower}}"), + oteltracer.Layer(oteltracer.LayerService), + ), + ) + defer span.End() - created, err := s.repo.Create(newCtx, m) + created, err := s.repo.Create(ctx, m) if err != nil { - _ = seg.AddError(err) + oteltracer.SetError(ctx, err) return domain.{{.DomainNamePascal}}{}, err } diff --git a/internal/templates/tmpl/domain/service/postgres/delete.go.tmpl b/internal/templates/tmpl/domain/service/postgres/delete.go.tmpl index f2b7a49..5b2b511 100644 --- a/internal/templates/tmpl/domain/service/postgres/delete.go.tmpl +++ b/internal/templates/tmpl/domain/service/postgres/delete.go.tmpl @@ -4,16 +4,21 @@ import ( "context" "{{.PackageName}}/{{ .DomainPath }}/domain" - "{{.PackageName}}/projects/framev2/pkg/tracer" + oteltracer "{{.PackageName}}/pkg/otel/tracer" ) func (s *Service) Delete(ctx context.Context, id string) (domain.{{.DomainNamePascal}}, error) { - newCtx, seg := tracer.BeginSubSegment(ctx, "domains.{{.DomainNameLower}}.service.Delete") - defer seg.Close(nil) + ctx, span := oteltracer.StartSpan(ctx, oteltracer.ServiceSpanName("{{.DomainNameLower}}", "delete"), + oteltracer.WithAttributes( + oteltracer.Domain("{{.DomainNameLower}}"), + oteltracer.Layer(oteltracer.LayerService), + ), + ) + defer span.End() - deleted, err := s.repo.Delete(newCtx, id) + deleted, err := s.repo.Delete(ctx, id) if err != nil { - _ = seg.AddError(err) + oteltracer.SetError(ctx, err) return domain.{{.DomainNamePascal}}{}, err } diff --git a/internal/templates/tmpl/domain/service/postgres/get.go.tmpl b/internal/templates/tmpl/domain/service/postgres/get.go.tmpl index 1a63799..ae2b16f 100644 --- a/internal/templates/tmpl/domain/service/postgres/get.go.tmpl +++ b/internal/templates/tmpl/domain/service/postgres/get.go.tmpl @@ -4,16 +4,21 @@ import ( "context" "{{.PackageName}}/{{ .DomainPath }}/domain" - "{{.PackageName}}/projects/framev2/pkg/tracer" + oteltracer "{{.PackageName}}/pkg/otel/tracer" ) func (s *Service) Get(ctx context.Context, id string) (domain.{{.DomainNamePascal}}, error) { - newCtx, seg := tracer.BeginSubSegment(ctx, "domains.{{.DomainNameLower}}.service.Get") - defer seg.Close(nil) + ctx, span := oteltracer.StartSpan(ctx, oteltracer.ServiceSpanName("{{.DomainNameLower}}", "get"), + oteltracer.WithAttributes( + oteltracer.Domain("{{.DomainNameLower}}"), + oteltracer.Layer(oteltracer.LayerService), + ), + ) + defer span.End() - item, err := s.repo.Get(newCtx, id) + item, err := s.repo.Get(ctx, id) if err != nil { - _ = seg.AddError(err) + oteltracer.SetError(ctx, err) return domain.{{.DomainNamePascal}}{}, err } diff --git a/internal/templates/tmpl/domain/service/postgres/provide.go.tmpl b/internal/templates/tmpl/domain/service/postgres/provide.go.tmpl index 32ac7ed..45b9c26 100644 --- a/internal/templates/tmpl/domain/service/postgres/provide.go.tmpl +++ b/internal/templates/tmpl/domain/service/postgres/provide.go.tmpl @@ -7,7 +7,6 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/repository" "{{.PackageName}}/pkg/providers" "{{.PackageName}}/projects/framev2/pkg/stage" - "{{.PackageName}}/projects/framev2/pkg/tracer" ) const PrvService = "domains.{{ .DomainNameLower }}.service" @@ -18,9 +17,6 @@ var ( ) func Provide(ctx context.Context, opts ...providers.Option) (any, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.service.Provide") - defer span.Close(nil) - if stage.IsTest() { return provideTest(ctx, opts...) } @@ -28,7 +24,7 @@ func Provide(ctx context.Context, opts ...providers.Option) (any, error) { var err error once.Do(func() { - i, errCreate := createInstance(newCtx, opts...) + i, errCreate := createInstance(ctx, opts...) if errCreate != nil { err = errCreate return @@ -38,7 +34,6 @@ func Provide(ctx context.Context, opts ...providers.Option) (any, error) { }) if err != nil { - _ = span.AddError(err) return nil, err } @@ -66,11 +61,7 @@ func provideTest(ctx context.Context, opts ...providers.Option) (any, error) { } func createInstance(ctx context.Context, opts ...providers.Option) (*Service, error) { - newCtx, span := tracer.BeginSubSegment(ctx, "domains.{{ .DomainNameLower }}.service.createInstance") - defer span.Close(nil) - - repo := providers.MustAs[Repository](newCtx, repository.Provide, opts...) + repo := providers.MustAs[Repository](ctx, repository.Provide, opts...) return New(repo), nil } - diff --git a/internal/templates/tmpl/domain/service/postgres/search.go.tmpl b/internal/templates/tmpl/domain/service/postgres/search.go.tmpl index 1f0fb6a..8d49371 100644 --- a/internal/templates/tmpl/domain/service/postgres/search.go.tmpl +++ b/internal/templates/tmpl/domain/service/postgres/search.go.tmpl @@ -5,16 +5,21 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/domain/options" - "{{.PackageName}}/projects/framev2/pkg/tracer" + oteltracer "{{.PackageName}}/pkg/otel/tracer" ) func (s *Service) Search(ctx context.Context, so options.SearchOptions) ([]domain.{{.DomainNamePascal}}, int64, error) { - newCtx, seg := tracer.BeginSubSegment(ctx, "domains.{{.DomainNameLower}}.service.Search") - defer seg.Close(nil) + ctx, span := oteltracer.StartSpan(ctx, oteltracer.ServiceSpanName("{{.DomainNameLower}}", "search"), + oteltracer.WithAttributes( + oteltracer.Domain("{{.DomainNameLower}}"), + oteltracer.Layer(oteltracer.LayerService), + ), + ) + defer span.End() - results, total, err := s.repo.Search(newCtx, so) + results, total, err := s.repo.Search(ctx, so) if err != nil { - _ = seg.AddError(err) + oteltracer.SetError(ctx, err) return nil, 0, err } diff --git a/internal/templates/tmpl/domain/service/postgres/search_one.go.tmpl b/internal/templates/tmpl/domain/service/postgres/search_one.go.tmpl index 9c9784e..1adb3bb 100644 --- a/internal/templates/tmpl/domain/service/postgres/search_one.go.tmpl +++ b/internal/templates/tmpl/domain/service/postgres/search_one.go.tmpl @@ -5,16 +5,21 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/domain/options" - "{{.PackageName}}/projects/framev2/pkg/tracer" + oteltracer "{{.PackageName}}/pkg/otel/tracer" ) func (s *Service) SearchOne(ctx context.Context, filters options.SearchFilters) (domain.{{.DomainNamePascal}}, error) { - newCtx, seg := tracer.BeginSubSegment(ctx, "domains.{{.DomainNameLower}}.service.SearchOne") - defer seg.Close(nil) + ctx, span := oteltracer.StartSpan(ctx, oteltracer.ServiceSpanName("{{.DomainNameLower}}", "search_one"), + oteltracer.WithAttributes( + oteltracer.Domain("{{.DomainNameLower}}"), + oteltracer.Layer(oteltracer.LayerService), + ), + ) + defer span.End() - result, err := s.repo.SearchOne(newCtx, filters) + result, err := s.repo.SearchOne(ctx, filters) if err != nil { - _ = seg.AddError(err) + oteltracer.SetError(ctx, err) return domain.{{.DomainNamePascal}}{}, err } diff --git a/internal/templates/tmpl/domain/service/postgres/update.go.tmpl b/internal/templates/tmpl/domain/service/postgres/update.go.tmpl index 47fd4ea..8f40e4b 100644 --- a/internal/templates/tmpl/domain/service/postgres/update.go.tmpl +++ b/internal/templates/tmpl/domain/service/postgres/update.go.tmpl @@ -5,16 +5,21 @@ import ( "{{.PackageName}}/{{ .DomainPath }}/domain" "{{.PackageName}}/{{ .DomainPath }}/domain/options" - "{{.PackageName}}/projects/framev2/pkg/tracer" + oteltracer "{{.PackageName}}/pkg/otel/tracer" ) func (s *Service) Update(ctx context.Context, id string, fields options.UpdateFields) (domain.{{.DomainNamePascal}}, error) { - newCtx, seg := tracer.BeginSubSegment(ctx, "domains.{{.DomainNameLower}}.service.Update") - defer seg.Close(nil) + ctx, span := oteltracer.StartSpan(ctx, oteltracer.ServiceSpanName("{{.DomainNameLower}}", "update"), + oteltracer.WithAttributes( + oteltracer.Domain("{{.DomainNameLower}}"), + oteltracer.Layer(oteltracer.LayerService), + ), + ) + defer span.End() - updated, err := s.repo.Update(newCtx, id, fields) + updated, err := s.repo.Update(ctx, id, fields) if err != nil { - _ = seg.AddError(err) + oteltracer.SetError(ctx, err) return domain.{{.DomainNamePascal}}{}, err } diff --git a/internal/templates/tmpl/sls/config/otel-layer/collector.yaml.tmpl b/internal/templates/tmpl/sls/config/otel-layer/collector.yaml.tmpl new file mode 100644 index 0000000..fd76b87 --- /dev/null +++ b/internal/templates/tmpl/sls/config/otel-layer/collector.yaml.tmpl @@ -0,0 +1,22 @@ +receivers: + otlp: + protocols: + http: + endpoint: localhost:4319 + +exporters: + otlphttp/datadog: + traces_endpoint: "https://otlp.${env:DD_SITE}/v1/traces" + metrics_endpoint: "https://otlp.${env:DD_SITE}/v1/metrics" + headers: + DD-API-KEY: "${env:DD_API_KEY}" + DD-OTEL-METRIC-CONFIG: '{"resource_attributes_as_tags": true}' + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [otlphttp/datadog] + metrics: + receivers: [otlp] + exporters: [otlphttp/datadog] diff --git a/internal/templates/tmpl/sls/config/sls/environment.yml.tmpl b/internal/templates/tmpl/sls/config/sls/environment.yml.tmpl index 06b48dc..3272706 100644 --- a/internal/templates/tmpl/sls/config/sls/environment.yml.tmpl +++ b/internal/templates/tmpl/sls/config/sls/environment.yml.tmpl @@ -6,18 +6,32 @@ CORS: true API_KEY: ${ssm:/service/auth/${self:custom.stage}/API_KEY} + DD_API_KEY: ${ssm:/service/datadog/${self:custom.stage}/API_KEY} + DD_SITE: datadoghq.com + DD_ENV: ${self:custom.stage} + DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT: "localhost:4318" + DD_SERVERLESS_LOGS_ENABLED: "true" + AWS_XRAY_CONTEXT_MISSING: IGNORE_ERROR + OTEL_TRACES_ENABLED: "true" + OTEL_METRICS_ENABLED: "true" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4318" + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "http://localhost:4319" + OPENTELEMETRY_COLLECTOR_CONFIG_URI: "/opt/collector.yaml" + feature: environment: <<: *default DEBUG: true + OTEL_TRACES_SAMPLE_RATE: "1.0" dev: environment: <<: *default DEBUG: false + OTEL_TRACES_SAMPLE_RATE: "1.0" prod: environment: <<: *default DEBUG: false - {{ if .HasSentry }}SENTRY_DSN: '{{ .SentryDSN }}'{{end}}{{ if not .HasSentry }}# SENTRY_DSN: 'Set sentry DSN here' {{ end }} + OTEL_TRACES_SAMPLE_RATE: "1.0" diff --git a/internal/templates/tmpl/sls/native/cron/handler/bootstrap.go.tmpl b/internal/templates/tmpl/sls/native/cron/handler/bootstrap.go.tmpl index e397e77..a034653 100644 --- a/internal/templates/tmpl/sls/native/cron/handler/bootstrap.go.tmpl +++ b/internal/templates/tmpl/sls/native/cron/handler/bootstrap.go.tmpl @@ -11,7 +11,11 @@ import ( "{{ .PackageName }}/pkg/config/loader/readers" "{{ .PackageName }}/pkg/lambda/decorators" "{{ .PackageName }}/pkg/lambda/decorators/health" + {{ if .UseOtel -}} + oteltracer "{{ .PackageName }}/pkg/otel/tracer" + {{- else -}} "{{ .PackageName }}/projects/framev2/pkg/tracer" + {{- end }} "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}cron/{{ .LambdaName }}/handler/worker" ) @@ -23,6 +27,31 @@ var embedFs embed.FS func Bootstrap() any { return decorators.Handle( func(ctx context.Context, event events.CloudWatchEvent) (error, error) { + {{ if .UseOtel -}} + ctx, span := oteltracer.StartSpan(ctx, oteltracer.HandlerSpanName("{{ .ServiceName }}", "{{ .LambdaName }}"), + oteltracer.WithAttributes( + oteltracer.Domain("{{ .ServiceName }}"), + oteltracer.Layer(oteltracer.LayerHandler), + ), + ) + defer span.End() + + loader.LoadConfigs(ctx, loader.WithReaders( + readers.Embed(embedFs, "embed/.app-config.yaml", yaml.Parser()), + )) + + r, err := worker.ProvideResources(ctx) + if err != nil { + oteltracer.SetError(ctx, err) + return nil, err + } + + if health.CheckHealthRequest(ctx) { + return nil, nil + } + + return worker.Worker(r)(ctx, event) + {{- else -}} newCtx, span := tracer.BeginSubSegment(ctx, "{{ .ServiceName }}.cron.{{ .LambdaName }}.handler.Bootstrap") defer span.Close(nil) @@ -41,6 +70,7 @@ func Bootstrap() any { } return worker.Worker(r)(newCtx, event) + {{- end }} }, ) } diff --git a/internal/templates/tmpl/sls/native/cron/lambda-config.yml.tmpl b/internal/templates/tmpl/sls/native/cron/lambda-config.yml.tmpl index 8737d1a..38cd45b 100644 --- a/internal/templates/tmpl/sls/native/cron/lambda-config.yml.tmpl +++ b/internal/templates/tmpl/sls/native/cron/lambda-config.yml.tmpl @@ -1,6 +1,9 @@ function: {{ .LambdaName }}: handler: {{if not .IsLegacy}}cmd/{{ end }}cron/{{ .LambdaName }}/main.go + {{- if .UseOtel }} + layers: ${self:custom.otelFunctionLayers} + {{- end }} disableLogs: true reservedConcurrency: ${self:custom.reservedConcurrency.{{ .ReservedConcurrency }}} events: diff --git a/internal/templates/tmpl/sls/native/custom/handler/bootstrap.go.tmpl b/internal/templates/tmpl/sls/native/custom/handler/bootstrap.go.tmpl index c0a807c..ffd3041 100644 --- a/internal/templates/tmpl/sls/native/custom/handler/bootstrap.go.tmpl +++ b/internal/templates/tmpl/sls/native/custom/handler/bootstrap.go.tmpl @@ -11,7 +11,11 @@ import ( "{{ .PackageName }}/pkg/lambda/decorators" "{{ .PackageName }}/pkg/lambda/decorators/health" "{{ .PackageName }}/pkg/providers" + {{ if .UseOtel -}} + oteltracer "{{ .PackageName }}/pkg/otel/tracer" + {{- else -}} "{{ .PackageName }}/projects/framev2/pkg/tracer" + {{- end }} "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}{{ .CustomTypePath }}/{{ .LambdaName }}/handler/worker" ) @@ -23,6 +27,27 @@ var embedFs embed.FS func Bootstrap() any { return decorators.Handle( func(ctx context.Context, input json.RawMessage) (any, error) { + {{ if .UseOtel -}} + ctx, span := oteltracer.StartSpan(ctx, oteltracer.HandlerSpanName("{{ .ServiceName }}", "{{ .LambdaName }}"), + oteltracer.WithAttributes( + oteltracer.Domain("{{ .ServiceName }}"), + oteltracer.Layer(oteltracer.LayerHandler), + ), + ) + defer span.End() + + loader.LoadConfigs(ctx, loader.WithReaders( + readers.Embed(embedFs, "embed/.app-config.yaml", yaml.Parser(), readers.IsRequired()), + )) + + r := providers.MustAs[*worker.Resources](ctx, worker.ProvideResources) + + if health.CheckHealthRequest(ctx) { + return nil, nil + } + + return worker.Worker(r)(ctx, input) + {{- else -}} newCtx, span := tracer.BeginSubSegment(ctx, "{{ .ServiceName }}.{{ .CustomTypePath }}.{{ .LambdaName }}.handler.Bootstrap") defer span.Close(nil) @@ -37,6 +62,7 @@ func Bootstrap() any { } return worker.Worker(r)(newCtx, input) + {{- end }} }, ) } diff --git a/internal/templates/tmpl/sls/native/custom/lambda-config.yml.tmpl b/internal/templates/tmpl/sls/native/custom/lambda-config.yml.tmpl index 0cafcf3..362c13f 100644 --- a/internal/templates/tmpl/sls/native/custom/lambda-config.yml.tmpl +++ b/internal/templates/tmpl/sls/native/custom/lambda-config.yml.tmpl @@ -1,6 +1,9 @@ function: {{ .LambdaName }}: handler: {{if not .IsLegacy}}cmd/{{ end }}{{ .CustomTypePath }}/{{ .LambdaName }}/main.go + {{- if .UseOtel }} + layers: ${self:custom.otelFunctionLayers} + {{- end }} disableLogs: true reservedConcurrency: ${self:custom.reservedConcurrency.{{ .ReservedConcurrency }}} # Example: Configure EventBridge bus event diff --git a/internal/templates/tmpl/sls/native/http/handler/bootstrap.go.tmpl b/internal/templates/tmpl/sls/native/http/handler/bootstrap.go.tmpl index 7430a3d..1cba77e 100644 --- a/internal/templates/tmpl/sls/native/http/handler/bootstrap.go.tmpl +++ b/internal/templates/tmpl/sls/native/http/handler/bootstrap.go.tmpl @@ -15,7 +15,11 @@ import ( "{{ .PackageName }}/pkg/lambda/decorators/health" "{{ .PackageName }}/pkg/lambda/decorators" "{{ .PackageName }}/projects/framev2/engine/http/middlewares" + {{ if .UseOtel -}} + oteltracer "{{ .PackageName }}/pkg/otel/tracer" + {{- else -}} "{{ .PackageName }}/projects/framev2/pkg/tracer" + {{- end }} "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}http/{{ .LambdaName }}/handler/worker" ) @@ -27,6 +31,37 @@ var embedFs embed.FS func Bootstrap() any { return decorators.Handle( func(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + {{ if .UseOtel -}} + ctx, span := oteltracer.StartSpan(ctx, oteltracer.HandlerSpanName("{{ .ServiceName }}", "{{ .LambdaName }}"), + oteltracer.WithAttributes( + oteltracer.Domain("{{ .ServiceName }}"), + oteltracer.Layer(oteltracer.LayerHandler), + ), + ) + defer span.End() + + loader.LoadConfigs(ctx, loader.WithReaders( + readers.Embed(embedFs, "embed/.app-config.yaml", yaml.Parser()), + )) + + r, err := worker.ProvideResources(ctx) + if err != nil { + oteltracer.SetError(ctx, err) + return events.APIGatewayV2HTTPResponse{ + StatusCode: http.StatusInternalServerError, + Body: `{"message": "error processing request", "error": "` + err.Error() + `" }`, + }, nil + } + + if health.CheckHealthRequest(ctx) { + return events.APIGatewayV2HTTPResponse{}, nil + } + + e := lecho.New() + e.{{ .HTTPMethod }}(worker.Path, worker.Worker(r), middlewares.AuthAPIKey()) + + return lhttp.Serve(ctx, event, e) + {{- else -}} newCtx, span := tracer.BeginSubSegment(ctx, "{{ .ServiceName }}.http.{{ .LambdaName }}.handler.Bootstrap") defer span.Close(nil) @@ -50,6 +85,7 @@ func Bootstrap() any { e.{{ .HTTPMethod }}(worker.Path, worker.Worker(r), middlewares.AuthAPIKey()) return lhttp.Serve(newCtx, event, e) + {{- end }} }, ) } diff --git a/internal/templates/tmpl/sls/native/http/lambda-config.yml.tmpl b/internal/templates/tmpl/sls/native/http/lambda-config.yml.tmpl index 393331b..6b9e3b3 100644 --- a/internal/templates/tmpl/sls/native/http/lambda-config.yml.tmpl +++ b/internal/templates/tmpl/sls/native/http/lambda-config.yml.tmpl @@ -1,6 +1,9 @@ function: {{ .LambdaName }}: handler: {{if not .IsLegacy}}cmd/{{ end }}http/{{ .LambdaName }}/main.go + {{- if .UseOtel }} + layers: ${self:custom.otelFunctionLayers} + {{- end }} disableLogs: true reservedConcurrency: ${self:custom.reservedConcurrency.{{ .ReservedConcurrency }}} events: diff --git a/internal/templates/tmpl/sls/native/plain/handler/bootstrap.go.tmpl b/internal/templates/tmpl/sls/native/plain/handler/bootstrap.go.tmpl index 4a692d3..58f0552 100644 --- a/internal/templates/tmpl/sls/native/plain/handler/bootstrap.go.tmpl +++ b/internal/templates/tmpl/sls/native/plain/handler/bootstrap.go.tmpl @@ -4,12 +4,16 @@ import ( "context" "embed" - "{{ .PackageName }}/config/loaders/parsers/yaml" + "{{ .PackageName }}/config/loaders/parsers/yaml" "{{ .PackageName }}/pkg/config/loader" "{{ .PackageName }}/pkg/config/loader/readers" "{{ .PackageName }}/pkg/lambda/decorators" "{{ .PackageName }}/pkg/lambda/decorators/health" - "{{ .PackageName }}/projects/framev2/pkg/tracer" + {{ if .UseOtel -}} + oteltracer "{{ .PackageName }}/pkg/otel/tracer" + {{- else -}} + "{{ .PackageName }}/projects/framev2/pkg/tracer" + {{- end }} "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}plain/{{ .LambdaName }}/handler/dtos" "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}plain/{{ .LambdaName }}/handler/worker" ) @@ -22,6 +26,31 @@ var embedFs embed.FS func Bootstrap() any { return decorators.Handle( func(ctx context.Context, input dtos.Input) (dtos.Output, error) { + {{ if .UseOtel -}} + ctx, span := oteltracer.StartSpan(ctx, oteltracer.HandlerSpanName("{{ .ServiceName }}", "{{ .LambdaName }}"), + oteltracer.WithAttributes( + oteltracer.Domain("{{ .ServiceName }}"), + oteltracer.Layer(oteltracer.LayerHandler), + ), + ) + defer span.End() + + loader.LoadConfigs(ctx, loader.WithReaders( + readers.Embed(embedFs, "embed/.app-config.yaml", yaml.Parser(), readers.IsRequired()), + )) + + r, err := worker.ProvideResources(ctx) + if err != nil { + oteltracer.SetError(ctx, err) + return dtos.Output{}, err + } + + if health.CheckHealthRequest(ctx) { + return dtos.Output{}, nil + } + + return worker.Worker(r)(ctx, input) + {{- else -}} newCtx, span := tracer.BeginSubSegment(ctx, "{{ .ServiceName }}.plain.{{ .LambdaName }}.handler.Bootstrap") defer span.Close(nil) @@ -35,11 +64,12 @@ func Bootstrap() any { return dtos.Output{}, err } - if health.CheckHealthRequest(newCtx) { - return dtos.Output{}, nil - } + if health.CheckHealthRequest(newCtx) { + return dtos.Output{}, nil + } return worker.Worker(r)(newCtx, input) + {{- end }} }, ) } diff --git a/internal/templates/tmpl/sls/native/plain/lambda-config.yml.tmpl b/internal/templates/tmpl/sls/native/plain/lambda-config.yml.tmpl index 3cae948..b71bf1f 100644 --- a/internal/templates/tmpl/sls/native/plain/lambda-config.yml.tmpl +++ b/internal/templates/tmpl/sls/native/plain/lambda-config.yml.tmpl @@ -1,5 +1,8 @@ function: {{ .LambdaName }}: handler: {{if not .IsLegacy}}cmd/{{ end }}plain/{{ .LambdaName }}/main.go + {{- if .UseOtel }} + layers: ${self:custom.otelFunctionLayers} + {{- end }} disableLogs: true reservedConcurrency: ${self:custom.reservedConcurrency.{{ .ReservedConcurrency }}} \ No newline at end of file diff --git a/internal/templates/tmpl/sls/native/snssqs/handler/bootstrap.go.tmpl b/internal/templates/tmpl/sls/native/snssqs/handler/bootstrap.go.tmpl index 324a96e..11324a9 100644 --- a/internal/templates/tmpl/sls/native/snssqs/handler/bootstrap.go.tmpl +++ b/internal/templates/tmpl/sls/native/snssqs/handler/bootstrap.go.tmpl @@ -16,7 +16,11 @@ import ( "{{ .PackageName }}/pkg/lambda/decorators" "{{ .PackageName }}/pkg/lambda/decorators/health" "{{ .PackageName }}/projects/framev2/pkg/publisher/types" + {{ if .UseOtel -}} + oteltracer "{{ .PackageName }}/pkg/otel/tracer" + {{- else -}} "{{ .PackageName }}/projects/framev2/pkg/tracer" + {{- end }} "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}snssqs/{{ .LambdaName }}/handler/dtos" "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}snssqs/{{ .LambdaName }}/handler/worker" ) @@ -28,6 +32,39 @@ var embedFs embed.FS func Bootstrap() any { return decorators.Handle( func(ctx context.Context, event events.SQSEvent) (events.SQSEventResponse, error) { + {{ if .UseOtel -}} + ctx, span := oteltracer.StartSpan(ctx, oteltracer.HandlerSpanName("{{ .ServiceName }}", "{{ .LambdaName }}"), + oteltracer.WithAttributes( + oteltracer.Domain("{{ .ServiceName }}"), + oteltracer.Layer(oteltracer.LayerHandler), + ), + ) + defer span.End() + + loader.LoadConfigs(ctx, loader.WithReaders( + readers.Embed(embedFs, "embed/.app-config.yaml", yaml.Parser()), + )) + + r, err := worker.ProvideResources(ctx) + if err != nil { + oteltracer.SetError(ctx, err) + return events.SQSEventResponse{}, err + } + + if health.CheckHealthRequest(ctx) { + return events.SQSEventResponse{}, nil + } + + errConsume := consumer.NewSqs(ctx, event, worker.Worker(r), sqs.WithSNS[types.Event[dtos.Event]]()). + WithWorkerDecorators(consumerdecorators.IdempotencyOnDeadline[types.Event[dtos.Event]]( + worker.Action, + r.GenerateIdempotencyKey, + r.DelIdempotency, + )). + Consume() + + return sqshelpers.Response(ctx, errConsume) + {{- else -}} newCtx, span := tracer.BeginSubSegment(ctx, "{{ .ServiceName }}.snssqs.{{ .LambdaName }}.handler.Bootstrap") defer span.Close(nil) @@ -54,6 +91,7 @@ func Bootstrap() any { Consume() return sqshelpers.Response(newCtx, errConsume) + {{- end }} }, ) } diff --git a/internal/templates/tmpl/sls/native/snssqs/lambda-config.yml.tmpl b/internal/templates/tmpl/sls/native/snssqs/lambda-config.yml.tmpl index b414767..e391411 100644 --- a/internal/templates/tmpl/sls/native/snssqs/lambda-config.yml.tmpl +++ b/internal/templates/tmpl/sls/native/snssqs/lambda-config.yml.tmpl @@ -1,6 +1,9 @@ function: {{ .LambdaName }}: handler: {{if not .IsLegacy}}cmd/{{ end }}snssqs/{{ .LambdaName }}/main.go + {{- if .UseOtel }} + layers: ${self:custom.otelFunctionLayers} + {{- end }} disableLogs: true reservedConcurrency: ${self:custom.reservedConcurrency.{{ .ReservedConcurrency }}} events: diff --git a/internal/templates/tmpl/sls/native/sqs/handler/bootstrap.go.tmpl b/internal/templates/tmpl/sls/native/sqs/handler/bootstrap.go.tmpl index 09de3f7..a56fea9 100644 --- a/internal/templates/tmpl/sls/native/sqs/handler/bootstrap.go.tmpl +++ b/internal/templates/tmpl/sls/native/sqs/handler/bootstrap.go.tmpl @@ -15,7 +15,11 @@ import ( "{{ .PackageName }}/pkg/providers" "{{ .PackageName }}/pkg/lambda/decorators" "{{ .PackageName }}/pkg/lambda/decorators/health" + {{ if .UseOtel -}} + oteltracer "{{ .PackageName }}/pkg/otel/tracer" + {{- else -}} "{{ .PackageName }}/projects/framev2/pkg/tracer" + {{- end }} "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}sqs/{{ .LambdaName }}/handler/dtos" "{{ .PackageName }}/{{ .ServicePath }}/{{ if not .IsLegacy }}cmd/{{ end }}sqs/{{ .LambdaName }}/handler/worker" ) @@ -28,6 +32,35 @@ var embedFs embed.FS func Bootstrap() any { return decorators.Handle( func(ctx context.Context, event events.SQSEvent) (events.SQSEventResponse, error) { + {{ if .UseOtel -}} + ctx, span := oteltracer.StartSpan(ctx, oteltracer.HandlerSpanName("{{ .ServiceName }}", "{{ .LambdaName }}"), + oteltracer.WithAttributes( + oteltracer.Domain("{{ .ServiceName }}"), + oteltracer.Layer(oteltracer.LayerHandler), + ), + ) + defer span.End() + + loader.LoadConfigs(ctx, loader.WithReaders( + readers.Embed(embedFs, "embed/.app-config.yaml", yaml.Parser()), + )) + + r := providers.MustAs[*worker.Resources](ctx, worker.ProvideResources) + + if health.CheckHealthRequest(ctx) { + return events.SQSEventResponse{}, nil + } + + errConsume := consumer.NewSqs(ctx, event, worker.Worker(r)). + WithWorkerDecorators(consumerdecorators.IdempotencyOnDeadline[dtos.Event]( + worker.Action, + r.GenerateIdempotencyKey, + r.DelIdempotency, + )). + Consume() + + return sqshelpers.Response(ctx, errConsume) + {{- else -}} newCtx, span := tracer.BeginSubSegment(ctx, "{{ .ServiceName }}.sqs.{{ .LambdaName }}.handler.Bootstrap") defer span.Close(nil) @@ -50,6 +83,7 @@ func Bootstrap() any { Consume() return sqshelpers.Response(newCtx, errConsume) + {{- end }} }, ) } diff --git a/internal/templates/tmpl/sls/native/sqs/lambda-config.yml.tmpl b/internal/templates/tmpl/sls/native/sqs/lambda-config.yml.tmpl index fdab825..2afaa4d 100644 --- a/internal/templates/tmpl/sls/native/sqs/lambda-config.yml.tmpl +++ b/internal/templates/tmpl/sls/native/sqs/lambda-config.yml.tmpl @@ -1,6 +1,9 @@ function: {{ .LambdaName }}: handler: {{if not .IsLegacy}}cmd/{{ end }}sqs/{{ .LambdaName }}/main.go + {{- if .UseOtel }} + layers: ${self:custom.otelFunctionLayers} + {{- end }} disableLogs: true reservedConcurrency: ${self:custom.reservedConcurrency.{{ .ReservedConcurrency }}} events: diff --git a/internal/templates/tmpl/sls/serverless.yml.tmpl b/internal/templates/tmpl/sls/serverless.yml.tmpl index 0b08c56..4c82736 100644 --- a/internal/templates/tmpl/sls/serverless.yml.tmpl +++ b/internal/templates/tmpl/sls/serverless.yml.tmpl @@ -4,7 +4,12 @@ frameworkVersion: '3' custom: stage: ${opt:stage, self:provider.stage} - datadog: ${file(../../config/sls/datadog.yml):datadog} + otelCollectorLayerArn: arn:aws:lambda:${self:provider.region}:901920570463:layer:aws-otel-collector-arm64-ver-0-117-0:1 + ddExtensionLayerArn: arn:aws:lambda:us-east-2:464622532012:layer:Datadog-Extension-ARM:96 + otelFunctionLayers: + - Ref: OtelCollectorConfigLambdaLayer + - ${self:custom.otelCollectorLayerArn} + - ${self:custom.ddExtensionLayerArn} pklConfig: ${file(../../config/sls/pkl-config.yml):config} reservedConcurrency: ${file(../../config/sls/concurrency.yml):${self:custom.stage}.reservedConcurrency} {{- if .WarmupEnabled }} @@ -24,14 +29,14 @@ custom: number: 5 go: - cmd: 'go build -ldflags="-s -w" -tags lambda.norpc' + cmd: 'go build -ldflags="-s -w" -tags "lambda.norpc,otel"' beforeBuild: - 'sh ../../scripts/build_pkl.sh "$PWD"' provider: name: aws architecture: arm64 - runtime: provided.al2 + runtime: provided.al2023 timeout: 30 stage: ${opt:stage, "dev"} region: ${opt:region, "us-east-2"} @@ -46,8 +51,15 @@ provider: metrics: true tracing: - apiGateway: true - lambda: true + apiGateway: false + lambda: false + +layers: + otelCollectorConfig: + path: config/otel-layer + name: ${self:service}-${self:custom.stage}-otel-collector-config + compatibleArchitectures: + - arm64 resources: ${file(./config/sls/resources.yml)} @@ -61,11 +73,10 @@ plugins: - serverless-plugin-lambda-insights - serverless-prune-plugin - serverless-plugin-s3-remover - - serverless-plugin-datadog - serverless-plugin-stage-reserved-concurrency {{- if .WarmupEnabled }} - serverless-plugin-warmup {{- end }} {{- if .CustomDomain }} - serverless-domain-manager - {{- end }} \ No newline at end of file + {{- end }}