diff --git a/lib/history/singleton.go b/lib/history/singleton.go new file mode 100644 index 0000000..a5c0c0c --- /dev/null +++ b/lib/history/singleton.go @@ -0,0 +1,67 @@ +package history + +import ( + "context" + "fmt" + "log/slog" + + "github.com/flachnetz/startup/v2/lib/events" + "github.com/flachnetz/startup/v2/lib/ql" + "github.com/flachnetz/startup/v2/startup_base" + "github.com/jackc/pgx/v5" +) + +// Options configures the global history Service set up by InitializeGlobal. +type Options struct { + // DB starts the transactions used to create the table and to write records. + DB ql.TxStarter + + // ServiceId identifies this service as the sender of the emitted events. + ServiceId string + // HistoryTable is the name of the table that history records are written to. + HistoryTable string + // EventCreator builds the event that is sent out for every tracked record. + EventCreator EventCreator +} + +// instance holds the global history Service initialized by InitializeGlobal. +var instance *Service + +// InitializeGlobal sets up the global history Service used by Track. It creates +// the history table (if it does not exist yet) and starts the background task +// that sends records tracked outside of a transaction. +func InitializeGlobal(ctx context.Context, opts Options) error { + err := ql.InNewTransaction(ctx, opts.DB, func(ctx ql.TxContext) error { + return CreateTable(ctx, opts.HistoryTable) + }) + + if err != nil { + return fmt.Errorf("create history table: %w", err) + } + + instance = New(opts.DB, pgx.Identifier{opts.HistoryTable}, &EventSending{ + EventSender: events.Sender, + EventCreator: opts.EventCreator, + ServiceId: opts.ServiceId, + ServiceVersion: startup_base.BuildVersion, + WriteToOutbox: true, + }) + + // start the background task that flushes records tracked outside of a + // transaction. Cancel ctx to stop it. + instance.SendAsync(ctx) + + return nil +} + +// Track uses the global history singleton. +// You need to initialize it using InitializeGlobal first. +func Track[T ~string](ctx context.Context, groupId T, item Item) { + if instance == nil { + // not initialized: just log the event instead of tracking it. + slog.WarnContext(ctx, "Tracking event", slog.String("event", item.HistoryString())) + return + } + + instance.Track(ctx, GroupId(groupId), item) +} diff --git a/startup_logging/resty.go b/startup_logging/resty.go new file mode 100644 index 0000000..0a8be38 --- /dev/null +++ b/startup_logging/resty.go @@ -0,0 +1,23 @@ +package sl + +import ( + "fmt" + "log/slog" +) + +// RestyAdapter bridges resty.Logger to *slog.Logger. +type RestyAdapter struct { + Logger *slog.Logger +} + +func (a RestyAdapter) Errorf(format string, v ...any) { + a.Logger.Error(fmt.Sprintf(format, v...)) +} + +func (a RestyAdapter) Warnf(format string, v ...any) { + a.Logger.Warn(fmt.Sprintf(format, v...)) +} + +func (a RestyAdapter) Debugf(format string, v ...any) { + a.Logger.Debug(fmt.Sprintf(format, v...)) +}