diff --git a/cmd/service/service.go b/cmd/service/service.go index 1990798..37963aa 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -9,12 +9,14 @@ import ( "reflect" "sync" "syscall" + "time" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/alecthomas/kong" "github.com/apex/log" "github.com/apex/log/handlers/json" + "github.com/getsentry/sentry-go" sentryhttp "github.com/getsentry/sentry-go/http" "github.com/rs/cors" "github.com/sparetimecoders/goamqp" @@ -30,21 +32,30 @@ import ( "gitlab.com/unboundsoftware/schemas/store" ) -var CLI struct { +type CLI struct { AmqpURL string `name:"amqp-url" env:"AMQP_URL" help:"URL to use to connect to RabbitMQ" default:"amqp://user:password@localhost:5672/"` Port int `name:"port" env:"PORT" help:"Listen-port for GraphQL API" default:"8080"` APIKey string `name:"api-key" env:"API_KEY" help:"The API-key that is required"` LogLevel string `name:"log-level" env:"LOG_LEVEL" help:"The level of logging to use (debug, info, warn, error, fatal)" default:"info"` DatabaseURL string `name:"postgres-url" env:"POSTGRES_URL" help:"URL to use to connect to Postgres" default:"postgres://postgres:postgres@:5432/schemas?sslmode=disable"` DatabaseDriverName string `name:"db-driver" env:"DB_DRIVER" help:"Driver to use to connect to db" default:"postgres"` + SentryConfig } +type SentryConfig struct { + DSN string `name:"sentry-dsn" env:"SENTRY_DSN" help:"Sentry dsn" default:""` + Environment string `name:"sentry-environment" env:"SENTRY_ENVIRONMENT" help:"Sentry environment" default:"development"` +} + +var buildVersion = "none" + const serviceName = "schemas" func main() { - _ = kong.Parse(&CLI) + var cli CLI + _ = kong.Parse(&cli) log.SetHandler(json.New(os.Stdout)) - log.SetLevelFromString(CLI.LogLevel) + log.SetLevelFromString(cli.LogLevel) logger := log.WithField("service", serviceName) closeEvents := make(chan error) @@ -52,16 +63,22 @@ func main() { closeEvents, logger, ConnectAMQP, + cli, ); err != nil { logger.WithError(err).Error("process error") } } -func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url string) (Connection, error)) error { +func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url string) (Connection, error), cli CLI) error { + if err := setupSentry(logger, cli.SentryConfig); err != nil { + return err + } + defer sentry.Flush(2 * time.Second) + rootCtx, rootCancel := context.WithCancel(context.Background()) defer rootCancel() - db, err := store.SetupDB(CLI.DatabaseDriverName, CLI.DatabaseURL) + db, err := store.SetupDB(cli.DatabaseDriverName, cli.DatabaseURL) if err != nil { return fmt.Errorf("failed to setup DB: %v", err) } @@ -85,7 +102,7 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url return fmt.Errorf("failed to create event publisher: %v", err) } amqp.New(eventPublisher) - conn, err := connectToAmqpFunc(CLI.AmqpURL) + conn, err := connectToAmqpFunc(cli.AmqpURL) if err != nil { return fmt.Errorf("failed to connect to AMQP: %v", err) } @@ -121,7 +138,7 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url logger.Info("Started") mux := http.NewServeMux() - httpSrv := &http.Server{Addr: fmt.Sprintf(":%d", CLI.Port), Handler: mux} + httpSrv := &http.Server{Addr: fmt.Sprintf(":%d", cli.Port), Handler: mux} wg := sync.WaitGroup{} @@ -179,7 +196,7 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url Resolvers: resolver, Complexity: generated.ComplexityRoot{}, } - apiKeyMiddleware := middleware.NewApiKey(CLI.APIKey, logger) + apiKeyMiddleware := middleware.NewApiKey(cli.APIKey, logger) config.Directives.HasApiKey = apiKeyMiddleware.Directive srv := handler.NewDefaultServer(generated.NewExecutableSchema( config, @@ -187,10 +204,10 @@ func start(closeEvents chan error, logger *log.Entry, connectToAmqpFunc func(url sentryHandler := sentryhttp.New(sentryhttp.Options{Repanic: true}) mux.Handle("/", sentryHandler.HandleFunc(playground.Handler("GraphQL playground", "/query"))) - mux.Handle("/health", sentryHandler.HandleFunc(healthFunc)) + mux.Handle("/health", http.HandlerFunc(healthFunc)) mux.Handle("/query", cors.AllowAll().Handler(sentryHandler.Handle(apiKeyMiddleware.Handler(srv)))) - logger.Infof("connect to http://localhost:%d/ for GraphQL playground", CLI.Port) + logger.Infof("connect to http://localhost:%d/ for GraphQL playground", cli.Port) if err := httpSrv.ListenAndServe(); err != nil { logger.WithError(err).Error("listen http") @@ -206,6 +223,38 @@ func healthFunc(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("OK")) } +func setupSentry(logger log.Interface, args SentryConfig) error { + if args.Environment == "" { + return fmt.Errorf("no Sentry environment supplied, exiting") + } + cfg := sentry.ClientOptions{ + Dsn: args.DSN, + Environment: args.Environment, + Release: fmt.Sprintf("%s-%s", serviceName, buildVersion), + } + switch args.Environment { + case "development": + cfg.Debug = true + cfg.EnableTracing = false + cfg.TracesSampleRate = 0.0 + case "production": + if args.DSN == "" { + return fmt.Errorf("no DSN supplied for non-dev environment, exiting") + } + cfg.Debug = false + cfg.EnableTracing = true + cfg.TracesSampleRate = 1.0 + default: + return fmt.Errorf("illegal environment %s", args.Environment) + } + + if err := sentry.Init(cfg); err != nil { + return fmt.Errorf("sentry setup: %w", err) + } + logger.Infof("configured Sentry for env: %s", args.Environment) + return nil +} + func ConnectAMQP(url string) (Connection, error) { return goamqp.NewFromURL(serviceName, url) } diff --git a/k8s/secrets.yaml b/k8s/secrets.yaml index 92f2ab5..1b8fb66 100644 --- a/k8s/secrets.yaml +++ b/k8s/secrets.yaml @@ -14,6 +14,8 @@ spec: data: POSTGRES_URL: "postgres://{{ .DB_USERNAME }}:{{ .DB_PASSWORD }}@{{ .DB_HOST }}:{{ .DB_PORT }}/schemas?sslmode=disable" API_KEY: "{{ .API_KEY }}" + SENTRY_DSN: "{{ .SENTRY_DSN }}" + SENTRY_ENVIRONMENT: "{{ .SENTRY_ENVIRONMENT }}" dataFrom: - extract: key: services/schemas