Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Daisuke Maki committed Oct 12, 2024
0 parents commit 164600e
Show file tree
Hide file tree
Showing 14 changed files with 534 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
target-branch: "v3"
labels:
- "go"
- "dependencies"
- "dependabot"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
target-branch: "v3"

- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
target-branch: "v2"
labels:
- "go"
- "dependencies"
- "dependabot"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
target-branch: "v2"

23 changes: 23 additions & 0 deletions .github/workflows/autodoc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Auto-Doc
on:
pull_request:
branches:
- v3
types:
- closed
workflow_dispatch: {}

jobs:
autodoc:
runs-on: ubuntu-latest
name: "Run commands to generate documentation"
if: github.event.pull_request.merged == true
steps:
- name: Checkout repositor
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Process markdown files
run: |
find . -name '*.md' | xargs perl tools/autodoc.pl
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI
on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go: [ '1.23', '1.22' ]
name: Go ${{ matrix.go }} test
steps:
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Check documentation generator
run: |
find . -name '*.md' | xargs env AUTODOC_DRYRUN=1 perl tools/autodoc.pl
- name: Install Go stable version
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ matrix.go }}
- name: Test
run: go test -v -race

17 changes: 17 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: lint
on: [push]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Install Go stable version
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
- name: Run go vet
run: |
go vet ./...
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 lestrrat

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# github.com/lestrrat-go/accesslog ![](https://github.com/lestrrat-go/accesslog/workflows/CI/badge.svg) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/accesslog.svg)](https://pkg.go.dev/github.com/lestrrat-go/accesslog)

`github.com/lestrrat-go/accesslog` provides an HTTP middleware that logs accesslogs based on `log/slog`.

# SYNOPSIS

<!-- INCLUDE(accesslog_example_test.go) -->
<!-- END INCLUDE -->
117 changes: 117 additions & 0 deletions accesslog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package accesslog

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

// Middleware is the main object that you interact with. Despite its name
// it's actually a Builder object, which needs to be applied to a http.Handler
// by calling `Wrap()`
//
// For frameworks such as CHI that require a function that returns a http.Handler,
// pass it the reference to the `Wrap()` method.
type Middleware struct {
clock Clock
collector Collector
logger *slog.Logger
logLevel slog.Level
recordResponse bool
rwbuilder ResponseWriterBuilder
}

// Collector is an object that collects attributes from the request and response
// and returns them as a slice of slog.Attr objects to be logged.
//
// If you do not like the default collector that is provided by the library,
// you can always implement your own. If you just want to extend the standard
// collector, you can embed the `Standard()` collector to your collector, and
// add your own attributes to the return values
type Collector interface {
Collect(ResponseWriter, *http.Request) []slog.Attr
}

type handler struct {
next http.Handler
clock Clock
collector Collector
logger *slog.Logger
logLevel slog.Level
recordResponse bool
rwbuilder ResponseWriterBuilder
}

// New creates a new Middleware object. You can further customize the object
// by calling its methods.
//
// By default the `Standard()` collector is added to the list of collectors,
// and the log level is set to `slog.LevelInfo`
func New() *Middleware {
return &Middleware{
clock: SystemClock{},
collector: Standard(),
logLevel: slog.LevelInfo,
logger: slog.Default(),
rwbuilder: DefaultResponseWriterBuilder(),
}
}

// Clock sets the Clock object to be used by the Middleware object. By default
// `SystemClock` is used.
func (al *Middleware) Clock(clock Clock) *Middleware {
al.clock = clock
return al
}

// Collector sets the Collector object to be used by the Middleware object. By
// default `Standard()` is used.
func (al *Middleware) Collector(collector Collector) *Middleware {
al.collector = collector
return al
}

// Logger sets the slog.Logger object to be used by the Middleware object. By
// default `slog.Default()` is used.
func (al *Middleware) Logger(logger *slog.Logger) *Middleware {
al.logger = logger
return al
}

// LogLevel sets the log level to be used by the Middleware object. By default
// `slog.LevelInfo` is used.
func (al *Middleware) LogLevel(level slog.Level) *Middleware {
al.logLevel = level
return al
}

// RecordResponse sets whether the response body should be recorded. By default
// this is set to false for performance reasons.
func (al *Middleware) RecordResponse(b bool) *Middleware {
al.recordResponse = b
return al
}

// Wrap returns a http.Handler that wraps the provided `next“ http.Handler object.
func (al *Middleware) Wrap(next http.Handler) http.Handler {
return handler{
next: next,
clock: al.clock,
collector: al.collector,
logger: al.logger,
logLevel: al.logLevel,
recordResponse: al.recordResponse,
rwbuilder: al.rwbuilder,
}
}

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rw := h.rwbuilder.Wrap(w, r, h.recordResponse)
h.next.ServeHTTP(rw, r)
rw.End()
h.process(rw, r)
}

func (m handler) process(rw ResponseWriter, r *http.Request) {
attrs := m.collector.Collect(rw, r)
m.logger.LogAttrs(r.Context(), m.logLevel, "access", attrs...)
}
52 changes: 52 additions & 0 deletions accesslog_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package accesslog_test

import (
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"time"

"github.com/lestrrat-go/accesslog"
)

func Example() {
al := accesslog.New().
// Set the clock to a static time to force duration=0 for testing
Clock(accesslog.StaticClock(time.Time{})).
Logger(
slog.New(
slog.NewJSONHandler(os.Stdout,
&slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
switch a.Key {
case slog.TimeKey:
// replace time to get static output for testing
return slog.Time(slog.TimeKey, time.Time{})
case "remote_addr":
// replace value to get static output for testing
return slog.String("remote_addr", "127.0.0.1:99999")
}
return a
},
},
),
),
)

srv := httptest.NewServer(al.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Hello, World!"))
})))
defer srv.Close()

_, err := http.Get(srv.URL)
if err != nil {
fmt.Println(err.Error())
return
}

// OUTPUT:
// {"time":"0001-01-01T00:00:00Z","level":"INFO","msg":"access","remote_addr":"127.0.0.1:99999","http_method":"GET","path":"/","status":200,"body_bytes_sent":13,"http_referer":"","http_user_agent":"Go-http-client/1.1"}
}
22 changes: 22 additions & 0 deletions clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package accesslog

import "time"

type Clock interface {
Now() time.Time
}

// StaticClock is a Clock that always returns the same time. It's only used for
// testing.
type StaticClock time.Time

func (c StaticClock) Now() time.Time {
return time.Time(c)
}

// SystemClock is a wrapper around time.Now().
type SystemClock struct{}

func (SystemClock) Now() time.Time {
return time.Now()
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/lestrrat-go/accesslog

go 1.22.6
Loading

0 comments on commit 164600e

Please sign in to comment.