-
Notifications
You must be signed in to change notification settings - Fork 0
MchLogToolkitGo - Envio de logs via UDP no formato GELF #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6b09c0e
490dca5
71c5e2a
a88d900
2074dbc
a1b8fd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ package mchlogtoolkitgo | |
| import ( | ||
| "encoding/json" | ||
| "errors" | ||
| "log" | ||
| "os" | ||
| "runtime" | ||
| "strconv" | ||
| "strings" | ||
|
|
@@ -20,6 +22,18 @@ const ( | |
|
|
||
| DebugPath = "./applog/" | ||
| ProdPath = "/applog/" | ||
|
|
||
| // EnvUDPTarget is the environment variable name for the UDP target address. | ||
| // Format: "host:port" (e.g., "graylog.example.com:12201") | ||
| EnvUDPTarget = "MCHLOG_UDP_TARGET" | ||
|
|
||
| // EnvUDPCompress is the environment variable to enable/disable GZIP compression for UDP. | ||
| // Values: "true" or "false" (default: "true") | ||
| EnvUDPCompress = "MCHLOG_UDP_COMPRESS" | ||
|
|
||
| // EnvFileOutput is the environment variable to enable/disable file output. | ||
| // Values: "true" or "false" (default: "true") | ||
| EnvFileOutput = "MCHLOG_FILE_OUTPUT" | ||
| ) | ||
|
|
||
| // Logger é a estrutura que encapsula as funcionalidades de log da aplicação | ||
|
|
@@ -50,8 +64,26 @@ func NewLogger(service, level string) (*Logger, error) { | |
| return l, nil | ||
| } | ||
|
|
||
| // Initialize inicializa o logger | ||
| // Initialize inicializa o logger. | ||
| // If the environment variable MCHLOG_UDP_TARGET is set, UDP output is automatically configured. | ||
| // If MCHLOG_FILE_OUTPUT is set to "false", file output is disabled. | ||
| func (l *Logger) Initialize() { | ||
| // Check environment variables for UDP configuration | ||
| if target := os.Getenv(EnvUDPTarget); target != "" { | ||
| compress := true | ||
| if v := os.Getenv(EnvUDPCompress); strings.ToLower(v) == "false" { | ||
| compress = false | ||
| } | ||
| if err := mchlogcore.SetUDPTarget(target, compress); err != nil { | ||
| log.Printf("[mchlog] failed to configure UDP target %q from environment: %v", target, err) | ||
| } | ||
| } | ||
|
|
||
| // Check environment variable for file output | ||
| if v := os.Getenv(EnvFileOutput); strings.ToLower(v) == "false" { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Attention · The environment variable |
||
| mchlogcore.SetFileOutput(false) | ||
| } | ||
|
|
||
| mchlogcore.InitializeMchLog(l.path + l.service + "/") | ||
| l.log = &mchlogcore.MchLog | ||
| } | ||
|
|
@@ -80,6 +112,39 @@ func (l *Logger) SetLevel(level string) error { | |
| return nil | ||
| } | ||
|
|
||
| // SetUDPTarget configures the logger to send GELF messages via UDP to the given address. | ||
| // The address should be in "host:port" format (e.g., "graylog.example.com:12201"). | ||
| // GZIP compression is enabled by default. | ||
| // | ||
| // Note: UDP target and file output settings are global and shared across all | ||
| // Logger instances. Changing them on one instance affects all others. | ||
| func (l *Logger) SetUDPTarget(address string) error { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Suggestion ·
|
||
| return mchlogcore.SetUDPTarget(address, true) | ||
| } | ||
|
|
||
| // SetUDPTargetWithOptions configures the logger to send GELF messages via UDP with explicit options. | ||
| // The address should be in "host:port" format (e.g., "graylog.example.com:12201"). | ||
| // If compress is true, messages will be GZIP compressed before sending. | ||
| func (l *Logger) SetUDPTargetWithOptions(address string, compress bool) error { | ||
| return mchlogcore.SetUDPTarget(address, compress) | ||
| } | ||
|
|
||
| // DisableFileOutput disables file-based log output. | ||
| // When called, logs are only sent via UDP (if configured). | ||
| // This is a global setting shared across all Logger instances. | ||
| // When disabled before Initialize(), no log directories or files are created. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Attention · The doc comment for |
||
| func (l *Logger) DisableFileOutput() { | ||
| mchlogcore.SetFileOutput(false) | ||
| } | ||
|
|
||
| // Close drains any buffered UDP messages and closes the UDP connection. | ||
| // It does NOT close file handles used by the file logging backends (V1/V2), | ||
| // as those write directly to unbuffered *os.File handles managed by zerolog | ||
| // and are kept open for the lifetime of the process. | ||
| func (l *Logger) Close() error { | ||
| return mchlogcore.CloseUDP() | ||
| } | ||
|
|
||
| func (l *Logger) Test(message string) { | ||
| if l.level != TestLevel { | ||
| return | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,13 @@ | ||
| package mchlogcore | ||
|
|
||
| import ( | ||
| "log" | ||
| "sync" | ||
| "sync/atomic" | ||
|
|
||
| "github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev1" | ||
| "github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev2" | ||
| "github.com/gaudiumsoftware/mchlogtoolkitgo/mchloggelf" | ||
| ) | ||
|
|
||
| // LogVersion is a type to define which version of the logger to use | ||
|
|
@@ -13,25 +18,178 @@ const ( | |
| V1 LogVersion = iota | ||
| // V2 refers to the second version of the logger, which has a simpler file structure without IP or timestamps in names | ||
| V2 | ||
|
|
||
| // udpBufferSize is the size of the buffered channel for async UDP sends. | ||
| // Messages beyond this buffer are dropped to prevent memory exhaustion. | ||
| udpBufferSize = 1000 | ||
| ) | ||
|
|
||
| var currentVersion = V1 | ||
|
|
||
| // udpMu protects udpTransport, udpChan, and udpDone from concurrent access. | ||
| // LogSubject holds RLock during the entire channel send to prevent the channel | ||
| // from being closed mid-send. | ||
| var udpMu sync.RWMutex | ||
| var udpTransport *mchloggelf.UDPTransport | ||
| var udpChan chan *mchloggelf.GELFMessage | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Suggestion · Package-level mutable state ( |
||
| var udpDone chan struct{} | ||
|
|
||
| // fileOutputEnabled uses atomic.Bool for lock-free concurrent reads in LogSubject. | ||
| var fileOutputEnabled atomic.Bool | ||
|
|
||
| func init() { | ||
| fileOutputEnabled.Store(true) | ||
| } | ||
|
|
||
| // SetVersion chooses which version to use (V1 or V2). | ||
| // This should ideally be called before InitializeMchLog. | ||
| func SetVersion(v LogVersion) { | ||
| currentVersion = v | ||
| } | ||
|
|
||
| // SetUDPTarget configures a UDP transport to send GELF messages to the given address. | ||
| // The address should be in "host:port" format (e.g., "graylog.example.com:12201"). | ||
| // If compress is true, messages will be GZIP compressed. | ||
| func SetUDPTarget(address string, compress bool) error { | ||
| // Phase 1: under write lock, close the channel and capture references | ||
| // to the old worker so we can wait on it without holding the lock. | ||
| udpMu.Lock() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Attention · In There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Atenção · Entre a Phase 1 (
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ramon-gaudium Vê esse comentário |
||
| oldDone, oldTransport := detachWorkerLocked() | ||
| udpMu.Unlock() | ||
|
|
||
| // Phase 2: wait for the old worker to drain without holding any lock, | ||
| // so LogSubject calls are not blocked during the drain. | ||
| waitAndClose(oldDone, oldTransport) | ||
|
|
||
| // Phase 3: create new transport (network operation, no lock needed) | ||
| t, err := mchloggelf.NewUDPTransport(address, compress) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Phase 4: install new worker under write lock | ||
| udpMu.Lock() | ||
| defer udpMu.Unlock() | ||
|
|
||
| // If another SetUDPTarget ran between phase 1 and 4 and installed a new | ||
| // worker, shut it down first (last caller wins). This wait is bounded: | ||
| // the write lock prevents new sends, so the worker only drains what is | ||
| // already in the buffer. | ||
| innerDone, innerTransport := detachWorkerLocked() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Attention · In |
||
| if innerDone != nil { | ||
| <-innerDone | ||
| } | ||
| if innerTransport != nil { | ||
| _ = innerTransport.Close() | ||
| } | ||
|
|
||
| udpTransport = t | ||
| startWorkerLocked() | ||
| return nil | ||
| } | ||
|
|
||
| // detachWorkerLocked closes the channel and clears the global references, | ||
| // returning the old done channel and transport so the caller can wait and | ||
| // clean up outside the lock. Must be called with udpMu write lock held. | ||
| // After this call, udpChan is nil so LogSubject will skip UDP sends. | ||
| func detachWorkerLocked() (<-chan struct{}, *mchloggelf.UDPTransport) { | ||
| if udpChan == nil { | ||
| return nil, nil | ||
| } | ||
| close(udpChan) | ||
| done := udpDone | ||
| transport := udpTransport | ||
| udpChan = nil | ||
| udpDone = nil | ||
| udpTransport = nil | ||
| return done, transport | ||
| } | ||
|
|
||
| // waitAndClose waits for the worker to finish draining and closes the transport. | ||
| // Safe to call with nil arguments (no-op). | ||
| func waitAndClose(done <-chan struct{}, transport *mchloggelf.UDPTransport) { | ||
| if done != nil { | ||
| <-done | ||
| } | ||
| if transport != nil { | ||
| _ = transport.Close() | ||
| } | ||
| } | ||
|
|
||
| // startWorkerLocked initializes the buffered channel and starts a single worker | ||
| // goroutine that reads messages and sends them over UDP sequentially. | ||
| // Must be called with udpMu write lock held. | ||
| func startWorkerLocked() { | ||
| udpChan = make(chan *mchloggelf.GELFMessage, udpBufferSize) | ||
| udpDone = make(chan struct{}) | ||
|
|
||
| // Capture references for the goroutine so it operates independently | ||
| // of the global variables. The worker reads from its own channel ref | ||
| // and does not need the mutex. | ||
| ch := udpChan | ||
| done := udpDone | ||
| transport := udpTransport | ||
|
|
||
| go func() { | ||
| defer close(done) | ||
| for msg := range ch { | ||
| if err := transport.Send(msg); err != nil { | ||
| log.Printf("[mchlog] failed to send GELF message via UDP: %v", err) | ||
| } | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| // SetFileOutput enables or disables file-based log output. | ||
| // When disabled, logs are only sent via UDP (if configured). | ||
| func SetFileOutput(enabled bool) { | ||
| fileOutputEnabled.Store(enabled) | ||
| } | ||
|
|
||
| // CloseUDP closes the UDP worker and transport connection if active. | ||
| // Waits for the worker to drain remaining buffered messages before returning. | ||
| func CloseUDP() error { | ||
| udpMu.Lock() | ||
| done, transport := detachWorkerLocked() | ||
| udpMu.Unlock() | ||
|
|
||
| waitAndClose(done, transport) | ||
| return nil | ||
| } | ||
|
Comment on lines
+151
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A função Sugiro refatorar Exemplo de alteração em func closeUDPWorker() error {
// ... (código existente)
if udpTransport != nil {
err := udpTransport.Close()
udpTransport = nil
return err
}
return nil
}func CloseUDP() error {
return closeUDPWorker()
} |
||
|
|
||
| // LogType is the facade structure that delegates calls to either V1 or V2 implementation | ||
| type LogType struct{} | ||
|
|
||
| // LogSubject records the content to the log file using the selected version | ||
| // LogSubject records the content to the log file and/or sends it via UDP using the selected version | ||
| func (l *LogType) LogSubject(subject string, content any, errLog error, ascendStackFrame ...int) { | ||
| if currentVersion == V1 { | ||
| mchlogcorev1.MchLog.LogSubject(subject, content, errLog, ascendStackFrame...) | ||
| } else { | ||
| mchlogcorev2.MchLog.LogSubject(subject, content, errLog, ascendStackFrame...) | ||
| if fileOutputEnabled.Load() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Attention · When |
||
| if currentVersion == V1 { | ||
| mchlogcorev1.MchLog.LogSubject(subject, content, errLog, ascendStackFrame...) | ||
| } else { | ||
| mchlogcorev2.MchLog.LogSubject(subject, content, errLog, ascendStackFrame...) | ||
| } | ||
| } | ||
|
|
||
| // Hold RLock for the entire nil-check + send to prevent detachWorkerLocked | ||
| // from closing the channel between the check and the send. The non-blocking | ||
| // select ensures we never block while holding the lock. | ||
| udpMu.RLock() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Critical · There is still a TOCTOU race between reading |
||
| defer udpMu.RUnlock() | ||
|
|
||
| if udpChan == nil { | ||
| return | ||
| } | ||
|
|
||
| msg, err := mchloggelf.NewGELFMessage(subject, content, errLog) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Attention ·
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Atenção ·
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ramon-gaudium Vê esse comentário |
||
| if err != nil { | ||
| log.Printf("[mchlog] failed to create GELF message for subject %q: %v", subject, err) | ||
| return | ||
| } | ||
| select { | ||
| case udpChan <- msg: | ||
| // Message queued successfully | ||
| default: | ||
| log.Printf("[mchlog] UDP send buffer full, dropping GELF message for subject %q", subject) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -55,14 +213,18 @@ func (l *LogType) GetIP() string { | |
| // MchLog is the global instance of the log facade | ||
| var MchLog LogType | ||
|
|
||
| // InitializeMchLog initializes the selected version's backend with the given path | ||
| // InitializeMchLog initializes the selected version's backend with the given path. | ||
| // If file output is disabled, the file backend is not initialized and no | ||
| // directories or files are created. | ||
| func InitializeMchLog(path string) { | ||
| versionName := "V1" | ||
| if currentVersion == V1 { | ||
| mchlogcorev1.InitializeMchLog(path) | ||
| } else { | ||
| versionName = "V2" | ||
| mchlogcorev2.InitializeMchLog(path) | ||
| if fileOutputEnabled.Load() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Attention · When |
||
| if currentVersion == V1 { | ||
| mchlogcorev1.InitializeMchLog(path) | ||
| } else { | ||
| versionName = "V2" | ||
| mchlogcorev2.InitializeMchLog(path) | ||
| } | ||
| } | ||
|
|
||
| // The first log in info should be the version of the logger (v1 or v2) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Attention ·
Bugs / Logic ErrorSetFileOutput(false)is called beforeInitializeMchLog(line 87). However, iffileOutputEnabledis set to false here, butInitialize()then callsmchlogcore.InitializeMchLog(...)unconditionally, file structures are still initialized. Additionally,SetFileOutputaffects a package-level global, meaning callingInitialize()on a secondLoggerinstance (with file output enabled) would not restore thefileOutputEnabled=truestate if another logger had disabled it. This is a pre-existing design concern made worse by the new code path. Document that these are global settings shared across allLoggerinstances.