package gotenbergcmd import ( "context" "errors" "fmt" "os" "os/signal" "strings" "syscall" "time" flag "github.com/spf13/pflag" "golang.org/x/sync/errgroup" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" ) // See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg. // Credits: https://github.com/labstack/echo/blob/v4.3.0/echo.go#L240. const banner = ` _____ __ __ / ___/__ / /____ ___ / / ___ _______ _ / (_ / _ \/ __/ -_) _ \/ _ \/ -_) __/ _ '/ \___/\___/\__/\__/_//_/_.__/\__/_/ \_, / /___/ A Docker-based API for converting documents to PDF. Version: %s ------------------------------------------------------- ` // Version is the... version of the Gotenberg application. We set it at the // build stage of the Docker image. var Version = "snapshot" // Run starts the Gotenberg application. Call this in the main of your program. func Run() { gotenberg.Version = Version // Create the root FlagSet and adds the modules flags to it. fs := flag.NewFlagSet("gotenberg", flag.ExitOnError) fs.Bool("gotenberg-hide-banner", false, "Hide the banner") fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration") fs.Bool("gotenberg-build-debug-data", true, "Set if build data is needed") // Logging & telemetry flags. fs.String("log-level", gotenberg.InfoLoggingLevel, "Set the log level") fs.String("log-fields-prefix", "", "Prepend a specified prefix to each log field key") fs.String("log-std-format", gotenberg.AutoLoggingFormat, "Set the log format for standard output") fs.Bool("log-std-enable-gcp-fields", false, "Use GCP-compatible field names in log output") fs.String("log-std-level-case", gotenberg.LowerLevelCase, "Set the case of the level field in the standard output, either lower or upper") // Deprecated logging flags. fs.String("log-format", gotenberg.AutoLoggingFormat, "Set the log format") fs.Bool("log-enable-gcp-fields", false, "Use GCP-compatible field names") if err := errors.Join( fs.MarkDeprecated("log-format", "use --log-std-format instead"), fs.MarkDeprecated("log-enable-gcp-fields", "use --log-std-enable-gcp-fields instead"), ); err != nil { fmt.Printf("[FATAL] mark deprecated flags: %s\n", err) os.Exit(1) } descriptors := gotenberg.GetModuleDescriptors() var modsInfo strings.Builder for _, desc := range descriptors { fs.AddFlagSet(desc.FlagSet) modsInfo.WriteString(desc.ID + " ") } // Parse the flags. err := fs.Parse(os.Args[1:]) if err != nil { fmt.Println(err) os.Exit(1) } // Override their values if the corresponding environment variables are // set. fs.VisitAll(func(f *flag.Flag) { envName := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_")) val, ok := os.LookupEnv(envName) if !ok { return } sliceVal, ok := f.Value.(flag.SliceValue) if ok { // We don't want to append the values (default pflag behavior). items := strings.Split(val, ",") err = sliceVal.Replace(items) if err != nil { fmt.Printf("[FATAL] invalid overriding value '%s' from %s: %v\n", val, envName, err) os.Exit(1) } f.Changed = true return } err = fs.Set(f.Name, val) if err != nil { fmt.Printf("[FATAL] invalid overriding value '%s' from %s: %v\n", val, envName, err) os.Exit(1) } }) // Create a wrapper around our flags. parsedFlags := gotenberg.ParsedFlags{FlagSet: fs} hideBanner := parsedFlags.MustBool("gotenberg-hide-banner") gracefulShutdownDuration := parsedFlags.MustDuration("gotenberg-graceful-shutdown-duration") // Initialize telemetry (logging + OTEL). serviceName := os.Getenv("OTEL_SERVICE_NAME") if serviceName == "" { serviceName = "gotenberg" } telemetryCfg := gotenberg.TelemetryConfig{ ServiceName: serviceName, ServiceVersion: Version, LogLevel: parsedFlags.MustDeprecatedString("log-format", "log-std-format"), LogFieldsPrefix: parsedFlags.MustString("log-fields-prefix"), LogStdFormat: parsedFlags.MustDeprecatedString("log-format", "log-std-format"), LogStdEnableGcpFields: parsedFlags.MustDeprecatedBool("log-enable-gcp-fields", "log-std-enable-gcp-fields"), LogStdLevelCase: parsedFlags.MustString("log-std-level-case"), } // LogLevel uses its own flag, not the format flag. telemetryCfg.LogLevel = parsedFlags.MustString("log-level") err = telemetryCfg.Validate() if err != nil { fmt.Printf("[FATAL] invalid telemetry config: %s\n", err) os.Exit(1) } shutdownTelemetry, err := gotenberg.StartTelemetry(telemetryCfg) if err != nil { fmt.Printf("[FATAL] start telemetry: %s\n", err) os.Exit(1) } if !hideBanner { fmt.Printf(banner, Version) } fmt.Printf("[SYSTEM] modules: %s\n", modsInfo.String()) ctx := gotenberg.NewContext(parsedFlags, descriptors) // Start application modules. apps, err := ctx.Modules(new(gotenberg.App)) if err != nil { fmt.Printf("[FATAL] %s\n", err) os.Exit(1) } for _, a := range apps { go func(app gotenberg.App) { id := app.(gotenberg.Module).Descriptor().ID err = app.Start() if err != nil { fmt.Printf("[FATAL] starting %s: %s\n", id, err) os.Exit(1) } startupMessage := app.StartupMessage() if startupMessage == "" { fmt.Printf("[SYSTEM] %s: application started\n", id) return } fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage) }(a.(gotenberg.App)) } // Get modules that want to print system messages. sysLoggers, err := ctx.Modules(new(gotenberg.SystemLogger)) if err != nil { fmt.Printf("[FATAL] %s\n", err) os.Exit(1) } for _, l := range sysLoggers { go func(logger gotenberg.SystemLogger) { id := logger.(gotenberg.Module).Descriptor().ID for _, message := range logger.SystemMessages() { fmt.Printf("[SYSTEM] %s: %s\n", id, message) } }(l.(gotenberg.SystemLogger)) } if parsedFlags.MustBool("gotenberg-build-debug-data") { // Build the debug data. gotenberg.BuildDebug(ctx) } quit := make(chan os.Signal, 1) // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) or SIGTERM (Kubernetes). signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // Block until we receive our signal. <-quit gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration) defer cancel() forceQuit := make(chan os.Signal, 1) signal.Notify(forceQuit, syscall.SIGINT) go func() { // In case of force quit, cancel the context. <-forceQuit cancel() }() fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration) eg, _ := errgroup.WithContext(gracefulShutdownCtx) for _, a := range apps { eg.Go(func(app gotenberg.App) func() error { return func() error { id := app.(gotenberg.Module).Descriptor().ID err = app.Stop(gracefulShutdownCtx) if errors.Is(err, gotenberg.ErrCancelGracefulShutdownContext) { cancel() } else if err != nil { return fmt.Errorf("stopping %s: %w", id, err) } fmt.Printf("[SYSTEM] %s: application stopped\n", id) return nil } }(a.(gotenberg.App))) } err = eg.Wait() if err != nil { cancel() fmt.Printf("[FATAL] %v\n", err) os.Exit(1) //nolint:gocritic // defers are already called explicitly above } // Shutdown telemetry (flush spans, metrics, logs). err = shutdownTelemetry(gracefulShutdownCtx) if err != nil { cancel() fmt.Printf("[FATAL] %v\n", err) os.Exit(1) //nolint:gocritic // defers are already called explicitly above } os.Exit(0) }