{"id":47685691,"url":"https://github.com/budistwn15/go-obskit","last_synced_at":"2026-04-02T14:49:11.077Z","repository":{"id":347083677,"uuid":"1192712712","full_name":"budistwn15/go-obskit","owner":"budistwn15","description":"go-obskit is a modular observability/logging toolkit for Go: structured logs, HTTP/DB/job tracing, secure by default,","archived":false,"fork":false,"pushed_at":"2026-03-27T04:37:37.000Z","size":156,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-27T06:25:49.773Z","etag":null,"topics":["go","golang","golang-package","logging"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/budistwn15.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-26T13:43:56.000Z","updated_at":"2026-03-27T04:36:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/budistwn15/go-obskit","commit_stats":null,"previous_names":["budistwn15/go-obskit"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/budistwn15/go-obskit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/budistwn15%2Fgo-obskit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/budistwn15%2Fgo-obskit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/budistwn15%2Fgo-obskit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/budistwn15%2Fgo-obskit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/budistwn15","download_url":"https://codeload.github.com/budistwn15/go-obskit/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/budistwn15%2Fgo-obskit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31308446,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","golang-package","logging"],"created_at":"2026-04-02T14:49:07.437Z","updated_at":"2026-04-02T14:49:11.066Z","avatar_url":"https://github.com/budistwn15.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-obskit\n\n`go-obskit` is a generic Go observability and logging toolkit built for shared use across many repositories.\n\nIt is modular: teams can adopt only what they need (logger only, HTTP adapters, outbound, DB, job logging, etc). It does not require ELK, OpenSearch, Loki, Datadog, Splunk, or any external collector. Logs go to stdout by default.\n\n## Features\n\n- Structured logging with `log/slog`\n- Stdout-first behavior by default\n- Correlation ID helpers for context propagation\n- Custom field enrichment patterns for app-specific metadata\n- Incoming HTTP logging abstraction (`httplog`)\n- Optional incoming adapters for `net/http`, Gin, and Fiber\n- Outbound HTTP observability via `http.RoundTripper`\n- Optional GORM logging integration\n- Job/scheduler/worker lifecycle logging\n- Sensitive header/JSON redaction helpers\n- Low-noise defaults with optional selective logging/sampling\n- Modular adoption across different app types\n\n## Installation\n\nCore module:\n\n```bash\ngo get github.com/budistwn15/go-obskit\ngo mod tidy\n```\n\nOptional adapters (pick only what you use):\n\n```bash\ngo get github.com/budistwn15/go-obskit/adapters/nethttp\ngo get github.com/budistwn15/go-obskit/adapters/ginx\ngo get github.com/budistwn15/go-obskit/adapters/fiberx\ngo get github.com/budistwn15/go-obskit/adapters/gormx\ngo get github.com/budistwn15/go-obskit/sinkenv\ngo get github.com/budistwn15/go-obskit/httpsink\ngo get github.com/budistwn15/go-obskit/lokisink\ngo mod tidy\n```\n\nNote: replace `OWNER/REPO` in the badge URL above with your actual repository path.\n\n## Environment Variables (.env)\n\nUse [`.env.example`](/Users/budisetiawan/Documents/bri/rsp/dependencies/obskit/.env.example) as minimal starter.\nIf you want Loki-only quick setup (5 keys), use [`.env.loki.example`](/Users/budisetiawan/Documents/bri/rsp/dependencies/obskit/.env.loki.example).\nFor advanced knobs, see [`.env.full.example`](/Users/budisetiawan/Documents/bri/rsp/dependencies/obskit/.env.full.example).\n\nImportant:\n- `obskit` **does not auto-read** `.env`.\n- Application should load env and map values into `logger.Config`, adapter options, sink configs (`sinkenv`/`elastic`/`httpsink`), `gormx.Options`, etc.\n\nMinimal vars:\n- `APP_NAME`\n- `APP_ENV`\n- `LOG_LEVEL`\n- `OBSKIT_SINK_PROVIDER` (`stdout` / `elastic` / `loki` / `http`)\n- `OBSKIT_ELASTIC_ENABLED` (default `false`)\n- `OBSKIT_ELASTIC_URL`\n- `OBSKIT_ELASTIC_INDEX`\n- `OBSKIT_ELASTIC_USERNAME` \n- `OBSKIT_ELASTIC_PASSWORD` \n\nFor full tracing / advanced tuning:\n- `OBSKIT_HTTP_FORENSIC=true`\n- `OBSKIT_GORM_TRACING=true`\n\nExample loader/config wiring:\n- [`examples/config-from-env/main.go`](/Users/budisetiawan/Documents/bri/rsp/dependencies/obskit/examples/config-from-env/main.go)\n\nSafe env injector utility (optional):\n\n```bash\ngo run github.com/budistwn15/go-obskit/cmd/obskit-envsync@latest\n```\n\nFull profile:\n\n```bash\ngo run github.com/budistwn15/go-obskit/cmd/obskit-envsync@latest -profile full\n```\n\nLoki minimal profile (5 keys):\n\n```bash\ngo run github.com/budistwn15/go-obskit/cmd/obskit-envsync@latest -profile loki\n```\n\nBehavior:\n- If `.env.example` exists: missing obskit keys are appended.\n- If `.env.example` does not exist: skip and exit success (no error).\n- Existing values are preserved (no override).\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/budistwn15/go-obskit/logger\"\n)\n\nfunc main() {\n\tlog := logger.New(logger.Config{\n\t\tServiceName: \"my-service\",\n\t\tEnvironment: \"local\",\n\t\tLevel:       logger.LevelInfo,\n\t})\n\n\tlog.Info(\"service started\", slog.String(\"component\", \"bootstrap\"))\n}\n```\n\n## Usage\n\n### logger\n\nUse `logger.New` to initialize the base logger.\n\n```go\nlog := logger.New(logger.Config{\n\tServiceName:    \"billing-api\",\n\tServiceVersion: \"1.3.0\",\n\tEnvironment:    \"production\",\n\tLevel:          logger.LevelInfo,\n\tAddSource:      false,\n})\n```\n\nStore/retrieve logger in context.\n\n```go\nctx = logger.WithContext(ctx, log)\nlog2 := logger.FromContext(ctx, log)\nlog2.Info(\"context logger ready\")\n```\n\nAdd reusable fields with `logger.WithCommon`.\n\n```go\nworkerLog := logger.WithCommon(log,\n\tslog.String(\"module\", \"invoice_worker\"),\n\tslog.String(\"region\", \"ap-southeast-1\"),\n)\nworkerLog.Info(\"worker initialized\")\n```\n\n### correlation\n\nSet and read correlation IDs explicitly.\n\n```go\nctx = correlation.WithID(ctx, \"corr-123\")\nid := correlation.ID(ctx)\n_ = id\n```\n\nGet existing or generate new ID.\n\n```go\nctx, corrID := correlation.GetOrGenerate(ctx)\n_ = corrID\n```\n\nGenerate standalone ID.\n\n```go\ncorrID := correlation.Generate()\n_ = corrID\n```\n\n### custom fields\n\nOne-off fields:\n\n```go\nlog.Info(\"invoice paid\",\n\tslog.String(\"tenant_id\", \"t-1\"),\n\tslog.String(\"invoice_id\", \"inv-9001\"),\n)\n```\n\nDerived logger with `.With(...)`:\n\n```go\ninvoiceLog := log.With(\n\tslog.String(\"tenant_id\", \"t-1\"),\n\tslog.String(\"feature_flag\", \"smart_retry\"),\n)\ninvoiceLog.Info(\"processing\")\n```\n\nContext-scoped fields:\n\n```go\nctx = logger.WithContextAttrs(ctx,\n\tslog.String(\"tenant_id\", \"t-1\"),\n\tslog.String(\"request_source\", \"partner-api\"),\n)\nctx = logger.AppendContextAttrs(ctx, slog.String(\"invoice_id\", \"inv-9001\"))\nlog.InfoContext(ctx, \"context-enriched log\")\n```\n\n### adapters/nethttp\n\nWrap standard `net/http` handlers with `nethttp.Middleware`.\n\n```go\nmux := http.NewServeMux()\nmux.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\t_, _ = w.Write([]byte(\"ok\"))\n})\n\nopts := nethttp.DefaultOptions()\nopts.SuccessSampleEvery = 10\nwrapped := nethttp.Middleware(log, opts)(mux)\n\n// incident mode (detail tinggi, opt-in)\nforensic := nethttp.ForensicOptions()\n_ = nethttp.Middleware(log, forensic)(mux)\n\n_ = http.ListenAndServe(\":8080\", wrapped)\n```\n\n### adapters/ginx\n\nUse as standard Gin middleware.\n\n```go\nr := gin.New()\nr.Use(ginx.Middleware(log, ginx.DefaultOptions()))\nr.GET(\"/health\", func(c *gin.Context) {\n\tc.Status(http.StatusOK)\n})\n```\n\n### adapters/fiberx\n\nUse as standard Fiber middleware.\n\n```go\napp := fiber.New()\napp.Use(fiberx.Middleware(log, fiberx.DefaultOptions()))\napp.Get(\"/health\", func(c *fiber.Ctx) error {\n\treturn c.SendStatus(fiber.StatusOK)\n})\n```\n\n### outbound HTTP logging\n\nWrap an existing transport/client with outbound observability.\n\n```go\ntr := outbound.NewTransport(nil, log, outbound.DefaultOptions())\nclient := \u0026http.Client{Transport: tr}\n\n// incident mode\nforensicTr := outbound.NewTransport(nil, log, outbound.ForensicOptions())\n_ = \u0026http.Client{Transport: forensicTr}\n\nreq, _ := http.NewRequest(http.MethodGet, \"https://example.com/health\", nil)\nresp, err := client.Do(req)\n_ = resp\n_ = err\n```\n\n### elastic (optional ELK/OpenSearch sink)\n\n```go\nelkMW := elastic.NewMiddleware(elastic.Config{\n\tEnabled:         true,\n\tElasticURL:      \"http://localhost:9200\",\n\tElasticIndex:    \"obskit-logs\",\n\tElasticUsername: \"elastic\",\n\tElasticPassword: \"secret\",\n\n\tIndexTimestampSuffix: true,    \n\tIndexTimestampLayout: \"2006.01.02\",\n\tIndexPattern:         \"obskit-logs-*\",\n\n\tBootstrap:               true,\n\tBootstrapOnStart:        true,\n\tPipelineName:            \"obskit-default-pipeline\",\n\tTemplateName:            \"obskit-default-template\",\n\tApplyPipelineToExisting: true,\n\n\tTimeout:       2 * time.Second,\n\tMaxRetries:    3,\n\tRetryBackoff:  150 * time.Millisecond,\n\tMaxBackoff:    2 * time.Second,\n\tQueueSize:     2048,\n\tBatchSize:     200,\n\tFlushInterval: 1 * time.Second,\n\n\tBlockOnQueueFull: false,\n\tEnableMonitor:    true,\n\tMonitorInterval:  15 * time.Second,\n})\n\nlog := logger.New(logger.Config{\n\tServiceName: \"my-service\",\n\tEnvironment: \"production\",\n\tMiddlewares: []logger.HandlerMiddleware{elkMW.LoggerMiddleware()},\n})\ndefer elkMW.Close(context.Background())\n```\n\n### sinkenv (provider from ENV, optional)\n\nSelect sink provider only via env:\n\n```env\nOBSKIT_SINK_PROVIDER=stdout   # stdout | elastic | loki | http\n```\n\n```go\nsink := sinkenv.FromEnv()\ndefer sink.Close(context.Background())\n\nlog := logger.New(logger.Config{\n\tServiceName: \"my-service\",\n\tEnvironment: \"production\",\n\tMiddlewares: sink.Middlewares,\n})\n```\n\n`stdout` remains default and safe fallback when provider/config is invalid.\n\n### httpsink (generic HTTP sink, optional)\n\nUse this for any backend that accepts HTTP log ingestion.\n\n```go\nmw := httpsink.NewMiddleware(httpsink.Config{\n\tEnabled:  true,\n\tEndpoint: \"https://collector.example.com/logs\",\n\tFormat:   httpsink.FormatNDJSON, // or FormatJSONArray\n\tHeaders: map[string]string{\n\t\t\"X-API-Key\": \"token\",\n\t},\n})\ndefer mw.Close(context.Background())\n```\n\n### lokisink (Grafana Loki, optional)\n\n```go\nmw := lokisink.NewMiddleware(lokisink.Config{\n\tEnabled:  true,\n\tEndpoint: \"http://localhost:3100\", // /loki/api/v1/push akan ditambahkan otomatis\n\tLabels: map[string]string{\n\t\t\"service\": \"billing-api\",\n\t\t\"env\":     \"production\",\n\t},\n})\ndefer mw.Close(context.Background())\n```\n\n### adapters/gormx\n\nAttach `gormx.New` as GORM logger implementation.\n\n```go\nopts := gormx.DefaultOptions()\nopts.LogSQLOnError = true  // default: true\nopts.LogSQLOnSlow = true   // default: true\nopts.LogSuccess = false    // default: low-noise\n\n// optional: attach app-specific query context for easier tracing\nopts.ErrorDetailFunc = func(ctx context.Context, err error, statement string, rows int64) map[string]any {\n\treturn map[string]any{\n\t\t\"query_name\": \"GetActiveUsers\",\n\t\t\"expected\":   \"non-empty result\",\n\t\t\"actual_rows\": rows,\n\t}\n}\n\ngormLog := gormx.New(log, opts)\ndb, err := gorm.Open(postgres.Open(dsn), \u0026gorm.Config{Logger: gormLog})\n_ = db\n_ = err\n```\n\n### job/scheduler logging\n\nUse `joblog.Start` and run methods for lifecycle logging.\n\n```go\nctx, run := joblog.Start(ctx, log, joblog.Meta{\n\tJobName:     \"daily-reconcile\",\n\tTriggerType: \"cron\",\n\tComponent:   \"scheduler\",\n\tOperation:   \"daily_reconcile\",\n})\n\nrun.AddProcessed(100)\nrun.AddSucceeded(99)\nrun.AddFailed(1)\nrun.Retry(joblog.RetryMeta{Attempt: 2, MaxAttempts: 3, Reason: \"timeout\"})\nrun.End(nil)\n\n_ = ctx\n```\n\n### errorsx\n\nWrap and extract layered error metadata.\n\n```go\nerr := errors.New(\"downstream timeout\")\nerr = errorsx.Wrap(err, errorsx.Meta{\n\tCode:      \"E_TIMEOUT\",\n\tType:      \"temporary\",\n\tLayer:     \"integration\",\n\tComponent: \"payment_client\",\n\tOperation: \"charge\",\n})\n\nif ex, ok := errorsx.Extract(err); ok {\n\tlog.Error(\"wrapped error\",\n\t\tslog.String(\"error.code\", ex.Meta.Code),\n\t\tslog.String(\"error.layer\", ex.Meta.Layer),\n\t)\n}\n```\n\n## Practical API Reference\n\n### logger\n\n- `logger.New(cfg Config) *slog.Logger`: create base logger with stdout fallback.\n- `logger.WithContext(ctx, log) context.Context`: put logger into context.\n- `logger.FromContext(ctx, fallback) *slog.Logger`: read logger from context.\n- `logger.WithCommon(log, attrs...) *slog.Logger`: derived logger with shared fields.\n- `logger.WithContextAttrs(ctx, attrs...) context.Context`: replace context attrs.\n- `logger.AppendContextAttrs(ctx, attrs...) context.Context`: append context attrs.\n- `logger.ContextAttrs(ctx) ([]slog.Attr, bool)`: read context attrs.\n\n### correlation\n\n- `correlation.WithID(ctx, id) context.Context`\n- `correlation.ID(ctx) string`\n- `correlation.GetOrGenerate(ctx) (context.Context, string)`\n- `correlation.Generate() string`\n\n### errorsx\n\n- `errorsx.Wrap(err, meta) error`\n- `errorsx.Extract(err) (*errorsx.Error, bool)`\n\n### incoming adapters\n\n- `nethttp.Middleware(log, opts) func(http.Handler) http.Handler`\n- `nethttp.ForensicOptions() Options`\n- `ginx.Middleware(log, opts) gin.HandlerFunc`\n- `ginx.ForensicOptions() Options`\n- `fiberx.Middleware(log, opts) fiber.Handler`\n- `fiberx.ForensicOptions() Options`\n\n### outbound\n\n- `outbound.NewTransport(base, log, opts) http.RoundTripper`\n- `outbound.ForensicOptions() Options`\n- `outbound.WrapClient(client, log, opts) *http.Client`\n\n### sinks\n\n- `sinkenv.FromEnv() Runtime`\n- `Runtime.Close(ctx) error`\n- `httpsink.NewMiddleware(cfg) *httpsink.Middleware`\n- `(*httpsink.Middleware).LoggerMiddleware() logger.HandlerMiddleware`\n- `(*httpsink.Middleware).Close(ctx) error`\n- `lokisink.NewMiddleware(cfg) *lokisink.Middleware`\n- `(*lokisink.Middleware).LoggerMiddleware() logger.HandlerMiddleware`\n- `(*lokisink.Middleware).Close(ctx) error`\n\n### gormx\n\n- `gormx.New(log, opts) gormlogger.Interface`\n- `gormx.TracingOptions() Options`\n- `gormx.WithQueryName(ctx, queryName) context.Context`\n- `gormx.WithExpected(ctx, expected) context.Context`\n\n### joblog\n\n- `joblog.Start(ctx, log, meta, opts...) (context.Context, *Run)`\n- `Run.End(err)`\n- `Run.Fail(err)`\n- `Run.Complete()`\n- `Run.Retry(meta RetryMeta)`\n- `Run.SetCounts(counts Counts)`\n- `Run.AddProcessed(n)`\n- `Run.AddSucceeded(n)`\n- `Run.AddFailed(n)`\n- `Run.AddSkipped(n)`\n- `Run.Logger() *slog.Logger`\n- `Run.Context() context.Context`\n\n## Custom Fields Patterns\n\nOne-off:\n\n```go\nlog.Info(\"batch closed\",\n\tslog.String(\"tenant_id\", \"t-1\"),\n\tslog.String(\"batch_id\", \"batch-20260326-001\"),\n)\n```\n\nDerived logger:\n\n```go\nbatchLog := log.With(\n\tslog.String(\"tenant_id\", \"t-1\"),\n\tslog.String(\"feature_flag\", \"reconcile_v2\"),\n)\nbatchLog.Info(\"start\")\n```\n\nRequest/job scoped:\n\n```go\nctx = logger.WithContextAttrs(ctx,\n\tslog.String(\"tenant_id\", \"t-1\"),\n\tslog.String(\"invoice_id\", \"inv-9001\"),\n\tslog.String(\"batch_id\", \"batch-20260326-001\"),\n)\nlog.InfoContext(ctx, \"scoped log\")\n```\n\n```go\n// incoming HTTP\ninOpts := nethttp.ForensicOptions()\ninOpts.SuccessSampleEvery = 5\n\n// outbound HTTP\noutOpts := outbound.ForensicOptions()\noutOpts.SuccessSampleEvery = 5\n\n// gorm\ndbOpts := gormx.TracingOptions()\ndbOpts.SuccessSampleEvery = 10\n```\n\n## Linting\n\nRun lint locally with `golangci-lint`:\n\n```bash\ngolangci-lint run ./...\n```\n\nFor adapter/examples modules:\n\n```bash\ncd adapters/nethttp\ngolangci-lint run ./...\n```\n\n```bash\ncd adapters/ginx\ngolangci-lint run ./...\n```\n\n```bash\ncd adapters/fiberx\ngolangci-lint run ./...\n```\n\n```bash\ncd adapters/gormx\ngolangci-lint run ./...\n```\n\n```bash\ncd examples\ngolangci-lint run ./...\n```\n\n\n## Example\n\nLogger basic:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:40.011Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"service started\",\n  \"service_name\": \"billing-api\",\n  \"service_version\": \"1.3.0\",\n  \"environment\": \"production\",\n  \"host\": \"node-a1\",\n  \"instance_id\": \"billing-api-7fd6\"\n}\n```\n\nLogger + custom fields:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:40.500Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"invoice paid\",\n  \"service_name\": \"billing-api\",\n  \"tenant_id\": \"t-1\",\n  \"invoice_id\": \"inv-9001\",\n  \"feature_flag\": \"smart_retry\"\n}\n```\n\nIncoming HTTP complete (net/http / Gin / Fiber):\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:44.102Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"http request completed\",\n  \"event\": \"http.request.complete\",\n  \"service_name\": \"billing-api\",\n  \"correlation_id\": \"corr-7f6a\",\n  \"http.method\": \"GET\",\n  \"http.path\": \"/v1/invoices\",\n  \"http.route\": \"/v1/invoices\",\n  \"http.status_code\": 200,\n  \"agent.name\": \"postman\",\n  \"agent.type\": \"api_client\",\n  \"agent.device\": \"desktop\",\n  \"source.ip\": \"10.10.1.54\",\n  \"source.port\": 53718,\n  \"target.host\": \"api.internal.local\",\n  \"target.port\": 443,\n  \"duration_ms\": 23,\n  \"slow\": false,\n  \"threshold_ms\": 1000\n}\n```\n\nIncoming HTTP error:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:44.900Z\",\n  \"level\": \"ERROR\",\n  \"msg\": \"http request failed\",\n  \"event\": \"http.request.error\",\n  \"service_name\": \"billing-api\",\n  \"correlation_id\": \"corr-7f6a\",\n  \"http.method\": \"POST\",\n  \"http.path\": \"/v1/charge\",\n  \"http.status_code\": 500,\n  \"error.kind\": \"http_error\",\n  \"error.message\": \"Internal Server Error\",\n  \"duration_ms\": 145,\n  \"slow\": false,\n  \"threshold_ms\": 1000\n}\n```\n\nOutbound HTTP complete:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:45.301Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"outbound http request completed\",\n  \"event\": \"http.outbound.complete\",\n  \"service_name\": \"billing-api\",\n  \"layer\": \"integration\",\n  \"component\": \"http_client\",\n  \"operation\": \"outbound_request\",\n  \"correlation_id\": \"corr-7f6a\",\n  \"http.method\": \"GET\",\n  \"http.host\": \"profile.internal\",\n  \"http.status_code\": 200,\n  \"duration_ms\": 18,\n  \"slow\": false,\n  \"threshold_ms\": 1000\n}\n```\n\nOutbound HTTP error:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:45.781Z\",\n  \"level\": \"ERROR\",\n  \"msg\": \"outbound http request failed\",\n  \"event\": \"http.outbound.error\",\n  \"service_name\": \"billing-api\",\n  \"layer\": \"integration\",\n  \"component\": \"http_client\",\n  \"operation\": \"outbound_request\",\n  \"correlation_id\": \"corr-7f6a\",\n  \"http.method\": \"POST\",\n  \"http.host\": \"payments.internal\",\n  \"error.kind\": \"context_deadline_exceeded\",\n  \"error.message\": \"context deadline exceeded\",\n  \"duration_ms\": 2001,\n  \"slow\": true,\n  \"threshold_ms\": 1000\n}\n```\n\nGORM query complete (jika `LogSuccess=true`):\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:46.001Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"gorm query\",\n  \"event\": \"db.query.complete\",\n  \"service_name\": \"billing-api\",\n  \"layer\": \"repository\",\n  \"component\": \"gorm\",\n  \"operation\": \"db.query\",\n  \"db.system\": \"gorm\",\n  \"db.rows_affected\": 3,\n  \"db.result_status\": \"success\",\n  \"db.statement\": \"SELECT * FROM invoices WHERE status='paid'\",\n  \"db.statement_truncated\": false,\n  \"duration_ms\": 12\n}\n```\n\nGORM slow query:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:46.119Z\",\n  \"level\": \"WARN\",\n  \"msg\": \"gorm slow query\",\n  \"event\": \"db.query.slow\",\n  \"service_name\": \"billing-api\",\n  \"layer\": \"repository\",\n  \"component\": \"gorm\",\n  \"operation\": \"db.query\",\n  \"correlation_id\": \"corr-7f6a\",\n  \"db.system\": \"gorm\",\n  \"db.rows_affected\": 124,\n  \"db.result_status\": \"success\",\n  \"db.statement\": \"SELECT * FROM users WHERE status = 'active'\",\n  \"db.statement_truncated\": false,\n  \"duration_ms\": 612,\n  \"slow\": true,\n  \"threshold_ms\": 250\n}\n```\n\nGORM error:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:46.401Z\",\n  \"level\": \"ERROR\",\n  \"msg\": \"gorm query error\",\n  \"event\": \"db.query.error\",\n  \"service_name\": \"billing-api\",\n  \"layer\": \"repository\",\n  \"component\": \"gorm\",\n  \"operation\": \"db.query\",\n  \"db.query_type\": \"UPDATE\",\n  \"db.statement\": \"UPDATE invoices SET status='paid' WHERE id='inv-9001'\",\n  \"db.statement_truncated\": false,\n  \"db.result_status\": \"error\",\n  \"error.kind\": \"db_error\",\n  \"error.message\": \"deadlock detected\",\n  \"error.expected\": \"single-row update committed\",\n  \"error.actual\": \"deadlock detected by database\",\n  \"error.details\": {\n    \"query_name\": \"MarkInvoicePaid\",\n    \"retryable\": true\n  },\n  \"duration_ms\": 41\n}\n```\n\nJob started:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:46.700Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"job started\",\n  \"event\": \"job.started\",\n  \"service_name\": \"billing-worker\",\n  \"layer\": \"job\",\n  \"job.run_id\": \"job_7fd1c2a3\",\n  \"job.name\": \"daily-reconcile\",\n  \"job.trigger_type\": \"cron\"\n}\n```\n\nJob completed:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:47.004Z\",\n  \"level\": \"INFO\",\n  \"msg\": \"job completed\",\n  \"event\": \"job.completed\",\n  \"service_name\": \"billing-worker\",\n  \"layer\": \"job\",\n  \"job.run_id\": \"job_7fd1c2a3\",\n  \"job.name\": \"daily-reconcile\",\n  \"duration_ms\": 1840,\n  \"slow\": false,\n  \"threshold_ms\": 5000,\n  \"job.count.processed\": 1000,\n  \"job.count.succeeded\": 998,\n  \"job.count.failed\": 2\n}\n```\n\nJob retry:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:47.110Z\",\n  \"level\": \"WARN\",\n  \"msg\": \"job retry\",\n  \"event\": \"job.retry\",\n  \"service_name\": \"billing-worker\",\n  \"layer\": \"job\",\n  \"job.run_id\": \"job_7fd1c2a3\",\n  \"job.retry_attempt\": 2,\n  \"job.retry_max_attempts\": 3,\n  \"job.retry_reason\": \"timeout\",\n  \"job.retry_delay_ms\": 30000\n}\n```\n\nJob failed:\n\n```json\n{\n  \"time\": \"2026-03-26T13:12:47.220Z\",\n  \"level\": \"ERROR\",\n  \"msg\": \"job failed\",\n  \"event\": \"job.failed\",\n  \"service_name\": \"billing-worker\",\n  \"layer\": \"job\",\n  \"job.run_id\": \"job_7fd1c2a3\",\n  \"duration_ms\": 1840,\n  \"error.message\": \"downstream timeout\",\n  \"error.code\": \"E_TIMEOUT\",\n  \"error.layer\": \"integration\",\n  \"error.component\": \"payment_client\",\n  \"error.operation\": \"charge\"\n}\n```\n\nredaction:\n\n```json\n{\n  \"event\": \"http.request.complete\",\n  \"http.request.headers\": {\n    \"authorization\": \"***redacted***\",\n    \"x-api-key\": \"***redacted***\",\n    \"content-type\": \"application/json\"\n  },\n  \"http.request.body\": \"{\\\"username\\\":\\\"john\\\",\\\"password\\\":\\\"***redacted***\\\",\\\"token\\\":\\\"***redacted***\\\"}\"\n}\n```\n\n## Testing\n\nRun all tests:\n\n```bash\ngo test ./...\n```\n\nRun race tests:\n\n```bash\ngo test -race ./...\n```\n\nFor adapter submodules:\n\n```bash\ncd adapters/nethttp \u0026\u0026 go test ./...\ncd ../ginx \u0026\u0026 go test ./...\ncd ../fiberx \u0026\u0026 go test ./...\ncd ../gormx \u0026\u0026 go test ./...\n```\n\n## License\n\nThis project is licensed under the MIT License.\nSee [LICENSE](LICENSE).\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbudistwn15%2Fgo-obskit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbudistwn15%2Fgo-obskit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbudistwn15%2Fgo-obskit/lists"}