{"id":37161087,"url":"https://github.com/rafaelespinoza/slogtesting","last_synced_at":"2026-01-14T19:10:24.898Z","repository":{"id":324474199,"uuid":"1097353473","full_name":"rafaelespinoza/slogtesting","owner":"rafaelespinoza","description":"test structured logging with log/slog values","archived":false,"fork":false,"pushed_at":"2025-12-30T16:53:01.000Z","size":1898,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-03T12:06:25.370Z","etag":null,"topics":["slog","slog-handler","structured-logging","testing"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/rafaelespinoza/slogtesting","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/rafaelespinoza.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"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":"2025-11-16T02:20:17.000Z","updated_at":"2025-12-20T00:02:46.000Z","dependencies_parsed_at":"2025-12-20T05:04:51.621Z","dependency_job_id":null,"html_url":"https://github.com/rafaelespinoza/slogtesting","commit_stats":null,"previous_names":["rafaelespinoza/slogtesting"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/rafaelespinoza/slogtesting","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafaelespinoza%2Fslogtesting","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafaelespinoza%2Fslogtesting/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafaelespinoza%2Fslogtesting/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafaelespinoza%2Fslogtesting/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rafaelespinoza","download_url":"https://codeload.github.com/rafaelespinoza/slogtesting/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafaelespinoza%2Fslogtesting/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28431591,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T18:57:19.464Z","status":"ssl_error","status_checked_at":"2026-01-14T18:52:48.501Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6: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":["slog","slog-handler","structured-logging","testing"],"created_at":"2026-01-14T19:10:24.157Z","updated_at":"2026-01-14T19:10:24.889Z","avatar_url":"https://github.com/rafaelespinoza.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"slogtesting\n---\n\n[![](https://pkg.go.dev/badge/github.com/rafaelespinoza/slogtesting)](https://pkg.go.dev/github.com/rafaelespinoza/slogtesting)\n[![tests](https://github.com/rafaelespinoza/slogtesting/actions/workflows/tests.yaml/badge.svg)](https://github.com/rafaelespinoza/slogtesting/actions/workflows/tests.yaml)\n[![codecov](https://codecov.io/gh/rafaelespinoza/slogtesting/graph/badge.svg?token=ZL2BP29ZVX)](https://codecov.io/gh/rafaelespinoza/slogtesting)\n\n`slogtesting` is a golang library to test that your application's structured\nlogging outputs the intended data.\nIt requires the use of [log/slog](https://pkg.go.dev/log/slog).\n\n![the golang gopher wearing a labcoat, inspecting a log](./.assets/gopher.png)\n\nThis library operates on the interface level and on the outputs. It has a\n`slog.Handler` implementation (the backend) made to intercept messages from the\napplication's `*slog.Logger`(the frontend), and outputs golang values rather\nthan log entries in a particular format. When you're testing values at a higher\nabstraction level, your tests do not need to be concerned with interpreting log\nentries in their format.\n\n## Why\n\nThis begs the question, why test logs in the first place?\n\n* To check that sensitive information, such as PII or credentials, is redacted\n  or not logged at all. Example: the application is handling this kind of data,\n  and you need tests for this behavior.\n* To check that a log is emitted with sufficient contextual data to be useful;\n  IDs, paths, etc. Example: the logs are sent to some monitoring tool that\n  alerts when it sees messages in some pattern and you want a unit test close\n  to the application.\n* To check that logs have an appropriate logging level. Example: oh, this\n  critical error occurred, so we're going to need something more obvious than a\n  typical INFO log here.\n\nLogs can be difficult to test. They're often overlooked in tests because\nthey're a side effect of something else more important: the thing that's worth\nlogging in the first place. Yet, logs are an essential component of\nobservability for any modern application. This library aims to make testing\nlogs easier.\n\n## Features\n\n* In-Memory Capture: Provides a `slog.Handler` that captures `slog.Record`\n  golang values. This eases testing because there is no need to parse the\n  formatted data. Instead, work directly with golang data structures.\n* Simple: Integrates with the standard library's `*slog.Logger`.\n* High-Level Checks: Includes helpers like `HasAttr` to simplify checking\n  for specific key-value pairs in the captured logs. Use the `InGroup` check to\n  compose many checks together in the expected shape of your data.\n* Concurrency-Safe: Built to ensure that only 1 record is captured at a time in\n  its entirety.\n\nThe said handler adheres to the same tests ([testing/slogtest](https://pkg.go.dev/testing/slogtest))\nrun against the standard library handlers `slog.TextHandler` and `slog.JSONHandler`.\n\nAlso provided are a set of `Check` functions to test attributes. They're higher\norder functions: the input specifies something about a target attribute and the\nreturn is a check function to run against a list of attributes. If the check\nfails, an error is returned. This functional approach works well when you want\nto inspect group attributes.\n\n## The `testing` package\n\nThough this is a testing library, at this time the API doesn't use `testing`.\nThis is because I couldn't figure out how to write tests that fail\nsuccessfully. That is, you can have tests that demonstrate the true negative\ncase, but AFAIK you can't run them automatically, as actual tests, without the\nbuild failing. Those tests are designed to fail. For this reason, a `Check`\nreturns an error, which is very easy to test.\n\nIt's easy to adapt a `Check` to use `testing`.\n```go\npackage main_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\tst \"github.com/rafaelespinoza/slogtesting\"\n)\n\nfunc TestSimpleExample(t *testing.T) {\n\tattrs := []slog.Attr{slog.String(\"foo\", \"bar\")}\n\tchecks := []st.Check{\n\t\tst.HasKey(\"foo\"),\n\t\tst.MissingKey(\"bar\"),\n\t\tst.HasAttr(slog.String(\"foo\", \"bar\")),\n\t}\n\tfor _, check := range checks {\n\t\terr := check(attrs)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n}\n```\n\nHere's a more complete example using `testing`:\n```go\npackage main_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\tst \"github.com/rafaelespinoza/slogtesting\"\n)\n\nfunc TestCompleteExample(t *testing.T) {\n\t// Configure the handler if needed.\n\topts := st.AttrHandlerOptions{HandlerOptions: slog.HandlerOptions{Level: slog.LevelInfo}}\n\n\t// This function is the part of your application to test. It's passed a\n\t// slog.Handler built to capture every processed slog.Record. This example\n\t// accumulates some data and outputs a record at the INFO level.\n\trun := func(h slog.Handler) error {\n\t\tlogger := slog.New(h)\n\n\t\t// This output invocation will be recorded b/c the handler's logging\n\t\t// level will allow calls at the INFO level.\n\t\t//\n\t\t// If the handler was a *slog.TextHandler, the output would look similar to:\n\t\t// \ttime=2006-01-02T15:04:05.012Z level=INFO msg=msg a=b G.c=d G.H.e=f\n\t\tlogger.With(\"a\", \"b\").WithGroup(\"G\").With(\"c\", \"d\").WithGroup(\"H\").Info(\"msg\", \"e\", \"f\")\n\n\t\t// This output won't be recorded b/c the level of the underlying handler\n\t\t// is above DEBUG.\n\t\tlogger.Debug(\"You won't see me\")\n\n\t\treturn nil\n\t}\n\trecords, err := st.CaptureRecords(\u0026opts, run)\n\tif err != nil {\n\t\tt.Fatalf(\"unexpected error while capturing records: %v\", err.Error())\n\t}\n\tif len(records) != 1 {\n\t\tt.Fatalf(\"wrong number of captured records; got %d, expected %d\", len(records), 1)\n\t}\n\n\t// This is the output data to inspect. Collect the attributes for each\n\t// record that was output by the logger. It will also include the built-in\n\t// attributes: time, level, message.\n\tattrs := st.GetRecordAttrs(records[0])\n\n\t// Run these tests.\n\ttests := []struct {\n\t\tname  string\n\t\tcheck st.Check\n\t}{\n\t\t{\n\t\t\tname:  \"key \" + slog.TimeKey,\n\t\t\tcheck: st.HasKey(slog.TimeKey),\n\t\t},\n\t\t{\n\t\t\tname:  \"key \" + slog.LevelKey,\n\t\t\tcheck: st.HasKey(slog.LevelKey),\n\t\t},\n\t\t{\n\t\t\tname:  \"attribute with key \" + slog.MessageKey,\n\t\t\tcheck: st.HasAttr(slog.String(slog.MessageKey, \"msg\")),\n\t\t},\n\t\t{\n\t\t\tname:  \"attribute with key a\",\n\t\t\tcheck: st.HasAttr(slog.String(\"a\", \"b\")),\n\t\t},\n\t\t{\n\t\t\tname:  \"group G and attribute with key c\",\n\t\t\tcheck: st.InGroup(\"G\", st.HasAttr(slog.String(\"c\", \"d\"))),\n\t\t},\n\t\t{\n\t\t\tname:  \"group G, another group H and attribute with key e\",\n\t\t\tcheck: st.InGroup(\"G\", st.InGroup(\"H\", st.HasAttr(slog.String(\"e\", \"f\")))),\n\t\t},\n\t\t{\n\t\t\tname:  \"should not find attribute with key z\",\n\t\t\tcheck: st.MissingKey(\"z\"),\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\terr := test.check(attrs)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frafaelespinoza%2Fslogtesting","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frafaelespinoza%2Fslogtesting","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frafaelespinoza%2Fslogtesting/lists"}