File: babylogger.go

package info (click to toggle)
golang-github-meowgorithm-babylogger 1.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 92 kB
  • sloc: makefile: 3
file content (191 lines) | stat: -rw-r--r-- 5,277 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// Package babylogger is a simple HTTP logging middleware. It works with any
// multiplexer compatible with the Go standard library.
//
// When a terminal is present it will log using nice colors. When the output is
// not in a terminal (for example in logs) ANSI escape sequences (read: colors)
// will be stripped from the output.
//
// Also note that for accurate response time logging Babylogger should be the
// first middleware called.
//
// Windows support is not currently implemented, however it would be trivial
// enough with the help of a couple packages from Mattn:
// http://github.com/mattn/go-isatty and https://github.com/mattn/go-colorable
//
// Example using the standard library:
//
//	package main
//
//	import (
//		"fmt"
//		"net/http"
//		"github.com/meowgorithm/babylogger"
//	)
//
//	func main() {
//		http.Handle("/", babylogger.Middleware(http.HandlerFunc(handler)))
//		http.ListenAndServe(":8000", nil)
//	}
//
//	handler(w http.ResponseWriter, r *http.Request) {
//		fmt.FPrintln(w, "Oh, hi, I didn’t see you there.")
//	}
//
// Example with Goji:
//
//	import (
//		"fmt"
//		"net/http"
//		"github.com/meowgorithm/babylogger"
//		"goji.io"
//		"goji.io/pat"
//	)
//
//	func main() {
//		mux := goji.NewMux()
//		mux.Use(babylogger.Middleware)
//		mux.HandleFunc(pat.Get("/"), handler)
//		http.ListenAndServe(":8000", mux)
//	}
//
//	handler(w http.ResponseWriter, r *http.Request) {
//		fmt.FPrintln(w, "Oh hi, I didn’t see you there.")
//	}
package babylogger

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/charmbracelet/lipgloss"
	humanize "github.com/dustin/go-humanize"
)

// Styles.
var (
	timeStyle = lipgloss.NewStyle().
			Foreground(lipgloss.AdaptiveColor{Light: "240", Dark: "240"})

	uriStyle = timeStyle.Copy()

	methodStyle = lipgloss.NewStyle().
			Foreground(lipgloss.AdaptiveColor{Light: "62", Dark: "62"})

	http200Style = lipgloss.NewStyle().
			Foreground(lipgloss.AdaptiveColor{Light: "35", Dark: "48"})

	http300Style = lipgloss.NewStyle().
			Foreground(lipgloss.AdaptiveColor{Light: "208", Dark: "192"})

	http400Style = lipgloss.NewStyle().
			Foreground(lipgloss.AdaptiveColor{Light: "39", Dark: "86"})

	http500Style = lipgloss.NewStyle().
			Foreground(lipgloss.AdaptiveColor{Light: "203", Dark: "204"})

	subtleStyle = lipgloss.NewStyle().
			Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "250"})

	addressStyle = subtleStyle.Copy()
)

type logWriter struct {
	http.ResponseWriter
	code, bytes int
}

func (r *logWriter) Write(p []byte) (int, error) {
	written, err := r.ResponseWriter.Write(p)
	r.bytes += written
	return written, err
}

// Note this is generally only called when sending an HTTP error, so it's
// important to set the `code` value to 200 as a default
func (r *logWriter) WriteHeader(code int) {
	r.code = code
	r.ResponseWriter.WriteHeader(code)
}

// Hijack exposes the underlying ResponseWriter Hijacker implementation for
// WebSocket compatibility
func (r *logWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	hj, ok := r.ResponseWriter.(http.Hijacker)
	if !ok {
		return nil, nil, fmt.Errorf("WebServer does not support hijacking")
	}
	return hj.Hijack()
}

// Middleware is the logging middleware where we log incoming and outgoing
// requests for a multiplexer. It should be the first middleware called so it
// can log request times accurately.
func Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		addr := r.RemoteAddr
		if colon := strings.LastIndex(addr, ":"); colon != -1 {
			addr = addr[:colon]
		}

		arrow := subtleStyle.Render("<-")
		method := methodStyle.Render(r.Method)
		uri := uriStyle.Render(r.RequestURI)
		address := addressStyle.Render(addr)

		// Log request
		log.Printf("%s %s %s %s", arrow, method, uri, address)

		writer := &logWriter{
			ResponseWriter: w,
			code:           http.StatusOK, // default. so important! see above.
		}

		arrow = subtleStyle.Render("->")
		startTime := time.Now()

		// Not sure why the request could possibly be nil, but it has happened
		if r == nil {
			http.Error(w, http.StatusText(http.StatusInternalServerError),
				http.StatusInternalServerError)
			writer.code = http.StatusInternalServerError
		} else {
			next.ServeHTTP(writer, r)
		}

		elapsedTime := time.Now().Sub(startTime)

		var statusStyle lipgloss.Style

		if writer.code < 300 { // 200s
			statusStyle = http200Style
		} else if writer.code < 400 { // 300s
			statusStyle = http300Style
		} else if writer.code < 500 { // 400s
			statusStyle = http400Style
		} else { // 500s
			statusStyle = http500Style
		}

		status := statusStyle.Render(fmt.Sprintf("%d %s", writer.code, http.StatusText(writer.code)))

		// The excellent humanize package adds a space between the integer and
		// the unit as far as bytes are conerned (105 B). In our case that
		// makes it a little harder on the eyes when scanning the logs, so
		// we're stripping that space
		formattedBytes := strings.Replace(
			humanize.Bytes(uint64(writer.bytes)),
			" ", "", 1)

		bytes := subtleStyle.Render(formattedBytes)
		time := timeStyle.Render(fmt.Sprintf("%s", elapsedTime))

		// Log response
		log.Printf("%s %s %s %v", arrow, status, bytes, time)
	})
}