mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-30 19:59:48 +02:00
- Update Next.js build IDs and chunk hashes in workflow upload static files - Replace deprecated 'input' field syntax with new exports-style variables in event triggers (e.g., 'Target: event_data.value' instead of type/field/name) - Simplify parameter override syntax in test workflows (remove nested 'default' structure)
649 lines
18 KiB
Go
649 lines
18 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/j3ssie/osmedeus/v5/internal/config"
|
|
"github.com/j3ssie/osmedeus/v5/internal/core"
|
|
"github.com/j3ssie/osmedeus/v5/internal/database"
|
|
"github.com/j3ssie/osmedeus/v5/internal/database/repository"
|
|
"github.com/j3ssie/osmedeus/v5/internal/executor"
|
|
oslogger "github.com/j3ssie/osmedeus/v5/internal/logger"
|
|
"github.com/j3ssie/osmedeus/v5/internal/parser"
|
|
"github.com/j3ssie/osmedeus/v5/internal/scheduler"
|
|
"github.com/j3ssie/osmedeus/v5/internal/terminal"
|
|
"github.com/j3ssie/osmedeus/v5/pkg/server/handlers"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// EventReceiver manages event-triggered workflows in server mode.
|
|
// It loads all workflows with event triggers at startup and executes
|
|
// them when matching events are received through the scheduler.
|
|
type EventReceiver struct {
|
|
scheduler *scheduler.Scheduler
|
|
loader *parser.Loader
|
|
config *config.Config
|
|
logger *zap.Logger
|
|
workflows []*core.Workflow
|
|
triggers []*RegisteredEventTrigger
|
|
mu sync.RWMutex
|
|
running bool
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// RegisteredEventTrigger holds workflow and trigger info for event-triggered workflows
|
|
type RegisteredEventTrigger struct {
|
|
Workflow *core.Workflow
|
|
Trigger *core.Trigger
|
|
}
|
|
|
|
// NewEventReceiver creates a new event receiver for the server.
|
|
// It initializes the scheduler and workflow loader but does not start
|
|
// listening for events until Start() is called.
|
|
func NewEventReceiver(cfg *config.Config) (*EventReceiver, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("config is required")
|
|
}
|
|
|
|
sched, err := scheduler.NewScheduler()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create scheduler: %w", err)
|
|
}
|
|
|
|
loader := parser.NewLoader(cfg.WorkflowsPath)
|
|
|
|
return &EventReceiver{
|
|
scheduler: sched,
|
|
loader: loader,
|
|
config: cfg,
|
|
logger: oslogger.Get(),
|
|
workflows: make([]*core.Workflow, 0),
|
|
triggers: make([]*RegisteredEventTrigger, 0),
|
|
}, nil
|
|
}
|
|
|
|
// Start loads all event-triggered workflows and begins listening for events.
|
|
// It scans the workflows directory, filters to only workflows with enabled
|
|
// event triggers, and registers them with the scheduler.
|
|
func (er *EventReceiver) Start(ctx context.Context) error {
|
|
er.mu.Lock()
|
|
defer er.mu.Unlock()
|
|
|
|
if er.running {
|
|
return nil
|
|
}
|
|
|
|
// Create cancellable context for workflow executions
|
|
er.ctx, er.cancel = context.WithCancel(ctx)
|
|
|
|
// Load all workflows
|
|
allWorkflows, err := er.loader.LoadAllWorkflows()
|
|
if err != nil {
|
|
er.logger.Warn("Failed to load some workflows", zap.Error(err))
|
|
// Continue with partial results
|
|
}
|
|
|
|
// Filter to only workflows with event triggers
|
|
var workflowCount, triggerCount int
|
|
for _, wf := range allWorkflows {
|
|
eventTriggers := wf.GetEventTriggers()
|
|
if len(eventTriggers) == 0 {
|
|
continue
|
|
}
|
|
|
|
er.workflows = append(er.workflows, wf)
|
|
workflowCount++
|
|
|
|
// Register each event trigger
|
|
for i := range eventTriggers {
|
|
trigger := &eventTriggers[i]
|
|
if err := er.scheduler.RegisterTrigger(wf, trigger); err != nil {
|
|
er.logger.Warn("Failed to register event trigger",
|
|
zap.String("workflow", wf.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.Error(err),
|
|
)
|
|
continue
|
|
}
|
|
|
|
er.triggers = append(er.triggers, &RegisteredEventTrigger{
|
|
Workflow: wf,
|
|
Trigger: trigger,
|
|
})
|
|
triggerCount++
|
|
|
|
er.logger.Debug("Registered event trigger",
|
|
zap.String("workflow", wf.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.String("topic", trigger.Event.Topic),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Set handler for event triggers (use SetEventHandler to receive full event envelope)
|
|
er.scheduler.SetEventHandler(string(core.TriggerEvent), er.handleEventTriggerWithEnvelope)
|
|
|
|
// Set handler for cron triggers
|
|
er.scheduler.SetHandler(string(core.TriggerCron), er.handleCronTrigger)
|
|
|
|
// Set handler for watch triggers
|
|
er.scheduler.SetHandler(string(core.TriggerWatch), er.handleWatchTrigger)
|
|
|
|
// Start the scheduler
|
|
if err := er.scheduler.Start(); err != nil {
|
|
return fmt.Errorf("failed to start scheduler: %w", err)
|
|
}
|
|
|
|
er.running = true
|
|
|
|
er.logger.Debug("Event receiver started",
|
|
zap.Int("workflows", workflowCount),
|
|
zap.Int("triggers", triggerCount),
|
|
)
|
|
|
|
// Process any queued events from the database asynchronously
|
|
go er.processQueuedEvents()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully stops the event receiver and its scheduler.
|
|
func (er *EventReceiver) Stop() error {
|
|
er.mu.Lock()
|
|
defer er.mu.Unlock()
|
|
|
|
if !er.running {
|
|
return nil
|
|
}
|
|
|
|
// Cancel any running workflow executions
|
|
if er.cancel != nil {
|
|
er.cancel()
|
|
}
|
|
|
|
// Stop the scheduler
|
|
if err := er.scheduler.Stop(); err != nil {
|
|
er.logger.Warn("Error stopping scheduler", zap.Error(err))
|
|
}
|
|
|
|
er.running = false
|
|
er.logger.Info("Event receiver stopped")
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleEventTriggerWithEnvelope is called when an event matches a trigger.
|
|
// It receives the full event envelope JSON and resolved variables (from the new Vars syntax).
|
|
func (er *EventReceiver) handleEventTriggerWithEnvelope(workflow *core.Workflow, trigger *core.Trigger, input string, eventEnvelope string, resolvedVars map[string]string) error {
|
|
er.logger.Info("Event trigger fired",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.String("input", input),
|
|
)
|
|
er.logger.Debug("Event envelope received",
|
|
zap.String("event_envelope", eventEnvelope),
|
|
)
|
|
if resolvedVars != nil {
|
|
er.logger.Debug("Resolved vars from Vars syntax",
|
|
zap.Any("resolved_vars", resolvedVars),
|
|
)
|
|
}
|
|
|
|
// Print resolved variables to console
|
|
if len(resolvedVars) > 0 {
|
|
printer := terminal.NewPrinter()
|
|
printer.EventTriggerInfo(trigger.Name, workflow.Name, resolvedVars)
|
|
}
|
|
|
|
// Execute workflow in goroutine to not block the scheduler
|
|
go func() {
|
|
// Build params from trigger input
|
|
params := make(map[string]string)
|
|
|
|
// If this trigger is linked to a Schedule record, look it up to get Target/Params first
|
|
if trigger.ScheduleID != "" {
|
|
schedule, err := database.GetScheduleByID(er.ctx, trigger.ScheduleID)
|
|
if err != nil {
|
|
er.logger.Warn("Failed to load schedule for event trigger",
|
|
zap.String("schedule_id", trigger.ScheduleID),
|
|
zap.Error(err),
|
|
)
|
|
} else {
|
|
// Skip execution if schedule is disabled
|
|
if !schedule.IsEnabled {
|
|
er.logger.Info("Skipping disabled schedule",
|
|
zap.String("schedule_id", trigger.ScheduleID),
|
|
zap.String("workflow", workflow.Name),
|
|
)
|
|
return
|
|
}
|
|
// Use target from schedule (can be overridden by event input)
|
|
if schedule.Target != "" {
|
|
params["target"] = schedule.Target
|
|
}
|
|
// Use params from schedule
|
|
if schedule.Params != nil {
|
|
for k, v := range schedule.Params {
|
|
if s, ok := v.(string); ok {
|
|
params[k] = s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// New Vars syntax: set all resolved variables (overrides schedule params)
|
|
if resolvedVars != nil {
|
|
for name, value := range resolvedVars {
|
|
params[name] = value
|
|
}
|
|
// Normalize "Target" (capital T) to "target" (lowercase) for builtin variable derivation
|
|
// This allows event input `Target: event_data.url` to behave like CLI `-t` flag
|
|
if targetVal, hasTarget := resolvedVars["Target"]; hasTarget && targetVal != "" {
|
|
if _, hasLowercase := params["target"]; !hasLowercase {
|
|
params["target"] = targetVal
|
|
}
|
|
}
|
|
} else {
|
|
// Legacy syntax: set the input parameter using the trigger's input.name field
|
|
if trigger.Input.Name != "" {
|
|
params[trigger.Input.Name] = input
|
|
}
|
|
|
|
// Default to "target" if no input name specified
|
|
if input != "" && trigger.Input.Name == "" {
|
|
params["target"] = input
|
|
}
|
|
}
|
|
|
|
// Pass the event envelope as a special parameter
|
|
if eventEnvelope != "" {
|
|
params[scheduler.EventEnvelopeKey] = eventEnvelope
|
|
}
|
|
|
|
// Create executor with server mode
|
|
exec := executor.NewExecutor()
|
|
exec.SetServerMode(true)
|
|
exec.SetSchedulerInvocation(true) // Mark as scheduler invocation
|
|
exec.SetLoader(er.loader)
|
|
|
|
var err error
|
|
var result *core.WorkflowResult
|
|
|
|
if workflow.IsFlow() {
|
|
result, err = exec.ExecuteFlow(er.ctx, workflow, params, er.config)
|
|
} else {
|
|
result, err = exec.ExecuteModule(er.ctx, workflow, params, er.config)
|
|
}
|
|
|
|
if err != nil {
|
|
er.logger.Error("Event-triggered workflow failed",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.Error(err),
|
|
)
|
|
return
|
|
}
|
|
|
|
er.logger.Info("Event-triggered workflow completed",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("status", string(result.Status)),
|
|
zap.Duration("duration", result.EndTime.Sub(result.StartTime)),
|
|
)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleCronTrigger is called when a cron trigger fires.
|
|
func (er *EventReceiver) handleCronTrigger(workflow *core.Workflow, trigger *core.Trigger, input string) error {
|
|
er.logger.Info("Cron trigger fired",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
)
|
|
|
|
go func() {
|
|
params := make(map[string]string)
|
|
|
|
// If this trigger is linked to a Schedule record, look it up to get Target/Params
|
|
if trigger.ScheduleID != "" {
|
|
schedule, err := database.GetScheduleByID(er.ctx, trigger.ScheduleID)
|
|
if err != nil {
|
|
er.logger.Warn("Failed to load schedule for cron trigger",
|
|
zap.String("schedule_id", trigger.ScheduleID),
|
|
zap.Error(err),
|
|
)
|
|
} else {
|
|
// Skip execution if schedule is disabled
|
|
if !schedule.IsEnabled {
|
|
er.logger.Info("Skipping disabled schedule",
|
|
zap.String("schedule_id", trigger.ScheduleID),
|
|
zap.String("workflow", workflow.Name),
|
|
)
|
|
return
|
|
}
|
|
// Use target from schedule
|
|
if schedule.Target != "" {
|
|
params["target"] = schedule.Target
|
|
}
|
|
// Use params from schedule
|
|
if schedule.Params != nil {
|
|
for k, v := range schedule.Params {
|
|
if s, ok := v.(string); ok {
|
|
params[k] = s
|
|
}
|
|
}
|
|
}
|
|
er.logger.Debug("Loaded schedule params for cron trigger",
|
|
zap.String("schedule_id", trigger.ScheduleID),
|
|
zap.String("target", schedule.Target),
|
|
zap.Int("param_count", len(schedule.Params)),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Set input parameter if specified (can override schedule params)
|
|
if trigger.Input.Name != "" && input != "" {
|
|
params[trigger.Input.Name] = input
|
|
}
|
|
|
|
exec := executor.NewExecutor()
|
|
exec.SetServerMode(true)
|
|
exec.SetSchedulerInvocation(true)
|
|
exec.SetLoader(er.loader)
|
|
|
|
var err error
|
|
var result *core.WorkflowResult
|
|
|
|
if workflow.IsFlow() {
|
|
result, err = exec.ExecuteFlow(er.ctx, workflow, params, er.config)
|
|
} else {
|
|
result, err = exec.ExecuteModule(er.ctx, workflow, params, er.config)
|
|
}
|
|
|
|
if err != nil {
|
|
er.logger.Error("Cron-triggered workflow failed",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.Error(err),
|
|
)
|
|
return
|
|
}
|
|
|
|
er.logger.Info("Cron-triggered workflow completed",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("status", string(result.Status)),
|
|
zap.Duration("duration", result.EndTime.Sub(result.StartTime)),
|
|
)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleWatchTrigger is called when a file watch trigger fires.
|
|
func (er *EventReceiver) handleWatchTrigger(workflow *core.Workflow, trigger *core.Trigger, input string) error {
|
|
er.logger.Info("Watch trigger fired",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.String("path", trigger.Path),
|
|
)
|
|
|
|
go func() {
|
|
params := make(map[string]string)
|
|
|
|
// If this trigger is linked to a Schedule record, look it up to get Target/Params
|
|
if trigger.ScheduleID != "" {
|
|
schedule, err := database.GetScheduleByID(er.ctx, trigger.ScheduleID)
|
|
if err != nil {
|
|
er.logger.Warn("Failed to load schedule for watch trigger",
|
|
zap.String("schedule_id", trigger.ScheduleID),
|
|
zap.Error(err),
|
|
)
|
|
} else {
|
|
// Skip execution if schedule is disabled
|
|
if !schedule.IsEnabled {
|
|
er.logger.Info("Skipping disabled schedule",
|
|
zap.String("schedule_id", trigger.ScheduleID),
|
|
zap.String("workflow", workflow.Name),
|
|
)
|
|
return
|
|
}
|
|
// Use target from schedule
|
|
if schedule.Target != "" {
|
|
params["target"] = schedule.Target
|
|
}
|
|
// Use params from schedule
|
|
if schedule.Params != nil {
|
|
for k, v := range schedule.Params {
|
|
if s, ok := v.(string); ok {
|
|
params[k] = s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set input parameter if specified (can override schedule params)
|
|
if trigger.Input.Name != "" && input != "" {
|
|
params[trigger.Input.Name] = input
|
|
}
|
|
|
|
exec := executor.NewExecutor()
|
|
exec.SetServerMode(true)
|
|
exec.SetSchedulerInvocation(true)
|
|
exec.SetLoader(er.loader)
|
|
|
|
var err error
|
|
var result *core.WorkflowResult
|
|
|
|
if workflow.IsFlow() {
|
|
result, err = exec.ExecuteFlow(er.ctx, workflow, params, er.config)
|
|
} else {
|
|
result, err = exec.ExecuteModule(er.ctx, workflow, params, er.config)
|
|
}
|
|
|
|
if err != nil {
|
|
er.logger.Error("Watch-triggered workflow failed",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.Error(err),
|
|
)
|
|
return
|
|
}
|
|
|
|
er.logger.Info("Watch-triggered workflow completed",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("status", string(result.Status)),
|
|
zap.Duration("duration", result.EndTime.Sub(result.StartTime)),
|
|
)
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetScheduler returns the underlying scheduler instance.
|
|
func (er *EventReceiver) GetScheduler() *scheduler.Scheduler {
|
|
return er.scheduler
|
|
}
|
|
|
|
// GetRegisteredWorkflows returns the list of registered event-triggered workflows.
|
|
func (er *EventReceiver) GetRegisteredWorkflows() []*core.Workflow {
|
|
er.mu.RLock()
|
|
defer er.mu.RUnlock()
|
|
return er.workflows
|
|
}
|
|
|
|
// GetRegisteredTriggers returns the list of registered event triggers.
|
|
func (er *EventReceiver) GetRegisteredTriggers() []*RegisteredEventTrigger {
|
|
er.mu.RLock()
|
|
defer er.mu.RUnlock()
|
|
return er.triggers
|
|
}
|
|
|
|
// IsRunning returns true if the event receiver is currently running.
|
|
func (er *EventReceiver) IsRunning() bool {
|
|
er.mu.RLock()
|
|
defer er.mu.RUnlock()
|
|
return er.running
|
|
}
|
|
|
|
// EmitEvent emits an event to trigger matching workflows.
|
|
// This is used by the API endpoint to trigger workflows via events.
|
|
func (er *EventReceiver) EmitEvent(event *core.Event) error {
|
|
if !er.running {
|
|
return fmt.Errorf("event receiver not running")
|
|
}
|
|
return er.scheduler.EmitEvent(event)
|
|
}
|
|
|
|
// GetRegisteredTriggersInfo returns trigger information for the API.
|
|
// This implements the EventReceiverProvider interface method.
|
|
func (er *EventReceiver) GetRegisteredTriggersInfo() []*handlers.EventReceiverTriggerInfo {
|
|
er.mu.RLock()
|
|
defer er.mu.RUnlock()
|
|
|
|
result := make([]*handlers.EventReceiverTriggerInfo, 0, len(er.triggers))
|
|
for _, t := range er.triggers {
|
|
topic := ""
|
|
if t.Trigger.Event != nil {
|
|
topic = t.Trigger.Event.Topic
|
|
}
|
|
result = append(result, &handlers.EventReceiverTriggerInfo{
|
|
WorkflowName: t.Workflow.Name,
|
|
TriggerName: t.Trigger.Name,
|
|
Type: string(t.Trigger.On),
|
|
Topic: topic,
|
|
Enabled: t.Trigger.Enabled,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetWorkflowLoader returns the workflow loader for dynamic schedule registration.
|
|
// This implements the EventReceiverProvider interface method.
|
|
func (er *EventReceiver) GetWorkflowLoader() handlers.WorkflowLoader {
|
|
return er.loader
|
|
}
|
|
|
|
// RegisterSchedule dynamically registers a trigger with the running scheduler.
|
|
// This implements the EventReceiverProvider interface method.
|
|
func (er *EventReceiver) RegisterSchedule(workflow *core.Workflow, trigger *core.Trigger) error {
|
|
if !er.running {
|
|
return fmt.Errorf("event receiver not running")
|
|
}
|
|
|
|
err := er.scheduler.RegisterTrigger(workflow, trigger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Track the trigger
|
|
er.mu.Lock()
|
|
er.triggers = append(er.triggers, &RegisteredEventTrigger{
|
|
Workflow: workflow,
|
|
Trigger: trigger,
|
|
})
|
|
er.mu.Unlock()
|
|
|
|
er.logger.Info("Dynamically registered trigger",
|
|
zap.String("workflow", workflow.Name),
|
|
zap.String("trigger", trigger.Name),
|
|
zap.String("type", string(trigger.On)),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// processQueuedEvents processes any unprocessed events from the database.
|
|
// This is called asynchronously when the event receiver starts to handle
|
|
// events that were queued while the server was unavailable.
|
|
func (er *EventReceiver) processQueuedEvents() {
|
|
db := database.GetDB()
|
|
if db == nil {
|
|
er.logger.Debug("Database not available, skipping queued event processing")
|
|
return
|
|
}
|
|
|
|
repo := repository.NewEventLogRepository(db)
|
|
ctx := er.ctx
|
|
|
|
// Fetch unprocessed events
|
|
events, err := repo.ListUnprocessed(ctx, 100)
|
|
if err != nil {
|
|
er.logger.Warn("Failed to fetch unprocessed events", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
if len(events) == 0 {
|
|
er.logger.Debug("No queued events to process")
|
|
return
|
|
}
|
|
|
|
er.logger.Info("Processing queued events", zap.Int("count", len(events)))
|
|
|
|
processedCount := 0
|
|
for _, eventLog := range events {
|
|
// Convert EventLog to core.Event
|
|
event := &core.Event{
|
|
Topic: eventLog.Topic,
|
|
ID: eventLog.EventID,
|
|
Name: eventLog.Name,
|
|
Source: eventLog.Source,
|
|
DataType: eventLog.DataType,
|
|
Data: eventLog.Data,
|
|
Timestamp: eventLog.CreatedAt,
|
|
}
|
|
|
|
// Parse the data into ParsedData
|
|
if eventLog.Data != "" {
|
|
var parsedData map[string]interface{}
|
|
if err := json.Unmarshal([]byte(eventLog.Data), &parsedData); err == nil {
|
|
event.ParsedData = parsedData
|
|
} else {
|
|
// If not valid JSON, wrap the value
|
|
event.ParsedData = map[string]interface{}{"value": eventLog.Data}
|
|
}
|
|
}
|
|
|
|
// Emit the event to trigger matching workflows
|
|
emitErr := er.scheduler.EmitEvent(event)
|
|
|
|
// Mark as processed (even if emit failed, to avoid retrying forever)
|
|
errMsg := ""
|
|
if emitErr != nil {
|
|
errMsg = emitErr.Error()
|
|
er.logger.Warn("Failed to emit queued event",
|
|
zap.Int64("id", eventLog.ID),
|
|
zap.String("topic", eventLog.Topic),
|
|
zap.Error(emitErr),
|
|
)
|
|
} else {
|
|
processedCount++
|
|
er.logger.Debug("Emitted queued event",
|
|
zap.Int64("id", eventLog.ID),
|
|
zap.String("topic", eventLog.Topic),
|
|
)
|
|
}
|
|
|
|
if markErr := repo.MarkProcessed(ctx, eventLog.ID, errMsg); markErr != nil {
|
|
er.logger.Warn("Failed to mark event as processed",
|
|
zap.Int64("id", eventLog.ID),
|
|
zap.Error(markErr),
|
|
)
|
|
}
|
|
|
|
// Small delay to avoid overwhelming the scheduler
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
er.logger.Info("Finished processing queued events",
|
|
zap.Int("processed", processedCount),
|
|
zap.Int("total", len(events)),
|
|
)
|
|
}
|