Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6ec54d6
feat(mchlogcore): add Transport and optional Closer interfaces
JVVeiga May 4, 2026
1727d9c
refactor(mchlogcore): dispatch via Transport interface
JVVeiga May 4, 2026
8e4782d
feat(mchlogcorev3): add NetworkConfig, Protocol and Configure
JVVeiga May 4, 2026
06b3310
feat(mchlogcorev3): add GELF builder and level mapping
JVVeiga May 4, 2026
1b0be02
feat(mchlogcorev3): add Graylog UDP transport
JVVeiga May 4, 2026
1072cd6
feat(mchlogcore): wire V3 dispatch into facade
JVVeiga May 4, 2026
a917ea5
test(mchlogcorev3): cover level mapping, gzip toggle, edge cases
JVVeiga May 4, 2026
7333dae
test(mchlogcorev3): cover failure handling and rate-limited warn
JVVeiga May 4, 2026
dea2eab
test(mchlogcorev3): add opt-in integration test scaffold
JVVeiga May 4, 2026
4eca751
docs(readme): add V3 (Graylog UDP) usage section
JVVeiga May 4, 2026
766763e
style(mchlogcorev3): gofmt coverage_test.go
JVVeiga May 4, 2026
49802fe
refactor(mchlogcorev3)!: rename NetworkConfig to BackendConfig and re…
JVVeiga May 5, 2026
350f568
test(mchlogcorev3): cover ProtocolFile behavior
JVVeiga May 5, 2026
283fc85
docs(readme): rewrite V3 section as unified backend (file + Graylog)
JVVeiga May 5, 2026
c263e43
refactor(mchlogcorev3): rename BackendConfig to DestinationConfig
JVVeiga May 5, 2026
900f805
fix(mchlogcorev3): address code review findings
JVVeiga May 5, 2026
82aa71d
docs(mchlogcorev3): align comments with destination terminology
JVVeiga May 5, 2026
660c1c5
test(mchlogcorev3): close coverage gaps in failure paths
JVVeiga May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 96 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,99 @@ if err != nil {
logger.Fatal("Error connecting to mysql: " + err.Error())
os.Exit(1)
}
```
```

## V3 - Destino unificado (arquivo ou Graylog)
A V3 é o destino unificado da toolkit. O serviço escolhe entre **arquivo** (mesmo layout do V2) e **GELF UDP** (Graylog) configurando `DestinationConfig.Protocol`. A API do `Logger` não muda — serviços que ainda usam V1 (default) ou V2 seguem funcionando sem alteração.

A V3 é a forma recomendada daqui em diante. V1 e V2 continuam disponíveis para retrocompatibilidade enquanto serviços migram.

### Modo arquivo (`ProtocolFile`)
Comportamento idêntico ao V2: layout `<basePath>/<service>/<level>/<level>.log`, mesma JSON shape (`message`, `level`, `source`, `line`, `trace`, `timestamp`).
```go
import (
mchlogtoolkitgo "github.com/gaudiumsoftware/mchlogtoolkitgo"
"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcore"
"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev3"
)

func main() {
if err := mchlogcorev3.Configure(mchlogcorev3.DestinationConfig{
Protocol: mchlogcorev3.ProtocolFile,
}); err != nil {
panic(err)
}
mchlogcore.SetVersion(mchlogcore.V3)

logger, _ := mchlogtoolkitgo.NewLogger("payments-api", "info")
logger.Initialize() // grava em /applog/payments-api/...
logger.Info("aplicação iniciada")
}
```

### Modo Graylog UDP (`ProtocolGraylogUDP`)
Para `dev`/`qa` que centralizam logs no Graylog em vez de arquivo local:
```go
import (
"os"

mchlogtoolkitgo "github.com/gaudiumsoftware/mchlogtoolkitgo"
"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcore"
"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev3"
)

func main() {
if err := mchlogcorev3.Configure(mchlogcorev3.DestinationConfig{
Protocol: mchlogcorev3.ProtocolGraylogUDP,
Addr: "graylog.dev.internal:12201",
Source: "payments-api-qa-" + os.Getenv("POD_NAME"),
// DisableGZIP: true, // opcional, default = compressão habilitada
}); err != nil {
panic(err)
}
mchlogcore.SetVersion(mchlogcore.V3)

logger, _ := mchlogtoolkitgo.NewLogger("payments-api", "debug")
logger.Initialize()
logger.Info("aplicação iniciada e ouvindo na porta 80")
}
```

### Campos do `DestinationConfig`
| Campo | Obrigatório quando… | Descrição |
|---------------|--------------------------------|--------------------------------------------------------------------------------------|
| `Protocol` | — | `ProtocolFile` (default) ou `ProtocolGraylogUDP`. |
| `Addr` | `Protocol = ProtocolGraylogUDP`| Endereço do Graylog no formato `host:porta`. |
| `Source` | `Protocol = ProtocolGraylogUDP`| Valor do campo GELF `host` (coluna `source` no Graylog). **Fornecido pelo serviço** — a toolkit não autodetecta. Ex.: `payments-api-qa-pod-7f8d2`. Use `mchlogcorev3.DefaultSource()` se quiser apenas o hostname. |
| `DisableGZIP` | nunca (opcional) | Default `false` (gzip habilitado). Aplica só ao `ProtocolGraylogUDP`. |

### Como aparece no Graylog (modo `ProtocolGraylogUDP`)
| GELF field | Origem | Coluna/campo no Graylog |
|---------------------|----------------------------------------------|-------------------------|
| `host` | `cfg.Source` | `source` (default) |
| `short_message` | chave `message` do payload | `message` (default) |
| `level` | severity syslog (info=6, debug=7, warn=4, error=3, fatal=2) | `level` |
| `_application_name` | parâmetro `service` de `NewLogger` | `application_name` |
| `_log_id` | `<service>-mchlog-<level>` (espelha pasta dos arquivos) | `log_id` |
| `_level_name` | level em texto | `level_name` |
| `_file`, `_line` | `runtime.Caller` | `file`, `line` |
| `_error` | `errLog.Error()`, quando presente | `error` |

Exemplos de busca:
- `application_name:payments-api AND level:<=3` — erros de um serviço.
- `log_id:payments-api-mchlog-info` — equivale ao arquivo `INFO`.
- `source:*-qa-*` — todos os pods de QA (env embutido em `Source` pelo caller).

### Falhas de envio (modo `ProtocolGraylogUDP`)
UDP é fire-and-forget. Se o destino estiver inacessível, a toolkit
**descarta a mensagem silenciosamente** e emite no máximo **uma linha em
`stderr` a cada 60s** (`mchlogcorev3: GELF UDP send failed: ...`).
Não há fallback automático para arquivo.

### Quando usar cada modo
- **Produção**: `ProtocolFile` (ou seguir em V1/V2). Arquivos persistidos em `/applog/<service>/...` são a fonte de verdade.
- **Dev/QA**: `ProtocolGraylogUDP` para concentrar logs no Graylog.

### Migração de V1/V2 para V3
Trocar `mchlogcore.SetVersion(mchlogcore.V2)` por `Configure(DestinationConfig{Protocol: ProtocolFile}) + SetVersion(V3)` mantém o comportamento bit-a-bit (mesmo layout, mesma JSON shape).
V1 e V2 seguem disponíveis até a próxima onda de migração.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/gaudiumsoftware/mchlogtoolkitgo

go 1.22.1

require github.com/rs/zerolog v1.33.0
require (
github.com/Graylog2/go-gelf v0.0.0-20170811154226-7ebf4f536d8f
github.com/rs/zerolog v1.33.0
)

require (
github.com/mattn/go-colorable v0.1.13 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/Graylog2/go-gelf v0.0.0-20170811154226-7ebf4f536d8f h1:xMWj7GzE4gCkm8e+661/GJHDXr4h7/jt4kM1Vvr9c5k=
github.com/Graylog2/go-gelf v0.0.0-20170811154226-7ebf4f536d8f/go.mod h1:fBaQWrftOD5CrVCUfoYGHs4X4VViTuGOXA8WloCjTY0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
Expand Down
129 changes: 100 additions & 29 deletions mchlogcore/mchlog.go
Original file line number Diff line number Diff line change
@@ -1,70 +1,141 @@
package mchlogcore

import (
"fmt"
"os"

"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev1"
"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev2"
"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev3"
)

// Asserções de tempo de compilação garantindo que cada destino
// satisfaz a interface Transport (e Closer, quando aplicável).
var (
_ Transport = (*mchlogcorev1.LogType)(nil)
_ Transport = (*mchlogcorev2.LogType)(nil)
_ Transport = (*mchlogcorev3.LogType)(nil)
_ Closer = (*mchlogcorev3.LogType)(nil)
)

// LogVersion is a type to define which version of the logger to use
// LogVersion identifica qual destino de log está em uso.
type LogVersion int

const (
// V1 refers to the first version of the logger, which includes IP in filename and uses a channel-based approach
// V1 — destino de arquivo, formato com IP e timestamp por hora.
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 — destino de arquivo, formato simples (um arquivo por subject).
V2
// V3 — destino unificado. Suporta arquivo (mesmo layout do V2) e
// rede (GELF UDP) selecionados via mchlogcorev3.DestinationConfig.Protocol.
// Outros protocolos (graylog-tcp, syslog, splunk-hec, ...) podem ser
// adicionados sem bumpar o enum.
V3
)

var currentVersion = V1
var (
currentVersion = V1
// current aponta para o Transport efetivamente em uso. É populado
// por SetVersion (e, na primeira chamada, por init).
current Transport
)

func init() {
current = transportFor(currentVersion)
}

// SetVersion chooses which version to use (V1 or V2).
// This should ideally be called before InitializeMchLog.
// transportFor mapeia uma LogVersion para o Transport correspondente.
// Centraliza o dispatch: adicionar uma nova versão significa estender
// apenas este switch.
//
// Para V3, devolvemos &mchlogcorev3.MchLog. O facade interno do V3
// (LogType) tolera estado pré-Initialize (early-return em LogSubject /
// GetFileNameFromStreamName / Close), então chamadas antes de
// InitializeMchLog não panicam.
func transportFor(v LogVersion) Transport {
switch v {
case V3:
return &mchlogcorev3.MchLog
case V2:
return &mchlogcorev2.MchLog
default:
return &mchlogcorev1.MchLog
}
}

// SetVersion escolhe qual versão (V1 ou V2) será usada.
// Deve ser chamado antes de InitializeMchLog.
func SetVersion(v LogVersion) {
currentVersion = v
current = transportFor(v)
}

// LogType is the facade structure that delegates calls to either V1 or V2 implementation
// LogType é o facade que delega ao Transport ativo.
type LogType struct{}

// LogSubject records the content to the log file using the selected version
// LogSubject delega para o transporte ativo.
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...)
}
current.LogSubject(subject, content, errLog, ascendStackFrame...)
}

// GetFileNameFromStreamName returns the log file path for the given subject
// GetFileNameFromStreamName delega para o transporte ativo.
func (l *LogType) GetFileNameFromStreamName(subject string) string {
if currentVersion == V1 {
return mchlogcorev1.MchLog.GetFileNameFromStreamName(subject)
}

return mchlogcorev2.MchLog.GetFileNameFromStreamName(subject)
return current.GetFileNameFromStreamName(subject)
}

// GetIP returns the IP where the log is running (only available in V1, returns empty for V2)
// GetIP retorna o IP onde o log está rodando, quando o transporte ativo
// expõe essa informação (V1). Para outros transportes retorna "".
func (l *LogType) GetIP() string {
if currentVersion == V1 {
return mchlogcorev1.MchLog.GetIP()
if v1, ok := current.(*mchlogcorev1.LogType); ok {
return v1.GetIP()
}
return ""
}

// MchLog is the global instance of the log facade
// Close libera recursos do transporte ativo, quando ele implementa
// a interface Closer (somente destinos que precisam de cleanup explícito,
// ex.: V3 sobre UDP). Para destinos de arquivo (V1, V2) é no-op.
// Idempotência é responsabilidade do destino.
func (l *LogType) Close() error {
if c, ok := current.(Closer); ok {
return c.Close()
}
return nil
}

// MchLog é a instância global do facade.
var MchLog LogType

// InitializeMchLog initializes the selected version's backend with the given path
// InitializeMchLog inicializa o destino selecionado com o caminho dado.
// Em todos os destinos o path tem a forma "<basePath>/<service>/":
// - V1, V2 e V3-ProtocolFile usam o caminho como diretório base de arquivos.
// - V3-ProtocolGraylogUDP usa o último segmento apenas para extrair
// o nome do serviço; o destino real é cfg.Addr.
func InitializeMchLog(path string) {
versionName := "V1"
if currentVersion == V1 {
mchlogcorev1.InitializeMchLog(path)
} else {
var versionName string
var initErr error

switch currentVersion {
case V3:
versionName = "V3"
initErr = mchlogcorev3.Initialize(path)
case V2:
versionName = "V2"
mchlogcorev2.InitializeMchLog(path)
default:
versionName = "V1"
mchlogcorev1.InitializeMchLog(path)
}

// Re-vincula current ao transporte agora inicializado. Necessário
// para V3, cuja global passou de nil para o ponteiro válido.
current = transportFor(currentVersion)

if initErr != nil {
fmt.Fprintf(os.Stderr, "mchlogcore: %s initialization failed: %v\n", versionName, initErr)
return
}

// The first log in info should be the version of the logger (v1 or v2)
// Primeiro log informa qual versão foi inicializada.
MchLog.LogSubject("info", map[string]string{"message": "MchLogToolkit initialized", "version": versionName}, nil)
}
57 changes: 57 additions & 0 deletions mchlogcore/mchlog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package mchlogcore

import (
"testing"

"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev1"
"github.com/gaudiumsoftware/mchlogtoolkitgo/mchlogcorev2"
)

// TestSetVersionSwapsTransport garante que SetVersion troca o Transport
// ativo para o backend correspondente (V1 ou V2). É a invariante central
// do facade refatorado.
func TestSetVersionSwapsTransport(t *testing.T) {
t.Cleanup(func() { SetVersion(V1) })

SetVersion(V1)
if _, ok := current.(*mchlogcorev1.LogType); !ok {
t.Fatalf("V1 should select *mchlogcorev1.LogType, got %T", current)
}

SetVersion(V2)
if _, ok := current.(*mchlogcorev2.LogType); !ok {
t.Fatalf("V2 should select *mchlogcorev2.LogType, got %T", current)
}
}

// TestGetIPOnlyV1 confirma que GetIP delega para o backend apenas quando
// V1 está ativo; para outros backends retorna string vazia.
func TestGetIPOnlyV1(t *testing.T) {
t.Cleanup(func() { SetVersion(V1) })

SetVersion(V2)
if got := MchLog.GetIP(); got != "" {
t.Fatalf("expected empty IP for V2, got %q", got)
}

SetVersion(V1)
// V1.GetIP pode retornar "" se a máquina não tem IP não-loopback,
// portanto não exigimos não-vazio — apenas que o tipo correto é consultado.
_ = MchLog.GetIP()
}

// TestCloseNoopForFileBackends garante que Close no facade é no-op para
// V1 e V2 (que não implementam Closer) e não retorna erro.
func TestCloseNoopForFileBackends(t *testing.T) {
t.Cleanup(func() { SetVersion(V1) })

SetVersion(V1)
if err := MchLog.Close(); err != nil {
t.Fatalf("Close on V1 should be no-op, got %v", err)
}

SetVersion(V2)
if err := MchLog.Close(); err != nil {
t.Fatalf("Close on V2 should be no-op, got %v", err)
}
}
30 changes: 30 additions & 0 deletions mchlogcore/transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package mchlogcore

// Transport é a estratégia que efetivamente persiste ou envia os logs.
// Cada destino (arquivo V1, arquivo V2, rede V3, ...) implementa esta
// interface e é selecionado pelo facade através de SetVersion.
//
// A interface é declarada aqui no pacote do facade. Os pacotes de destino
// não precisam importá-la: como Go usa interfaces estruturais, basta que
// os métodos coincidam. Isso evita ciclo de import entre mchlogcore e os
// pacotes mchlogcoreV*.
type Transport interface {
// LogSubject persiste/envia o conteúdo de log.
// Mesma assinatura usada pelos destinos V1 e V2.
LogSubject(subject string, content any, errLog error, ascendStackFrame ...int)

// GetFileNameFromStreamName devolve o caminho do arquivo (V1/V2)
// ou um descritor lógico (ex.: "udp://host:port/<subject>") para
// transportes de rede. Mantida por compatibilidade com testes existentes.
GetFileNameFromStreamName(subject string) string
}

// Closer é uma interface opcional: destinos que mantêm recursos
// que precisam ser liberados explicitamente (ex.: conexões de rede)
// implementam Close. O facade chama via type assertion, então
// destinos que não precisam (V1, V2) não são forçados a implementar.
type Closer interface {
// Close libera recursos do transporte.
// Deve ser idempotente.
Close() error
}
Loading
Loading