{"id":19866099,"url":"https://github.com/veqryn/slog-context","last_synced_at":"2025-04-05T17:18:24.295Z","repository":{"id":207529132,"uuid":"716785366","full_name":"veqryn/slog-context","owner":"veqryn","description":"Use golang structured logging (slog) with context. Add and retrieve logger to and from context. Add attributes to context. Automatically read any custom context values, such as OpenTelemetry TraceID.","archived":false,"fork":false,"pushed_at":"2025-04-03T07:11:36.000Z","size":205,"stargazers_count":70,"open_issues_count":1,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-03T08:24:09.752Z","etag":null,"topics":["context","golang","golang-library","logging","opentelemetry","slog","structured-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/veqryn.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":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-09T21:43:51.000Z","updated_at":"2025-03-23T20:30:13.000Z","dependencies_parsed_at":null,"dependency_job_id":"7ddd1cf7-fab7-4e68-bff5-985c92ba2a13","html_url":"https://github.com/veqryn/slog-context","commit_stats":null,"previous_names":["veqryn/slog-context"],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veqryn%2Fslog-context","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veqryn%2Fslog-context/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veqryn%2Fslog-context/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/veqryn%2Fslog-context/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/veqryn","download_url":"https://codeload.github.com/veqryn/slog-context/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247369953,"owners_count":20927928,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["context","golang","golang-library","logging","opentelemetry","slog","structured-logging"],"created_at":"2024-11-12T15:25:00.296Z","updated_at":"2025-04-05T17:18:24.288Z","avatar_url":"https://github.com/veqryn.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# slog-context\n[![tag](https://img.shields.io/github/tag/veqryn/slog-context.svg)](https://github.com/veqryn/slog-context/releases)\n![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.21-%23007d9c)\n[![GoDoc](https://godoc.org/github.com/veqryn/slog-context?status.svg)](https://pkg.go.dev/github.com/veqryn/slog-context)\n![Build Status](https://github.com/veqryn/slog-context/actions/workflows/build_and_test.yml/badge.svg)\n[![Go report](https://goreportcard.com/badge/github.com/veqryn/slog-context)](https://goreportcard.com/report/github.com/veqryn/slog-context)\n[![Coverage](https://img.shields.io/codecov/c/github/veqryn/slog-context)](https://codecov.io/gh/veqryn/slog-context)\n[![Contributors](https://img.shields.io/github/contributors/veqryn/slog-context)](https://github.com/veqryn/slog-context/graphs/contributors)\n[![License](https://img.shields.io/github/license/veqryn/slog-context)](./LICENSE)\n\nUse golang structured logging (slog) with context.\nAdd and retrieve logger to and from context.\nAdd attributes to context.\nAutomatically read any custom context values, such as OpenTelemetry TraceID.\n\nThis library supports two different workflows for using slog and context.\nThese workflows can be used individually or together at the same time.\n\n#### Attributes Extracted from Context Workflow:\n\nUsing the `Handler` lets us `Prepend` and `Append` attributes to\nlog lines, even when a logger is not passed into a function or in code we don't\ncontrol. This is done without storing the logger in the context; instead the\nattributes are stored in the context and the Handler picks them up later\nwhenever a new log line is written.\n\nIn that same workflow, the `HandlerOptions` and `AttrExtractor` types let us\nextract any custom values from a context and have them automatically be\nprepended or appended to all log lines using that context. By default, there are\nextractors for anything added via `Prepend` and `Append`, but this repository\ncontains some optional Extractors that can be added:\n* `slogotel.ExtractTraceSpanID` extractor will automatically extract the OTEL\n(OpenTelemetry) TraceID and SpanID, and add them to the log record, while also\nannotating the Span with an error code if the log is at error level.\n* `sloghttp.ExtractAttrCollection` extractor will automatically add to the log\nrecord any attributes added by `sloghttp.With` after the `sloghttp.AttrCollection`\nhttp middleware. This allows other middlewares to log with attributes that would\nnormally be out of scope, because they were added by a later middleware or the\nfinal http handler in the chain.\n\n#### Logger in Context Workflow:\n\nUsing `NewCtx` and `FromCtx` lets us store the logger itself within a context,\nand get it back out again. Wrapper methods `With`/`WithGroup`/`Debug`/`Info`/\n`Warn`/`Error`/`Log`/`LogAttrs` let us work directly with a logger residing\nwith the context (or the default logger if no logger is stored in the context).\n\n#### Compatibility with both Slog and Logr\nslog-context is compatible with both standard library [slog](https://pkg.go.dev/log/slog)\nand with [logr](https://github.com/go-logr/logr), which is an alternative\nlogging api/interface/frontend.\n\nIf only slog is used, only `*slog.Logger`'s will be stored in the context.\nIf both slog and logr are used, `*slog.Logger` will be automatically converted\nto a `logr.Logger` as needed, and vice versa. This allows full interoperability\ndown the stack and with any libraries that use either slog-context or logr.\n\n### Other Great SLOG Utilities\n- [slogctx](https://github.com/veqryn/slog-context): Add attributes to context and have them automatically added to all log lines. Work with a logger stored in context.\n- [slogotel](https://github.com/veqryn/slog-context/tree/main/otel): Automatically extract and add [OpenTelemetry](https://opentelemetry.io/) TraceID's to all log lines.\n- [sloggrpc](https://github.com/veqryn/slog-context/tree/main/grpc): Instrument [GRPC](https://grpc.io/) with automatic logging of all requests and responses.\n- [slogdedup](https://github.com/veqryn/slog-dedup): Middleware that deduplicates and sorts attributes. Particularly useful for JSON logging. Format logs for aggregators (Graylog, GCP/Stackdriver, etc).\n- [slogbugsnag](https://github.com/veqryn/slog-bugsnag): Middleware that pipes Errors to [Bugsnag](https://www.bugsnag.com/).\n- [slogjson](https://github.com/veqryn/slog-json): Formatter that uses the [JSON v2](https://github.com/golang/go/discussions/63397) [library](https://github.com/go-json-experiment/json), with optional single-line pretty-printing.\n\n## Install\n\n```\ngo get github.com/veqryn/slog-context\n```\n\n```go\nimport (\n\tslogctx \"github.com/veqryn/slog-context\"\n)\n```\n\n## Usage\n[Examples in repo](examples/)\n### Logger in Context Workflow\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"os\"\n\n\tslogctx \"github.com/veqryn/slog-context\"\n)\n\n// This workflow has us pass the *slog.Logger around inside a context.Context.\n// This lets us add attributes and groups to the logger, while naturally\n// keeping the logger scoped just like the context itself is scoped.\n//\n// This eliminates the need to use the default package-level slog, and also\n// eliminates the need to add a *slog.Logger as yet another argument to all\n// functions.\n//\n// You can still get the Logger out of the context at any time, and pass it\n// around manually if needed, but since contexts are already passed to most\n// functions, passing the logger explicitly is now optional.\n//\n// Attributes and key-value pairs like request-id, trace-id, user-id, etc, can\n// be added to the logger in the context, and as the context propagates the\n// logger and its attributes will propagate with it, adding these to any log\n// lines using that context.\nfunc main() {\n\th := slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)\n\tslog.SetDefault(slog.New(h))\n\n\t// Store the logger inside the context:\n\tctx := slogctx.NewCtx(context.Background(), slog.Default())\n\n\t// Get the logger back out again at any time, for manual usage:\n\tlog := slogctx.FromCtx(ctx)\n\tlog.Warn(\"warning\")\n\t/*\n\t\t{\n\t\t\t\"time\":\"2023-11-14T00:53:46.361201-07:00\",\n\t\t\t\"level\":\"INFO\",\n\t\t\t\"msg\":\"warning\"\n\t\t}\n\t*/\n\n\t// Add attributes directly to the logger in the context:\n\tctx = slogctx.With(ctx, \"rootKey\", \"rootValue\")\n\n\t// Create a group directly on the logger in the context:\n\tctx = slogctx.WithGroup(ctx, \"someGroup\")\n\n\t// With and wrapper methods have the same args signature as slog methods,\n\t// and can take a mix of slog.Attr and key-value pairs.\n\tctx = slogctx.With(ctx, slog.String(\"subKey\", \"subValue\"), slog.Bool(\"someBool\", true))\n\n\terr := errors.New(\"an error\")\n\n\t// Access the logger in the context directly with handy wrappers for Debug/Info/Warn/Error/Log/LogAttrs:\n\tslogctx.Error(ctx, \"main message\",\n\t\tslogctx.Err(err),\n\t\tslog.String(\"mainKey\", \"mainValue\"))\n\t/*\n\t\t{\n\t\t\t\"time\":\"2023-11-14T00:53:46.363072-07:00\",\n\t\t\t\"level\":\"ERROR\",\n\t\t\t\"msg\":\"main message\",\n\t\t\t\"rootKey\":\"rootValue\",\n\t\t\t\"someGroup\":{\n\t\t\t\t\"subKey\":\"subValue\",\n\t\t\t\t\"someBool\":true,\n\t\t\t\t\"err\":\"an error\",\n\t\t\t\t\"mainKey\":\"mainValue\"\n\t\t\t}\n\t\t}\n\t*/\n}\n```\n\n\n### Attributes Extracted from Context Workflow\n#### Append and Prepend\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"os\"\n\n\tslogctx \"github.com/veqryn/slog-context\"\n)\n\n// This workflow lets us use slog as normal, while adding the ability to put\n// slog attributes into the context which will then show up at the start or end\n// of log lines.\n//\n// This is useful when you are not passing a *slog.Logger around to different\n// functions (because you are making use of the default package-level slog),\n// but you are passing a context.Context around.\n//\n// This can also be used when a library or vendor code you don't control is\n// using the default log methods, default logger, or doesn't accept a slog\n// Logger to all functions you wish to add attributes to.\n//\n// Attributes and key-value pairs like request-id, trace-id, user-id, etc, can\n// be added to the context, and the *slogctx.Handler will make sure they\n// are prepended to the start, or appended to the end, of any log lines using\n// that context.\nfunc main() {\n\t// Create the *slogctx.Handler middleware\n\th := slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)\n\tslog.SetDefault(slog.New(h))\n\n\tctx := context.Background()\n\n\t// Prepend some slog attributes to the start of future log lines:\n\tctx = slogctx.Prepend(ctx, \"prependKey\", \"prependValue\")\n\n\t// Append some slog attributes to the end of future log lines:\n\t// Prepend and Append have the same args signature as slog methods,\n\t// and can take a mix of slog.Attr and key-value pairs.\n\tctx = slogctx.Append(ctx, slog.String(\"appendKey\", \"appendValue\"))\n\n\t// Use the logger like normal:\n\tslog.WarnContext(ctx, \"main message\", \"mainKey\", \"mainValue\")\n\t/*\n\t\t{\n\t\t\t\"time\": \"2023-11-15T18:43:23.290798-07:00\",\n\t\t\t\"level\": \"WARN\",\n\t\t\t\"msg\": \"main message\",\n\t\t\t\"prependKey\": \"prependValue\",\n\t\t\t\"mainKey\": \"mainValue\",\n\t\t\t\"appendKey\": \"appendValue\"\n\t\t}\n\t*/\n\n\t// Use the logger like normal; add attributes, create groups, pass it around:\n\tlog := slog.With(\"rootKey\", \"rootValue\")\n\tlog = log.WithGroup(\"someGroup\")\n\tlog = log.With(\"subKey\", \"subValue\")\n\n\t// The prepended/appended attributes end up in all log lines that use that context\n\tlog.InfoContext(ctx, \"main message\", \"mainKey\", \"mainValue\")\n\t/*\n\t\t{\n\t\t\t\"time\": \"2023-11-14T00:37:03.805196-07:00\",\n\t\t\t\"level\": \"INFO\",\n\t\t\t\"msg\": \"main message\",\n\t\t\t\"prependKey\": \"prependValue\",\n\t\t\t\"rootKey\": \"rootValue\",\n\t\t\t\"someGroup\": {\n\t\t\t\t\"subKey\": \"subValue\",\n\t\t\t\t\"mainKey\": \"mainValue\",\n\t\t\t\t\"appendKey\": \"appendValue\"\n\t\t\t}\n\t\t}\n\t*/\n}\n```\n\n#### Custom Context Value Extractor\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\tslogctx \"github.com/veqryn/slog-context\"\n)\n\ntype ctxKey struct{}\n\nfunc customExtractor(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr {\n\tif v, ok := ctx.Value(ctxKey{}).(string); ok {\n\t\treturn []slog.Attr{slog.String(\"my-key\", v)}\n\t}\n\treturn nil\n}\n\n// This workflow lets us use slog as normal, while letting us extract any\n// custom values we want from any context, and having them added to the start\n// or end of the log record.\nfunc main() {\n\t// Create the *slogctx.Handler middleware\n\th := slogctx.NewHandler(\n\t\tslog.NewJSONHandler(os.Stdout, nil), // The next handler in the chain\n\t\t\u0026slogctx.HandlerOptions{\n\t\t\t// Prependers stays as default (leaving as nil would accomplish the same)\n\t\t\tPrependers: []slogctx.AttrExtractor{\n\t\t\t\tslogctx.ExtractPrepended,\n\t\t\t},\n\t\t\t// Appenders first appends anything added with slogctx.Append,\n\t\t\t// then appends our custom ctx value\n\t\t\tAppenders: []slogctx.AttrExtractor{\n\t\t\t\tslogctx.ExtractAppended,\n\t\t\t\tcustomExtractor,\n\t\t\t},\n\t\t},\n\t)\n\tslog.SetDefault(slog.New(h))\n\n\t// Add a value to the context\n\tctx := context.WithValue(context.Background(), ctxKey{}, \"my-value\")\n\n\t// Use the logger like normal:\n\tslog.WarnContext(ctx, \"main message\", \"mainKey\", \"mainValue\")\n\t/*\n\t\t{\n\t\t\t\"time\": \"2023-11-17T04:35:30.333732-07:00\",\n\t\t\t\"level\": \"WARN\",\n\t\t\t\"msg\": \"main message\",\n\t\t\t\"mainKey\": \"mainValue\",\n\t\t\t\"my-key\": \"my-value\"\n\t\t}\n\t*/\n}\n```\n\n#### OpenTelemetry TraceID SpanID Extractor\nIn order to avoid making all users of this repo require all the OTEL libraries,\nthe OTEL extractor is in a separate module in this repo:\n\n`go get github.com/veqryn/slog-context/otel`\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\tslogctx \"github.com/veqryn/slog-context\"\n\tslogotel \"github.com/veqryn/slog-context/otel\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/stdout/stdouttrace\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.24.0\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\nfunc init() {\n\t// Create the *slogctx.Handler middleware\n\th := slogctx.NewHandler(\n\t\tslog.NewJSONHandler(os.Stdout, nil), // The next handler in the chain\n\t\t\u0026slogctx.HandlerOptions{\n\t\t\t// Prependers will first add the OTEL Trace ID,\n\t\t\t// then anything else Prepended to the ctx\n\t\t\tPrependers: []slogctx.AttrExtractor{\n\t\t\t\tslogotel.ExtractTraceSpanID,\n\t\t\t\tslogctx.ExtractPrepended,\n\t\t\t},\n\t\t\t// Appenders stays as default (leaving as nil would accomplish the same)\n\t\t\tAppenders: []slogctx.AttrExtractor{\n\t\t\t\tslogctx.ExtractAppended,\n\t\t\t},\n\t\t},\n\t)\n\tslog.SetDefault(slog.New(h))\n\n\tsetupOTEL()\n}\n\nfunc main() {\n\t// Handle OTEL shutdown properly so nothing leaks\n\tdefer traceProvider.Shutdown(context.Background())\n\n\tslog.Info(\"Starting server. Please run: curl localhost:8080/hello\")\n\n\t// Demonstrate the slogotel.ExtractTraceSpanID with a http server\n\thttp.HandleFunc(\"/hello\", helloHandler)\n\terr := http.ListenAndServe(\":8080\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// helloHandler starts an OTEL Span, then begins a long-running calculation.\n// The calculation will fail, and the logging at Error level will mark the span\n// as codes.Error.\nfunc helloHandler(w http.ResponseWriter, r *http.Request) {\n\tctx, span := tracer.Start(r.Context(), \"helloHandler\")\n\tdefer span.End()\n\n\tslogctx.Info(ctx, \"starting long calculation...\")\n\t/*\n\t\t{\n\t\t\t\"time\": \"2023-11-17T03:11:20.584592-07:00\",\n\t\t\t\"level\": \"INFO\",\n\t\t\t\"msg\": \"starting long calculation...\",\n\t\t\t\"TraceID\": \"15715df45965b4a2db6dc103a76e52ae\",\n\t\t\t\"SpanID\": \"76d364cdd598c895\"\n\t\t}\n\t*/\n\n\ttime.Sleep(5 * time.Second)\n\tslogctx.Error(ctx, \"something failed...\")\n\t/*\n\t\t{\n\t\t\t\"time\": \"2023-11-17T03:11:25.586464-07:00\",\n\t\t\t\"level\": \"ERROR\",\n\t\t\t\"msg\": \"something failed...\",\n\t\t\t\"TraceID\": \"15715df45965b4a2db6dc103a76e52ae\",\n\t\t\t\"SpanID\": \"76d364cdd598c895\"\n\t\t}\n\t*/\n\n\tw.WriteHeader(http.StatusInternalServerError)\n\n\t// The OTEL exporter will soon after output the trace, which will include this and much more:\n\t/*\n\t\t{\n\t\t\t\"Name\": \"helloHandler\",\n\t\t\t\"SpanContext\": {\n\t\t\t\t\"TraceID\": \"15715df45965b4a2db6dc103a76e52ae\",\n\t\t\t\t\"SpanID\": \"76d364cdd598c895\"\n\t\t\t},\n\t\t\t\"Status\": {\n\t\t\t\t\"Code\": \"Error\",\n\t\t\t\t\"Description\": \"something failed...\"\n\t\t\t}\n\t\t}\n\t*/\n}\n\nvar (\n\ttracer        trace.Tracer\n\ttraceProvider *sdktrace.TracerProvider\n)\n\n// OTEL setup\nfunc setupOTEL() {\n\texp, err := stdouttrace.New()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Create a new tracer provider with a batch span processor and the given exporter.\n\ttraceProvider = newTraceProvider(exp)\n\n\t// Set as global trace provider\n\totel.SetTracerProvider(traceProvider)\n\n\t// Finally, set the tracer that can be used for this package.\n\ttracer = traceProvider.Tracer(\"ExampleService\")\n}\n\n// OTEL tracer provider setup\nfunc newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {\n\tr, err := resource.Merge(\n\t\tresource.Default(),\n\t\tresource.NewWithAttributes(\n\t\t\tsemconv.SchemaURL,\n\t\t\tsemconv.ServiceName(\"ExampleService\"),\n\t\t),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn sdktrace.NewTracerProvider(\n\t\tsdktrace.WithBatcher(exp),\n\t\tsdktrace.WithResource(r),\n\t)\n}\n```\n\n#### Slog Attribute Collection HTTP Middleware and Extractor\n```go\npackage main\n\nimport (\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\n\tslogctx \"github.com/veqryn/slog-context\"\n\tsloghttp \"github.com/veqryn/slog-context/http\"\n)\n\nfunc init() {\n\t// Create the *slogctx.Handler middleware\n\th := slogctx.NewHandler(\n\t\tslog.NewJSONHandler(os.Stdout, nil), // The next or final handler in the chain\n\t\t\u0026slogctx.HandlerOptions{\n\t\t\t// Prependers will first add any sloghttp.With attributes,\n\t\t\t// then anything else Prepended to the ctx\n\t\t\tPrependers: []slogctx.AttrExtractor{\n\t\t\t\tsloghttp.ExtractAttrCollection, // our sloghttp middleware extractor\n\t\t\t\tslogctx.ExtractPrepended,       // for all other prepended attributes\n\t\t\t},\n\t\t},\n\t)\n\tslog.SetDefault(slog.New(h))\n}\n\nfunc main() {\n\tslog.Info(\"Starting server. Please run: curl localhost:8080/hello?id=24680\")\n\n\t// Wrap our final handler inside our middlewares.\n\t// AttrCollector -\u003e Request Logging -\u003e Final Endpoint Handler (helloUser)\n\thandler := sloghttp.AttrCollection(\n\t\thttpLoggingMiddleware(\n\t\t\thttp.HandlerFunc(helloUser),\n\t\t),\n\t)\n\n\t// Demonstrate the sloghttp middleware with a http server\n\thttp.Handle(\"/hello\", handler)\n\terr := http.ListenAndServe(\":8080\", nil)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// This is a stand-in for a middleware that might be capturing and logging out\n// things like the response code, request body, response body, url, method, etc.\n// It doesn't have access to any of the new context objects's created within the\n// next handler. But it should still log with any of the attributes added to our\n// sloghttp.Middleware, via sloghttp.With.\nfunc httpLoggingMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Add some logging context/baggage before the handler\n\t\tr = r.WithContext(sloghttp.With(r.Context(), \"path\", r.URL.Path))\n\n\t\t// Call the next handler\n\t\tnext.ServeHTTP(w, r)\n\n\t\t// Log out that we had a response. This would be where we could add\n\t\t// things such as the response status code, body, etc.\n\n\t\t// Should also have both \"path\" and \"id\", but not \"foo\".\n\t\t// Having \"id\" included in the log is the whole point of this package!\n\t\tslogctx.Info(r.Context(), \"Response\", \"method\", r.Method)\n\t\t/*\n\t\t\t{\n\t\t\t\t\"time\": \"2024-04-01T00:06:11Z\",\n\t\t\t\t\"level\": \"INFO\",\n\t\t\t\t\"msg\": \"Response\",\n\t\t\t\t\"path\": \"/hello\",\n\t\t\t\t\"id\": \"24680\",\n\t\t\t\t\"method\": \"GET\"\n\t\t\t}\n\t\t*/\n\t})\n}\n\n// This is our final api endpoint handler\nfunc helloUser(w http.ResponseWriter, r *http.Request) {\n\t// Stand-in for a User ID.\n\t// Add it to our middleware's context\n\tid := r.URL.Query().Get(\"id\")\n\n\t// sloghttp.With will add the \"id\" to the middleware, because it is a\n\t// synchronized map. It will show up in all log calls up and down the stack,\n\t// until the request sloghttp middleware exits.\n\tctx := sloghttp.With(r.Context(), \"id\", id)\n\n\t// The regular slogctx.With will add \"foo\" only to the Returned context,\n\t// which will limits its scope to the rest of this function (helloUser) and\n\t// any functions called by helloUser and passed this context.\n\t// The original caller of helloUser and all the middlewares will NOT see\n\t// \"foo\", because it is only part of the newly returned ctx.\n\tctx = slogctx.With(ctx, \"foo\", \"bar\")\n\n\t// Log some things.\n\t// Should also have both \"path\", \"id\", and \"foo\"\n\tslogctx.Info(ctx, \"saying hello...\")\n\t/*\n\t\t{\n\t\t\t\"time\": \"2024-04-01T00:06:11Z\",\n\t\t\t\"level\": \"INFO\",\n\t\t\t\"msg\": \"saying hello...\",\n\t\t\t\"path\": \"/hello\",\n\t\t\t\"id\": \"24680\",\n\t\t\t\"foo\": \"bar\"\n\t\t}\n\t*/\n\n\t// Response\n\t_, _ = w.Write([]byte(\"Hello User #\" + id))\n}\n```\n\n### gRPC Logging Interceptors/Middlewares\n#### Server Interceptors\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\n\tslogctx \"github.com/veqryn/slog-context\"\n\tsloggrpc \"github.com/veqryn/slog-context/grpc\"\n\tpb \"github.com/veqryn/slog-context/grpc/test/gen\"\n\t\"google.golang.org/grpc\"\n)\n\nfunc init() {\n\t// Create the *slogctx.Handler middleware\n\th := slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)\n\tslog.SetDefault(slog.New(h))\n}\n\nfunc main() {\n\tctx := context.TODO()\n\tslog.Info(\"Starting server. Please run: grpcurl localhost:8080/hello\") // TODO: fix\n\n\t// Create api app\n\tapp := \u0026Api{}\n\n\t// Create a listener on TCP port for gRPC:\n\tlis, err := net.Listen(\"tcp\", \":8000\")\n\tif err != nil {\n\t\tslogctx.Error(ctx, \"Unable to create grpc listener\", slogctx.Err(err))\n\t\tpanic(err)\n\t}\n\n\t// Create a gRPC server, and register our app as the handler/server for the service interface\n\t// https://github.com/grpc-ecosystem/go-grpc-middleware\n\tgrpcServer := grpc.NewServer(\n\t\t// Add the interceptors\n\t\t// We will use the sloggrpc.AppendToAttributesAll option, which is fairly verbose with the attributes.\n\t\t// There is also a slimmer sloggrpc.AppendToAttributesDefault, which is what it used if no option is provided.\n\t\t// You can also write your own to customize which attributes are added, or rename their keys.\n\t\t// There are also other options available: WithInterceptorFilter, WithErrorToLevel, and WithLogger\n\t\tgrpc.ChainUnaryInterceptor(sloggrpc.SlogUnaryServerInterceptor(sloggrpc.WithAppendToAttributes(sloggrpc.AppendToAttributesAll))),\n\t\tgrpc.ChainStreamInterceptor(sloggrpc.SlogStreamServerInterceptor(sloggrpc.WithAppendToAttributes(sloggrpc.AppendToAttributesAll))),\n\t)\n\tpb.RegisterTestServer(grpcServer, app)\n\n\t// Start gRPC server\n\tserveErr := grpcServer.Serve(lis)\n\tif serveErr != nil \u0026\u0026 !errors.Is(serveErr, grpc.ErrServerStopped) {\n\t\tpanic(serveErr)\n\t}\n}\n\n// GRPC setup\nvar _ pb.TestServer = \u0026Api{}\n\ntype Api struct{}\n\n// Each implemented RPC below includes an example of the logs generated by the sloggrpc interceptor\n\nfunc (a Api) Unary(ctx context.Context, req *pb.TestReq) (*pb.TestResp, error) {\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcReq\",\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"Unary\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195,\n\t\t  \"req\": {\n\t\t\t\"name\": \"John\",\n\t\t\t\"option\": 1\n\t\t  }\n\t\t}\n\t*/\n\treturn \u0026pb.TestResp{\n\t\tName:   \"Hello \" + req.Name,\n\t\tOption: req.Option + 1,\n\t}, nil\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcResp\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"Unary\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195,\n\t\t  \"ms\": 0.001,\n\t\t  \"resp\": {\n\t\t\t\"name\": \"Hello John\",\n\t\t\t\"option\": 2\n\t\t  }\n\t\t}\n\t*/\n}\n\nfunc (a Api) ClientStream(stream grpc.ClientStreamingServer[pb.TestReq, pb.TestResp]) error {\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamStart\",\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ClientStream\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195\n\t\t}\n\t*/\n\tvar reqNames []string\n\tvar lastReqOption int32\n\tfor {\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamRecv\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"ClientStream\",\n\t\t\t  \"role\": \"server\",\n\t\t\t  \"stream_server\": false,\n\t\t\t  \"stream_client\": true,\n\t\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t\t  \"peer_port\": 49195,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 3\n\t\t\t  },\n\t\t\t  \"ms\": 0.007708,\n\t\t\t  \"req\": {\n\t\t\t\t\"name\": \"Bob\",\n\t\t\t\t\"option\": 3\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\treq, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treqNames = append(reqNames, req.Name)\n\t\tlastReqOption = req.Option\n\t}\n\n\treturn stream.SendAndClose(\u0026pb.TestResp{\n\t\tName:   \"Hello \" + strings.Join(reqNames, \", \"),\n\t\tOption: lastReqOption + 1,\n\t})\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamEnd\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ClientStream\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195,\n\t\t  \"ms\": 0.113458,\n\t\t  \"resp\": {\n\t\t\t\"name\": \"Hello Bob, Bob, Bob\",\n\t\t\t\"option\": 4\n\t\t  }\n\t\t}\n\t*/\n}\n\nfunc (a Api) ServerStream(req *pb.TestReq, stream grpc.ServerStreamingServer[pb.TestResp]) error {\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamStart\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ServerStream\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195,\n\t\t  \"ms\": 0.032667,\n\t\t  \"req\": {\n\t\t\t\"name\": \"Jane\",\n\t\t\t\"option\": 1\n\t\t  }\n\t\t}\n\t*/\n\tfor i := int32(1); i \u003c= 3; i++ {\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamSend\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"ServerStream\",\n\t\t\t  \"role\": \"server\",\n\t\t\t  \"stream_server\": true,\n\t\t\t  \"stream_client\": false,\n\t\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t\t  \"peer_port\": 49195,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 1\n\t\t\t  },\n\t\t\t  \"ms\": 0.004417,\n\t\t\t  \"resp\": {\n\t\t\t\t\"name\": \"Hello Jane\",\n\t\t\t\t\"option\": 1\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\terr := stream.Send(\u0026pb.TestResp{\n\t\t\tName:   \"Hello \" + req.Name,\n\t\t\tOption: req.Option + i,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn nil\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamEnd\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ServerStream\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195,\n\t\t  \"ms\": 0.075041\n\t\t}\n\t*/\n}\n\nfunc (a Api) BidirectionalStream(stream grpc.BidiStreamingServer[pb.TestReq, pb.TestResp]) error {\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamStart\",\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195\n\t\t}\n\t*/\n\tvar i int32\n\tfor {\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamRecv\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t\t  \"role\": \"server\",\n\t\t\t  \"stream_server\": true,\n\t\t\t  \"stream_client\": true,\n\t\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t\t  \"peer_port\": 49195,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 1\n\t\t\t  },\n\t\t\t  \"ms\": 0.006166,\n\t\t\t  \"req\": {\n\t\t\t\t\"name\": \"Cat\",\n\t\t\t\t\"option\": 1\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\treq, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamSend\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t\t  \"role\": \"server\",\n\t\t\t  \"stream_server\": true,\n\t\t\t  \"stream_client\": true,\n\t\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t\t  \"peer_port\": 49195,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 2\n\t\t\t  },\n\t\t\t  \"ms\": 0.00525,\n\t\t\t  \"resp\": {\n\t\t\t\t\"name\": \"Hello Cat\",\n\t\t\t\t\"option\": 2\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\ti = req.Option + 1\n\t\terr = stream.Send(\u0026pb.TestResp{\n\t\t\tName:   \"Hello \" + req.Name,\n\t\t\tOption: i,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamSend\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195,\n\t\t  \"desc\": {\n\t\t\t\"msg_id\": 5\n\t\t  },\n\t\t  \"ms\": 0.000625,\n\t\t  \"resp\": {\n\t\t\t\"name\": \"Goodbye\",\n\t\t\t\"option\": 5\n\t\t  }\n\t\t}\n\t*/\n\treturn stream.Send(\u0026pb.TestResp{\n\t\tName:   \"Goodbye\",\n\t\tOption: i + 1,\n\t})\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamEnd\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t  \"role\": \"server\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"192.168.76.213\",\n\t\t  \"peer_port\": 49195,\n\t\t  \"ms\": 0.496166\n\t\t}\n\t*/\n}\n```\n\n#### Client Interceptors\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\n\tslogctx \"github.com/veqryn/slog-context\"\n\tsloggrpc \"github.com/veqryn/slog-context/grpc\"\n\tpb \"github.com/veqryn/slog-context/grpc/test/gen\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\nfunc init() {\n\t// Create the *slogctx.Handler middleware\n\th := slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)\n\tslog.SetDefault(slog.New(h))\n}\n\nfunc main() {\n\tctx := context.TODO()\n\tslog.Info(\"Starting client\")\n\n\t// Create a grpc client connection\n\tconn, err := grpc.NewClient(\"localhost:8000\",\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\t// Add the interceptors\n\t\t// We will use the sloggrpc.AppendToAttributesAll option, which is fairly verbose with the attributes.\n\t\t// There is also a slimmer sloggrpc.AppendToAttributesDefault, which is what it used if no option is provided.\n\t\t// You can also write your own to customize which attributes are added, or rename their keys.\n\t\t// There are also other options available: WithInterceptorFilter, WithErrorToLevel, and WithLogger\n\t\tgrpc.WithChainUnaryInterceptor(sloggrpc.SlogUnaryClientInterceptor(sloggrpc.WithAppendToAttributes(sloggrpc.AppendToAttributesAll))),\n\t\tgrpc.WithChainStreamInterceptor(sloggrpc.SlogStreamClientInterceptor(sloggrpc.WithAppendToAttributes(sloggrpc.AppendToAttributesAll))),\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer conn.Close()\n\n\tclient := pb.NewTestClient(conn)\n\n\t// Each called RPC below includes an example of the logs generated by the sloggrpc interceptor\n\n\t// Test the single/unary req-resp call\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcReq\",\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"Unary\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"req\": {\n\t\t\t\"name\": \"John\",\n\t\t\t\"option\": 1\n\t\t  }\n\t\t}\n\t*/\n\tresp, err := client.Unary(ctx, \u0026pb.TestReq{\n\t\tName:   \"John\",\n\t\tOption: 1,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcResp\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"Unary\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"ms\": 27.467792,\n\t\t  \"resp\": {\n\t\t\t\"name\": \"Hello John\",\n\t\t\t\"option\": 2\n\t\t  }\n\t\t}\n\t*/\n\n\t// Test the client streaming\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamStart\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ClientStream\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"ms\": 0.0175\n\t\t}\n\t*/\n\tcStream, err := client.ClientStream(ctx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor i := int32(1); i \u003c= 3; i++ {\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamSend\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"ClientStream\",\n\t\t\t  \"role\": \"client\",\n\t\t\t  \"stream_server\": false,\n\t\t\t  \"stream_client\": true,\n\t\t\t  \"peer_host\": \"localhost\",\n\t\t\t  \"peer_port\": 8000,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 3\n\t\t\t  },\n\t\t\t  \"ms\": 0.000333,\n\t\t\t  \"req\": {\n\t\t\t\t\"name\": \"Bob\",\n\t\t\t\t\"option\": 3\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\terr = cStream.Send(\u0026pb.TestReq{\n\t\t\tName:   \"Bob\",\n\t\t\tOption: i,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tresp, err = cStream.CloseAndRecv()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamEnd\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ClientStream\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": false,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"ms\": 0.427959,\n\t\t  \"resp\": {\n\t\t\t\"name\": \"Hello Bob, Bob, Bob\",\n\t\t\t\"option\": 4\n\t\t  }\n\t\t}\n\t*/\n\n\t// Test the server streaming\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamStart\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ServerStream\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"ms\": 0.010917,\n\t\t  \"req\": {\n\t\t\t\"name\": \"Jane\",\n\t\t\t\"option\": 1\n\t\t  }\n\t\t}\n\t*/\n\tsStream, err := client.ServerStream(ctx, \u0026pb.TestReq{\n\t\tName:   \"Jane\",\n\t\tOption: 1,\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamRecv\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"ServerStream\",\n\t\t\t  \"role\": \"client\",\n\t\t\t  \"stream_server\": true,\n\t\t\t  \"stream_client\": false,\n\t\t\t  \"peer_host\": \"localhost\",\n\t\t\t  \"peer_port\": 8000,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 1\n\t\t\t  },\n\t\t\t  \"ms\": 0.326,\n\t\t\t  \"resp\": {\n\t\t\t\t\"name\": \"Hello Jane\",\n\t\t\t\t\"option\": 1\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\tresp, err = sStream.Recv()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamEnd\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"ServerStream\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": false,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"ms\": 0.403125\n\t\t}\n\t*/\n\n\t// Test bi-direction streaming\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamStart\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"ms\": 0.006167\n\t\t}\n\t*/\n\tbStream, err := client.BidirectionalStream(ctx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor i := int32(1); i \u003c= 4; i++ {\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamSend\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t\t  \"role\": \"client\",\n\t\t\t  \"stream_server\": true,\n\t\t\t  \"stream_client\": true,\n\t\t\t  \"peer_host\": \"localhost\",\n\t\t\t  \"peer_port\": 8000,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 1\n\t\t\t  },\n\t\t\t  \"ms\": 0.000792,\n\t\t\t  \"req\": {\n\t\t\t\t\"name\": \"Cat\",\n\t\t\t\t\"option\": 1\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\terr = bStream.Send(\u0026pb.TestReq{\n\t\t\tName:   \"Cat\",\n\t\t\tOption: i,\n\t\t})\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamRecv\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t\t  \"role\": \"client\",\n\t\t\t  \"stream_server\": true,\n\t\t\t  \"stream_client\": true,\n\t\t\t  \"peer_host\": \"localhost\",\n\t\t\t  \"peer_port\": 8000,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 2\n\t\t\t  },\n\t\t\t  \"ms\": 0.299792,\n\t\t\t  \"resp\": {\n\t\t\t\t\"name\": \"Hello Cat\",\n\t\t\t\t\"option\": 2\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\tresp, err = bStream.Recv()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\ti += resp.Option - i\n\t}\n\n\terr = bStream.CloseSend()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor {\n\t\t/*\n\t\t\t{\n\t\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t\t  \"level\": \"INFO\",\n\t\t\t  \"msg\": \"rpcStreamRecv\",\n\t\t\t  \"code_name\": \"OK\",\n\t\t\t  \"code\": 0,\n\t\t\t  \"grpc_system\": \"grpc\",\n\t\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t\t  \"grpc_svc\": \"Test\",\n\t\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t\t  \"role\": \"client\",\n\t\t\t  \"stream_server\": true,\n\t\t\t  \"stream_client\": true,\n\t\t\t  \"peer_host\": \"localhost\",\n\t\t\t  \"peer_port\": 8000,\n\t\t\t  \"desc\": {\n\t\t\t\t\"msg_id\": 5\n\t\t\t  },\n\t\t\t  \"ms\": 0.182125,\n\t\t\t  \"resp\": {\n\t\t\t\t\"name\": \"Goodbye\",\n\t\t\t\t\"option\": 5\n\t\t\t  }\n\t\t\t}\n\t\t*/\n\t\tresp, err = bStream.Recv()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\t/*\n\t\t{\n\t\t  \"time\": \"2025-04-03T16:42:07Z\",\n\t\t  \"level\": \"INFO\",\n\t\t  \"msg\": \"rpcStreamEnd\",\n\t\t  \"code_name\": \"OK\",\n\t\t  \"code\": 0,\n\t\t  \"grpc_system\": \"grpc\",\n\t\t  \"grpc_pkg\": \"com.github.veqryn.slogcontext.grpc.test\",\n\t\t  \"grpc_svc\": \"Test\",\n\t\t  \"grpc_method\": \"BidirectionalStream\",\n\t\t  \"role\": \"client\",\n\t\t  \"stream_server\": true,\n\t\t  \"stream_client\": true,\n\t\t  \"peer_host\": \"localhost\",\n\t\t  \"peer_port\": 8000,\n\t\t  \"ms\": 0.830417\n\t\t}\n\t*/\n}\n```\n\n### slog-multi Middleware\nThis library has a convenience method that allow it to interoperate with [github.com/samber/slog-multi](https://github.com/samber/slog-multi),\nin order to easily setup slog workflows such as pipelines, fanout, routing, failover, etc.\n```go\nslog.SetDefault(slog.New(slogmulti.\n\tPipe(slogctx.NewMiddleware(\u0026slogctx.HandlerOptions{})).\n\tPipe(slogdedup.NewOverwriteMiddleware(\u0026slogdedup.OverwriteHandlerOptions{})).\n\tHandler(slog.NewJSONHandler(os.Stdout, \u0026slog.HandlerOptions{})),\n))\n```\n\n## Breaking Changes\n### O.4.0 -\u003e 0.5.0\nPackage function `ToCtx` renamed to `NewCtx`.\nPackage function `Logger` renamed to `FromCtx`.\n\nPackage renamed from `slogcontext` to `slogctx`.\nTo fix, change this:\n```go\nimport \"github.com/veqryn/slog-context\"\nvar h = slogcontext.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)\n```\nTo this:\n```go\nimport \"github.com/veqryn/slog-context\"\nvar h = slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)\n```\nNamed imports are unaffected.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fveqryn%2Fslog-context","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fveqryn%2Fslog-context","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fveqryn%2Fslog-context/lists"}