Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
veqryn committed Apr 1, 2024
1 parent a9086ef commit 63a89a5
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 16 deletions.
129 changes: 127 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ whenever a new log line is written.

In that same workflow, the `HandlerOptions` and `AttrExtractor` types let us
extract any custom values from a context and have them automatically be
prepended or appended to all log lines using that context. For example, the
`slogotel.ExtractTraceSpanID` extractor will automatically extract the OTEL
prepended or appended to all log lines using that context. By default, there are
extractors for anything added via `Prepend` and `Append`, but this repository
contains more optional Extractors that can be added:
* `slogotel.ExtractTraceSpanID` extractor will automatically extract the OTEL
(OpenTelemetry) TraceID and SpanID, and add them to the log record, while also
annotating the Span with an error code if the log is at error level.
* `sloghttp.ExtractAttrCollection` extractor will automatically add to the log
record any attributes added by `sloghttp.With` after the `sloghttp.AttrCollection`
http middleware. This allows other middlewares to log with attributes that would
normally be out of scope, because they were added by a later middleware or the
final http handler in the chain.

#### Logger in Context Workflow:

Expand Down Expand Up @@ -68,6 +75,7 @@ import (
```

## Usage
[Examples in repo](examples/)
### Logger in Context Workflow
```go
package main
Expand Down Expand Up @@ -443,6 +451,123 @@ func newTraceProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
}
```

#### Slog Attribute Collection HTTP Middleware and Extractor
```go
package main

import (
"log/slog"
"net/http"
"os"

slogctx "github.com/veqryn/slog-context"
sloghttp "github.com/veqryn/slog-context/http"
)

func init() {
// Create the *slogctx.Handler middleware
h := slogctx.NewHandler(
slog.NewJSONHandler(os.Stdout, nil), // The next or final handler in the chain
&slogctx.HandlerOptions{
// Prependers will first add the any sloghttp.With attributes,
// then anything else Prepended to the ctx
Prependers: []slogctx.AttrExtractor{
sloghttp.ExtractAttrCollection, // our sloghttp middleware extractor
slogctx.ExtractPrepended, // for all other prepended attributes
},
},
)
slog.SetDefault(slog.New(h))
}

func main() {
slog.Info("Starting server. Please run: curl localhost:8080/hello?id=24680")

// Wrap our final handler inside our middlewares.
// AttrCollector -> Request Logging -> Final Endpoint Handler (helloUser)
handler := sloghttp.AttrCollection(
httpLoggingMiddleware(
http.HandlerFunc(helloUser),
),
)

// Demonstrate the sloghttp middleware with a http server
http.Handle("/hello", handler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
panic(err)
}
}

// This is a stand-in for a middleware that might be capturing and logging out
// things like the response code, request body, response body, url, method, etc.
// It doesn't have access to any of the new context objects's created within the
// next handler. But it should still log with any of the attributes added to our
// sloghttp.Middleware, via sloghttp.With.
func httpLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add some logging context/baggage before the handler
r = r.WithContext(sloghttp.With(r.Context(), "path", r.URL.Path))

// Call the next handler
next.ServeHTTP(w, r)

// Log out that we had a response. This would be where we could add
// things such as the response status code, body, etc.

// Should also have both "path" and "id", but not "foo".
// Having "id" included in the log is the whole point of this package!
slogctx.Info(r.Context(), "Response", "method", r.Method)
/*
{
"time": "2024-04-01T00:06:11Z",
"level": "INFO",
"msg": "Response",
"path": "/hello",
"id": "24680",
"method": "GET"
}
*/
})
}

// This is our final api endpoint handler
func helloUser(w http.ResponseWriter, r *http.Request) {
// Stand-in for a User ID.
// Add it to our middleware's context
id := r.URL.Query().Get("id")

// sloghttp.With will add the "id" to to the middleware, because it is a
// synchronized map. It will show up in all log calls up and down the stack,
// until the request sloghttp middleware exits.
ctx := sloghttp.With(r.Context(), "id", id)

// The regular slogctx.With will add "foo" only to the Returned context,
// which will limits its scope to the rest of this function (helloUser) and
// any functions called by helloUser and passed this context.
// The original caller of helloUser and all the middlewares will NOT see
// "foo", because it is only part of the newly returned ctx.
ctx = slogctx.With(ctx, "foo", "bar")

// Log some things.
// Should also have both "path", "id", and "foo"
slogctx.Info(ctx, "saying hello...")
/*
{
"time": "2024-04-01T00:06:11Z",
"level": "INFO",
"msg": "saying hello...",
"path": "/hello",
"id": "24680",
"foo": "bar"
}
*/

// Response
_, _ = w.Write([]byte("Hello User #" + id))
}
```

### slog-multi Middleware
This library has a convenience method that allow it to interoperate with [github.com/samber/slog-multi](https://github.com/samber/slog-multi),
in order to easily setup slog workflows such as pipelines, fanout, routing, failover, etc.
Expand Down
10 changes: 5 additions & 5 deletions http/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ import (
sloghttp "github.com/veqryn/slog-context/http"
)

func ExampleAttrCollectorExtractor() {
func ExampleExtractAttrCollection() {
// Create the *slogctx.Handler middleware
h := slogctx.NewHandler(
slog.NewJSONHandler(os.Stdout, nil), // The next or final handler in the chain
&slogctx.HandlerOptions{
// Prependers will first add the any sloghttp.With attributes,
// then anything else Prepended to the ctx
Prependers: []slogctx.AttrExtractor{
sloghttp.AttrCollectorExtractor, // our sloghttp middleware extractor
slogctx.ExtractPrepended, // for all other prepended attributes
sloghttp.ExtractAttrCollection, // our sloghttp middleware extractor
slogctx.ExtractPrepended, // for all other prepended attributes
},
},
)
slog.SetDefault(slog.New(h))
}

func ExampleAttrCollectorMiddleware() {
func ExampleAttrCollection() {
// This is our final api endpoint handler
helloUserHandler := func(w http.ResponseWriter, r *http.Request) {
// Stand-in for a User ID.
Expand Down Expand Up @@ -75,7 +75,7 @@ func ExampleAttrCollectorMiddleware() {

// Wrap our final handler inside our middlewares.
// AttrCollector -> Request Logging -> Final Endpoint Handler (helloUser)
handler := sloghttp.AttrCollectorMiddleware(
handler := sloghttp.AttrCollection(
httpLoggingMiddleware(
http.HandlerFunc(helloUserHandler),
),
Expand Down
10 changes: 5 additions & 5 deletions http/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/veqryn/slog-context/internal/attr"
)

// AttrCollectorMiddleware is an http middleware that collects slog.Attr from
// AttrCollection is an http middleware that collects slog.Attr from
// any and all later middlewares and the final http request handler, and makes
// them available to all middlewares and the request handler.
// Essentially, it lets you collect slog attributes that are discovered later in
Expand All @@ -27,7 +27,7 @@ import (
// stored inside the context. All calls log that include the context will
// automatically have all the attributes included (ex: slogctx.Info, or
// slog.InfoContext).
func AttrCollectorMiddleware(next http.Handler) http.Handler {
func AttrCollection(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If the context already contains our map, we don't need to create a new one
if ctx := r.Context(); fromCtx(ctx) == nil {
Expand All @@ -38,7 +38,7 @@ func AttrCollectorMiddleware(next http.Handler) http.Handler {
}

// With adds the provided slog.Attr's to the context. If used with
// sloghttp.AttrCollectorMiddleware it will add them to the context in a way
// sloghttp.AttrCollection it will add them to the context in a way
// that is visible to all intermediate middlewares and functions between the
// collector middleware and the call to With.
func With(ctx context.Context, args ...any) context.Context {
Expand Down Expand Up @@ -73,11 +73,11 @@ func With(ctx context.Context, args ...any) context.Context {
return ctx
}

// AttrCollectorExtractor is a slogctx Extractor that must be used with a
// ExtractAttrCollection is a slogctx Extractor that must be used with a
// slogctx.Handler (via slogctx.HandlerOptions) as Prependers or Appenders.
// It will cause the Handler to add the Attributes added by sloghttp.With to all
// log lines using that same context.
func AttrCollectorExtractor(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr {
func ExtractAttrCollection(ctx context.Context, _ time.Time, _ slog.Level, _ string) []slog.Attr {
m := fromCtx(ctx)
if m == nil {
return nil
Expand Down
8 changes: 4 additions & 4 deletions http/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import (
"github.com/veqryn/slog-context/internal/test"
)

func TestAttrCollectorMiddleware(t *testing.T) {
func TestAttrCollection(t *testing.T) {
// Create the *slogctx.Handler middleware
tester := &test.Handler{}
h := slogctx.NewHandler(
tester,
&slogctx.HandlerOptions{
Prependers: []slogctx.AttrExtractor{
AttrCollectorExtractor, // our sloghttp middleware extractor
ExtractAttrCollection, // our sloghttp middleware extractor
slogctx.ExtractPrepended, // for all other prepended attributes
},
},
Expand All @@ -30,7 +30,7 @@ func TestAttrCollectorMiddleware(t *testing.T) {
ctx := slogctx.NewCtx(context.Background(), slog.New(h))

// Setup with our sloghttp middleware, a logging middleware, then our endpoint
httpHandler := AttrCollectorMiddleware(
httpHandler := AttrCollection(
httpLoggingMiddleware(
http.HandlerFunc(helloUser),
),
Expand Down Expand Up @@ -121,7 +121,7 @@ func TestOutsideRequest(t *testing.T) {
tester,
&slogctx.HandlerOptions{
Prependers: []slogctx.AttrExtractor{
AttrCollectorExtractor, // our sloghttp middleware extractor
ExtractAttrCollection, // our sloghttp middleware extractor
slogctx.ExtractPrepended, // for all other prepended attributes
},
},
Expand Down

0 comments on commit 63a89a5

Please sign in to comment.