Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support combined log format with VirtualHost #255

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}