Logolás
Go-ban az első hideg zuhany gyakran a logolás: a nyers log csomag egysoros, kontextus nélküli kimenete, vagy egy strukturált logger key=value fala, amiben egy stack trace egyetlen escape-elt stringgé lapul.
A kit válasza két rétegű. Az alap a standard log/slog: az alkalmazáskód kizárólag ezt importálja, semmi kit-specifikusat. Fölé a logx csomag rak kompozíciót, és egy Monolog-formátumú konzol-handlert (logx/monolog), ami dev módban pontosan úgy néz ki, mint amit PHP-ból ismersz: [dátum] channel.SZINT: üzenet {context}, alatta PHP-stílusú, teljes útvonalú stack trace.
Így néz ki
Állítsd be a LOG_FORMAT=monolog-ot (dev módban ezt érdemes), és a shop konzolja így ír:
[2026-07-15 10:23:44] shop.INFO: order placed {"order_id":"o_8412","total":12990}
[2026-07-15 10:23:45] shop.WARNING: payment provider slow, retrying {"attempt":2,"provider":"stripe"} in /home/you/shop/internal/modules/shop/surfaces/api/service/payments.go:114
És amikor a PlaceOrder a kanonikus hibánkba, az ErrOutOfStock-ba fut:
[2026-07-15 10:23:46] shop.ERROR: request failed: shop_out_of_stock: shop: place order: inventory: decrement: insufficient stock {"code":"shop_out_of_stock","method":"POST","path":"/api/v1/shop/orders","status":500}
Stack trace:
#0 /home/you/shop/internal/modules/shop/repository/inventory.go(87): repository.(*InventoryRepository).Decrement()
#1 /home/you/shop/internal/modules/shop/core/orders.go(41): core.(*Service).PlaceOrder()
#2 /home/you/shop/internal/modules/shop/surfaces/api/service/orders.go(52): service.(*OrderService).PlaceOrder()
#3 /home/you/shop/internal/modules/shop/surfaces/api/http/handlers.go(61): http.(*Handler).PlaceOrder()
#4 {main}
Ami ebben a pár sorban dolgozik:
- A channel a service neve (
OTEL_SERVICE_NAME, üresenapp), több service logját egymás mellett futtatva is látod, ki beszél. AWARNszint Monolog-hűenWARNING-ként jelenik meg. - A hiba fejléce a
errs-kóddal kezdődik (shop_out_of_stock: ...), így a hibamód a JSON-rész elolvasása nélkül azonosítható; a kód a JSON contextben is ott van. - A stack trace teljes, abszolút útvonalakkal: a legtöbb terminál és editor kattinthatóvá teszi. A frame-ek a hiba keletkezési pontjától felfelé sorakoznak; a dependency-kből (Go module cache-ből) jövő frame-ek halványítva jelennek meg, a runtime-belépési frame-eket a
{main}zárósor helyettesíti. - A hiba-attr nem duplázódik: a
slog.Any("error", err)értéke a fejlécbe és a stack-blokkba kerül, a JSON contextből kimarad (ahttperrduplikáltcauseattr-jával együtt). - Szín csak terminálon: a
NO_COLOR-t és aTERM=dumb-ot tiszteletben tartja, pipe-olt kimenetben nincs escape-szemét. - Egy
WARN/ERRORrekorderrsstack nélkül is kap lokációt: a hívási helye kerül a sor végérein file:lineformában.
A fenti hibát a httperr error handler logolta (request failed, method/path/status attr-okkal). Neked csak a hibát kellett visszaadni a service-ből. A stacket nem itt fogták el: az errs hiba születésekor rögzítette, és a wrap-lánc bármely pontjáról ugyanaz a teljes stack olvasható ki.
LOG_FORMAT csak azt dönti el, mit látsz a terminálban.Konfiguráció
A logx.Config a telemetry.Config Log mezője, a változók prefix nélkül, a top-szinten töltődnek:
| Változó | Default | Jelentés |
|---|---|---|
LOG_LEVEL | INFO | a default logger minimális szintje (minden sinkre érvényes) |
LOG_STDOUT | false | prod módban is írjon konzolra (JSON); hagyd kikapcsolva, ha a collector a konténer-kimenetet szedi, különben duplán ingesztálsz |
LOG_FORMAT | üres (auto) | a konzol-formátum: üresen szöveg dev módban / JSON prodban; monolog → Monolog-stílusú sorok, módtól függetlenül |
Ismeretlen LOG_FORMAT értékre a Config.Validate() (és így a boot) hibával áll le, nem csendben esik vissza.
A kompozíció: Fanout és LevelFilter
Monologban a logolást channelökből és stackekből raktad össze (stack driver, több channel egy alá fogva, level küszöbökkel). A logx ugyanezt a gondolatot adja vissza: csak nem YAML-ben, hanem három kis slog.Handler-kombinátorral:
func ConsoleHandler(w io.Writer, cfg Config, dev bool, channel string) slog.Handler
func Fanout(handlers ...slog.Handler) slog.Handler // ~ Monolog stack driver
func LevelFilter(min slog.Level, next slog.Handler) slog.Handler // ~ channel-szintű level küszöb
A telemetry.Setup pontosan így építi a default loggert:
slog.SetDefault(slog.New(
logx.LevelFilter(cfg.Log.Level, logx.Fanout(
logx.ConsoleHandler(os.Stdout, cfg.Log, dev, serviceName), // dev vagy LOG_STDOUT esetén
otelHandle.SlogHandler(), // OTLP bridge, prodban
sentryHandle.SlogHandler(), // Sentry capture, ha a DSN be van állítva
)),
))
A LevelFilter azért ül legkívül, mert nem minden leaf-handler szűr magától: az OTLP bridge például mindent továbbít, amit kap. A Fanout minden handlernek a rekord saját klónját adja, és az Enabled-et handlerenkénti gate-ként tiszteli: egyetlen handler esetén allokáció nélkül, önmagaként tér vissza.
Chassis-on belül ezt sosem írod meg: a server.Run / worker.Run / realtime.Run megkapja készen. Közvetlenül akkor nyúlsz hozzá, ha egy nem-chassis binárisban (CLI, migrátor) ugyanezt a konzol-élményt akarod, vagy egy plusz sinket (pl. fájl-handlert) fűznél a fanoutba.
logx/monolog közelről
A formátum-driver önálló csomag, egyetlen konstruktorral:
import "github.com/gp-system/telemetry/logx/monolog"
func NewHandler(w io.Writer, level slog.Level, channel string) *Handler
A LOG_FORMAT=monolog esetén a logx.ConsoleHandler ezt adja vissza, de bárhova bekötheted, ahol egy slog.Handler elvárt. Amit érdemes tudni róla:
slog-natív. AWith(...)attr-ok és aWithGroup(...)csoportok helyesen, beágyazott JSON-ként jelennek meg a context-részben ("req":{"status":200}).- A stackek forrása az
errs. A handler a rekord első hiba-értékű attr-ját emeli ki; ha az (vagy a wrap-láncában bármi)errshiba, azerrs.FullFramesteljes útvonalú stackje renderelődik. Egy simaerrors.Newhibánál nincs stack-blokk: helyette a log hívási helye kerül a sorba. - Párhuzamos futásban is atomi sorok: a handler klónjai közös mutexen írnak, torn write nincs.
Strukturált hibák a logban: errs nélkül semmi ilyen nincs
A Monolog-formátum a stack trace-t az errs hibákból nyeri, és ez nem konzol-privilégium: az *errs.Error implementálja a slog.LogValuer-t, így minden strukturált sinkben (JSON konzol, OTLP) a hiba csoportként bomlik ki:
"error": {
"msg": "shop: place order: inventory: decrement: insufficient stock",
"code": "shop_out_of_stock",
"public": "Egy vagy több tétel nincs elegendő készleten.",
"chain": [ { "msg": "shop: place order", "file": "service/orders.go", "line": 52, "..." : "…" }, "…" ],
"stack": [ { "file": "repository/inventory.go", "line": 87, "..." : "…" }, "…" ]
}
Ugyanaz a hiba tehát dev konzolon PHP-stílusú stack trace, prod logban gépi feldolgozásra kész JSON, a Sentryben pedig kód szerint csoportosított issue, mindez egyetlen return err-ből.
Monolog-fogalmak megfelelői
Ha a config/logging.php felől érkezel, így képződnek le a fogalmak:
| Monolog | gpsystem | Hol |
|---|---|---|
| channel neve | a service neve (OTEL_SERVICE_NAME) | a monolog-sor shop.INFO része |
stack driver (több channel egyben) | logx.Fanout | a telemetry.Setup építi |
channel level küszöb | LOG_LEVEL + logx.LevelFilter | env |
single / daily file driver | nincs (stdout az egyetlen cél) | lásd lentebb |
formatter (LineFormatter, JSON) | LOG_FORMAT (auto / monolog) | env |
Log::info('msg', ['ctx' => ...]) | slog.InfoContext(ctx, "msg", slog.Any("ctx", ...)) | app-kód |
Log::withContext([...]) | logger := slog.Default().With(...) | app-kód |
| Sentry / Slack channel a stackben | a fanout OTLP- és Sentry-ága | automatikus |
Egy csavar van, ami Monologban nem létezett: a context a trace-t is hordozza. A slog.InfoContext ctx-e nemcsak attr-forrás: az köti a logsort a trace-hez és a Sentry breadcrumb-hubhoz.
Hova lettek a fájl-logok?
Sehova, és ez szándékos. Nincs storage/logs/shop.log: a kit a 12-factor elvet követi, a logok eseményfolyamként a stdout-ra mennek, a gyűjtés-forgatás-őrzés pedig a platform dolga. Konténerben a runtime úgyis elkapja a stdout-ot; a collectorod (vagy LOG_STDOUT + a konténer-logok) viszik tovább. Prodban jellemzően nem is a konzol a fő csatorna, hanem az OTLP log-bridge: a logok ugyanabba a pipeline-ba érkeznek, mint a trace-ek és metrikák, trace ID-val összekötve.
Ha lokálisan mégis fájlba akarod fogni: go run ./cmd/shop 2>&1 | tee shop.log: a monolog-handler pipe-ban automatikusan színek nélkül ír. (Dev módban a kérésenkénti access-log sorokat nem a slog, hanem a router saját request loggere írja (chimiddleware.Logger), ezek formátuma ezért nem követi a LOG_FORMAT-ot.)
Hogyan logolj a service-ekben
Két szabály van, és mindkettő a context.Context-ről szól:
func (s *OrderService) PlaceOrder(ctx context.Context, in PlaceOrder) error {
// 1. Mindig a *Context változatot, a kérés ctx-ével:
slog.InfoContext(ctx, "order placed",
slog.String("order_id", order.ID),
slog.Int("total", order.Total))
// ...
}
slog.InfoContext/WarnContext/ErrorContext, sosemslog.Info. A ctx hordozza az aktív trace-t: az otelslog bridge ebből fűzi a trace/span ID-t a rekordhoz, a Sentry handler ebből linkeli a hibát a trace waterfallhoz, és a breadcrumb-ok is a kérés hubjára kerülnek. Ctx nélküli log = korrelálatlan log.- Hibát
slog.Any("error", err)-rel adj át, neerr.Error()stringgel: így marad meg a strukturált lánc, a stack és a kód minden sink számára.
Loggert injektálni, konstruálni, DI-ba kötni nem kell: a chassis a slog.Default()-ot állítja be, a csomag-szintű slog.*Context hívások pont jók. Az ERROR szint pedig nem „hangos log": az egyben Sentry event is: 5xx-et a kit logol helyetted, te akkor logolj ERROR-t, ha valami tényleg incidens.
errs hiba 4xx-ként megy ki, request failed sor és Sentry event nélkül.