|
15 | 15 | package log |
16 | 16 |
|
17 | 17 | import ( |
| 18 | + "encoding/json" |
18 | 19 | "fmt" |
19 | | - "log/slog" |
| 20 | + "os" |
20 | 21 |
|
21 | 22 | "github.com/blinklabs-io/adder/event" |
22 | 23 | "github.com/blinklabs-io/adder/internal/logging" |
23 | 24 | "github.com/blinklabs-io/adder/plugin" |
24 | 25 | ) |
25 | 26 |
|
| 27 | +const ( |
| 28 | + FormatText = "text" |
| 29 | + FormatJSON = "json" |
| 30 | +) |
| 31 | + |
26 | 32 | type LogOutput struct { |
27 | | - errorChan chan error |
28 | | - eventChan chan event.Event |
29 | | - logger plugin.Logger |
30 | | - outputLogger *slog.Logger |
31 | | - level string |
| 33 | + errorChan chan error |
| 34 | + eventChan chan event.Event |
| 35 | + doneChan chan struct{} |
| 36 | + logger plugin.Logger |
| 37 | + format string |
32 | 38 | } |
33 | 39 |
|
34 | 40 | func New(options ...LogOptionFunc) *LogOutput { |
35 | 41 | l := &LogOutput{ |
36 | | - level: "info", |
| 42 | + format: FormatText, |
37 | 43 | } |
38 | 44 | for _, option := range options { |
39 | 45 | option(l) |
40 | 46 | } |
41 | 47 | if l.logger == nil { |
42 | 48 | l.logger = logging.GetLogger() |
43 | 49 | } |
44 | | - |
45 | | - // Use the provided *slog.Logger if available, otherwise fall back to global logger |
46 | | - if providedLogger, ok := l.logger.(*slog.Logger); ok { |
47 | | - l.outputLogger = providedLogger.With("type", "event") |
48 | | - } else { |
49 | | - l.outputLogger = logging.GetLogger().With("type", "event") |
50 | | - } |
51 | 50 | return l |
52 | 51 | } |
53 | 52 |
|
54 | 53 | // Start the log output |
55 | 54 | func (l *LogOutput) Start() error { |
56 | 55 | l.eventChan = make(chan event.Event, 10) |
57 | 56 | l.errorChan = make(chan error) |
| 57 | + l.doneChan = make(chan struct{}) |
| 58 | + // Capture channels locally to avoid races with Stop() |
| 59 | + eventChan := l.eventChan |
| 60 | + doneChan := l.doneChan |
58 | 61 | go func() { |
59 | | - for { |
60 | | - evt, ok := <-l.eventChan |
61 | | - // Channel has been closed, which means we're shutting down |
62 | | - if !ok { |
63 | | - return |
64 | | - } |
65 | | - switch l.level { |
66 | | - case "info": |
67 | | - l.outputLogger.Info("", "event", fmt.Sprintf("%+v", evt)) |
68 | | - case "warn": |
69 | | - l.outputLogger.Warn("", "event", fmt.Sprintf("%+v", evt)) |
70 | | - case "error": |
71 | | - l.outputLogger.Error("", "event", fmt.Sprintf("%+v", evt)) |
| 62 | + defer close(doneChan) |
| 63 | + for evt := range eventChan { |
| 64 | + switch l.format { |
| 65 | + case FormatJSON: |
| 66 | + l.writeJSON(evt) |
72 | 67 | default: |
73 | | - // Use INFO level if log level isn't recognized |
74 | | - l.outputLogger.Info("", "event", fmt.Sprintf("%+v", evt)) |
| 68 | + l.writeText(evt) |
75 | 69 | } |
76 | 70 | } |
77 | 71 | }() |
78 | 72 | return nil |
79 | 73 | } |
80 | 74 |
|
| 75 | +// writeText writes events in a human-readable format to stdout. |
| 76 | +func (l *LogOutput) writeText(evt event.Event) { |
| 77 | + ts := evt.Timestamp.Format("2006-01-02 15:04:05") |
| 78 | + |
| 79 | + var line string |
| 80 | + switch payload := evt.Payload.(type) { |
| 81 | + case event.BlockEvent: |
| 82 | + ctx, _ := evt.Context.(event.BlockContext) |
| 83 | + line = fmt.Sprintf( |
| 84 | + "%s %-12s slot=%-10d block=%-8d hash=%s era=%-7s txs=%d size=%d", |
| 85 | + ts, "BLOCK", |
| 86 | + ctx.SlotNumber, ctx.BlockNumber, |
| 87 | + payload.BlockHash, |
| 88 | + ctx.Era, |
| 89 | + payload.TransactionCount, |
| 90 | + payload.BlockBodySize, |
| 91 | + ) |
| 92 | + case event.TransactionEvent: |
| 93 | + ctx, _ := evt.Context.(event.TransactionContext) |
| 94 | + line = fmt.Sprintf( |
| 95 | + "%s %-12s slot=%-10d block=%-8d tx=%s fee=%d inputs=%d outputs=%d", |
| 96 | + ts, "TX", |
| 97 | + ctx.SlotNumber, ctx.BlockNumber, |
| 98 | + ctx.TransactionHash, |
| 99 | + payload.Fee, |
| 100 | + len(payload.Inputs), len(payload.Outputs), |
| 101 | + ) |
| 102 | + case event.RollbackEvent: |
| 103 | + line = fmt.Sprintf( |
| 104 | + "%s %-12s slot=%-10d hash=%s", |
| 105 | + ts, "ROLLBACK", |
| 106 | + payload.SlotNumber, |
| 107 | + payload.BlockHash, |
| 108 | + ) |
| 109 | + case event.GovernanceEvent: |
| 110 | + ctx, _ := evt.Context.(event.GovernanceContext) |
| 111 | + certs := len(payload.DRepCertificates) + |
| 112 | + len(payload.VoteDelegationCertificates) + |
| 113 | + len(payload.CommitteeCertificates) |
| 114 | + line = fmt.Sprintf( |
| 115 | + "%s %-12s slot=%-10d block=%-8d tx=%s proposals=%d votes=%d certs=%d", |
| 116 | + ts, "GOVERNANCE", |
| 117 | + ctx.SlotNumber, ctx.BlockNumber, |
| 118 | + ctx.TransactionHash, |
| 119 | + len(payload.ProposalProcedures), |
| 120 | + len(payload.VotingProcedures), |
| 121 | + certs, |
| 122 | + ) |
| 123 | + default: |
| 124 | + line = fmt.Sprintf( |
| 125 | + "%s %-12s %+v", |
| 126 | + ts, evt.Type, evt.Payload, |
| 127 | + ) |
| 128 | + } |
| 129 | + |
| 130 | + fmt.Fprintln(os.Stdout, line) |
| 131 | +} |
| 132 | + |
| 133 | +// writeJSON writes events as newline-delimited JSON to stdout. |
| 134 | +// Errors are written to stderr to avoid corrupting the JSON stream. |
| 135 | +func (l *LogOutput) writeJSON(evt event.Event) { |
| 136 | + data, err := json.Marshal(evt) |
| 137 | + if err != nil { |
| 138 | + fmt.Fprintf( |
| 139 | + os.Stderr, |
| 140 | + "error: failed to marshal event: %v\n", |
| 141 | + err, |
| 142 | + ) |
| 143 | + return |
| 144 | + } |
| 145 | + os.Stdout.Write(append(data, '\n')) |
| 146 | +} |
| 147 | + |
81 | 148 | // Stop the log output |
82 | 149 | func (l *LogOutput) Stop() error { |
83 | 150 | if l.eventChan != nil { |
84 | 151 | close(l.eventChan) |
| 152 | + // Wait for the goroutine to finish processing |
| 153 | + if l.doneChan != nil { |
| 154 | + <-l.doneChan |
| 155 | + } |
85 | 156 | l.eventChan = nil |
86 | 157 | } |
87 | 158 | if l.errorChan != nil { |
|
0 commit comments