Skip to content

Commit

Permalink
Support combined log format with VirtualHost
Browse files Browse the repository at this point in the history
  • Loading branch information
taoso committed Jan 23, 2024
1 parent 9c61bd8 commit f92c2fe
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 4 deletions.
47 changes: 43 additions & 4 deletions logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"unicode/utf8"

Expand Down Expand Up @@ -148,7 +149,7 @@ func appendQuoted(buf []byte, s string) []byte {
// buildCommonLogLine builds a log entry for req in Apache Common Log Format.
// ts is the timestamp with which the entry should be logged.
// status and size are used to provide the response HTTP status and size.
func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int, vhost bool) []byte {
username := "-"
if url.User != nil {
if name := url.User.Username(); name != "" {
Expand All @@ -163,6 +164,18 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int

uri := req.RequestURI

virtualHost := ""
if vhost {
virtualHost = "-"
if req.Method != http.MethodConnect && !strings.Contains(req.Host, ":") {
a, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr)
if ok {
s := a.String()
virtualHost = req.Host + s[strings.LastIndex(s, ":"):]
}
}
}

// Requests using the CONNECT method over HTTP/2.0 must use
// the authority field (aka r.Host) to identify the target.
// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
Expand All @@ -173,7 +186,11 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int
uri = url.RequestURI()
}

buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
buf := make([]byte, 0, 3*(len(virtualHost)+len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
if vhost {
buf = append(buf, virtualHost...)
buf = append(buf, ' ')
}
buf = append(buf, host...)
buf = append(buf, " - "...)
buf = append(buf, username...)
Expand All @@ -196,7 +213,7 @@ func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int
// ts is the timestamp with which the entry should be logged.
// status and size are used to provide the response HTTP status and size.
func writeLog(writer io.Writer, params LogFormatterParams) {
buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size, false)
buf = append(buf, '\n')
_, _ = writer.Write(buf)
}
Expand All @@ -205,7 +222,19 @@ func writeLog(writer io.Writer, params LogFormatterParams) {
// ts is the timestamp with which the entry should be logged.
// status and size are used to provide the response HTTP status and size.
func writeCombinedLog(writer io.Writer, params LogFormatterParams) {
buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size, false)
buf = append(buf, ` "`...)
buf = appendQuoted(buf, params.Request.Referer())
buf = append(buf, `" "`...)
buf = appendQuoted(buf, params.Request.UserAgent())
buf = append(buf, '"', '\n')
_, _ = writer.Write(buf)
}

// writeVhostCombinedLog writes a log entry for req to w in Apache Combined Log Format
// with VirtualHost.
func writeVhostCombinedLog(writer io.Writer, params LogFormatterParams) {
buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size, true)
buf = append(buf, ` "`...)
buf = appendQuoted(buf, params.Request.Referer())
buf = append(buf, `" "`...)
Expand All @@ -224,6 +253,16 @@ func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
return loggingHandler{out, h, writeCombinedLog}
}

// VhostCombinedVLoggingHandler return a http.Handler that wraps h and logs requests to out in
// Apache Combined Log Format with VirtualHost.
//
// See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format.
//
// LoggingHandler always sets the ident field of the log to -.
func VhostCombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
return loggingHandler{out, h, writeVhostCombinedLog}
}

// LoggingHandler return a http.Handler that wraps h and logs requests to out in
// Apache Common Log Format (CLF).
//
Expand Down
36 changes: 36 additions & 0 deletions logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ package handlers

import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io/fs"
"mime/multipart"
"net"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -229,6 +231,30 @@ func TestLogFormatterCombinedLog_Scenario5(t *testing.T) {
LoggingScenario5(t, formatter, expected)
}

func TestLogFormatterVhostCombinedLog_Scenario1(t *testing.T) {
formatter := writeVhostCombinedLog
expected := "- 192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 200 100 \"http://example.com\" " +
"\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " +
"AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n"
LoggingScenario1(t, formatter, expected)
}

func TestLogFormatterVhostCombinedLog_Scenario2(t *testing.T) {
formatter := writeVhostCombinedLog
expected := "- 192.168.100.5 - - [26/May/1983:03:30:45 +0200] \"CONNECT www.example.com:443 HTTP/2.0\" 200 100 \"http://example.com\" " +
"\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " +
"AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n"
LoggingScenario2(t, formatter, expected)
}

func TestLogFormatterVhostCombinedLog_Scenario3(t *testing.T) {
formatter := writeVhostCombinedLog
expected := "example.com:8080 192.168.100.5 - kamil [26/May/1983:03:30:45 +0200] \"GET / HTTP/1.1\" 401 500 \"http://example.com\" " +
"\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) " +
"AppleWebKit/537.33 (KHTML, like Gecko) Chrome/27.0.1430.0 Safari/537.33\"\n"
LoggingScenario3(t, formatter, expected)
}

func LoggingScenario1(t *testing.T, formatter LogFormatter, expected string) {
loc, err := time.LoadLocation("Europe/Warsaw")
if err != nil {
Expand Down Expand Up @@ -265,6 +291,7 @@ func LoggingScenario2(t *testing.T, formatter LogFormatter, expected string) {

// CONNECT request over http/2.0
req := constructConnectRequest()
req = req.WithContext(constructVhostAddrCtx("10.0.0.1", 8080))

buf := new(bytes.Buffer)
params := LogFormatterParams{
Expand Down Expand Up @@ -292,6 +319,7 @@ func LoggingScenario3(t *testing.T, formatter LogFormatter, expected string) {
// Request with an unauthorized user
req := constructTypicalRequestOk()
req.URL.User = url.User("kamil")
req = req.WithContext(constructVhostAddrCtx("10.0.0.1", 8080))

buf := new(bytes.Buffer)
params := LogFormatterParams{
Expand Down Expand Up @@ -401,3 +429,11 @@ func constructEncodedRequest() *http.Request {
req.URL, _ = url.Parse("http://example.com/test?abc=hello%20world&a=b%3F")
return req
}

func constructVhostAddrCtx(addr string, port int) context.Context {
ip := net.ParseIP(addr)

ctx := context.Background()
ctx = context.WithValue(ctx, http.LocalAddrContextKey, &net.TCPAddr{IP: ip, Port: port})
return ctx
}

0 comments on commit f92c2fe

Please sign in to comment.