Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Make Request Logging Format Configurable #423

Merged
merged 3 commits into from
Dec 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ Usage of oauth2_proxy:
-redeem-url string: Token redemption endpoint
-redirect-url string: the OAuth Redirect URL. ie: "https://internalapp.yourcompany.com/oauth2/callback"
-request-logging: Log requests to stdout (default true)
-request-logging-format: Template for request log lines (see "Logging Format" paragraph below)
-resource string: The resource that is protected (Azure AD only)
-scope string: OAuth scope specification
-set-xauthrequest: set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)
Expand Down Expand Up @@ -331,12 +332,21 @@ following:

## Logging Format

OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log.
By default, OAuth2 Proxy logs requests to stdout in a format similar to Apache Combined Log.

```
<REMOTE_ADDRESS> - <[email protected]> [19/Mar/2015:17:20:19 -0400] <HOST_HEADER> GET <UPSTREAM_HOST> "/path/" HTTP/1.1 "<USER_AGENT>" <RESPONSE_CODE> <RESPONSE_BYTES> <REQUEST_DURATION>
```

If you require a different format than that, you can configure it with the `-request-logging-format` flag.
The default format is configured as follows:

```
{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}
```

[See `logMessageData` in `logging_handler.go`](./logging_handler.go) for all available variables.

## Adding a new Provider

Follow the examples in the [`providers` package](providers/) to define a new
Expand Down
74 changes: 51 additions & 23 deletions logging_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ import (
"net"
"net/http"
"net/url"
"text/template"
"time"
)

const (
defaultRequestLoggingFormat = "{{.Client}} - {{.Username}} [{{.Timestamp}}] {{.Host}} {{.RequestMethod}} {{.Upstream}} {{.RequestURI}} {{.Protocol}} {{.UserAgent}} {{.StatusCode}} {{.ResponseSize}} {{.RequestDuration}}"
)

// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP status
// code and body size
type responseLogger struct {
Expand Down Expand Up @@ -64,15 +69,38 @@ func (l *responseLogger) Size() int {
return l.size
}

// logMessageData is the container for all values that are available as variables in the request logging format.
// All values are pre-formatted strings so it is easy to use them in the format string.
type logMessageData struct {
Client,
Host,
Protocol,
RequestDuration,
RequestMethod,
RequestURI,
ResponseSize,
StatusCode,
Timestamp,
Upstream,
UserAgent,
Username string
}

// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its friends
type loggingHandler struct {
writer io.Writer
handler http.Handler
enabled bool
writer io.Writer
handler http.Handler
enabled bool
logTemplate *template.Template
}

func LoggingHandler(out io.Writer, h http.Handler, v bool) http.Handler {
return loggingHandler{out, h, v}
func LoggingHandler(out io.Writer, h http.Handler, v bool, requestLoggingTpl string) http.Handler {
return loggingHandler{
writer: out,
handler: h,
enabled: v,
logTemplate: template.Must(template.New("request-log").Parse(requestLoggingTpl)),
}
}

func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
Expand All @@ -83,14 +111,13 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if !h.enabled {
return
}
logLine := buildLogLine(logger.authInfo, logger.upstream, req, url, t, logger.Status(), logger.Size())
h.writer.Write(logLine)
h.writeLogLine(logger.authInfo, logger.upstream, req, url, t, logger.Status(), logger.Size())
}

// Log entry for req similar to Apache Common Log Format.
// ts is the timestamp with which the entry should be logged.
// status, size are used to provide the response HTTP status and size.
func buildLogLine(username, upstream string, req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
func (h loggingHandler) writeLogLine(username, upstream string, req *http.Request, url url.URL, ts time.Time, status int, size int) {
if username == "" {
username = "-"
}
Expand All @@ -114,19 +141,20 @@ func buildLogLine(username, upstream string, req *http.Request, url url.URL, ts

duration := float64(time.Now().Sub(ts)) / float64(time.Second)

logLine := fmt.Sprintf("%s - %s [%s] %s %s %s %q %s %q %d %d %0.3f\n",
client,
username,
ts.Format("02/Jan/2006:15:04:05 -0700"),
req.Host,
req.Method,
upstream,
url.RequestURI(),
req.Proto,
req.UserAgent(),
status,
size,
duration,
)
return []byte(logLine)
h.logTemplate.Execute(h.writer, logMessageData{
Client: client,
Host: req.Host,
Protocol: req.Proto,
RequestDuration: fmt.Sprintf("%0.3f", duration),
RequestMethod: req.Method,
RequestURI: fmt.Sprintf("%q", url.RequestURI()),
ResponseSize: fmt.Sprintf("%d", size),
StatusCode: fmt.Sprintf("%d", status),
Timestamp: ts.Format("02/Jan/2006:15:04:05 -0700"),
Upstream: upstream,
UserAgent: fmt.Sprintf("%q", req.UserAgent()),
Username: username,
})

h.writer.Write([]byte("\n"))
}
42 changes: 42 additions & 0 deletions logging_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestLoggingHandler_ServeHTTP(t *testing.T) {
ts := time.Now()

tests := []struct {
Format,
ExpectedLogMessage string
}{
{defaultRequestLoggingFormat, fmt.Sprintf("127.0.0.1 - - [%s] test-server GET - \"/foo/bar\" HTTP/1.1 \"\" 200 4 0.000\n", ts.Format("02/Jan/2006:15:04:05 -0700"))},
{"{{.RequestMethod}}", "GET\n"},
}

for _, test := range tests {
buf := bytes.NewBuffer(nil)
handler := func(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("test"))
}

h := LoggingHandler(buf, http.HandlerFunc(handler), true, test.Format)

r, _ := http.NewRequest("GET", "/foo/bar", nil)
r.RemoteAddr = "127.0.0.1"
r.Host = "test-server"

h.ServeHTTP(httptest.NewRecorder(), r)

actual := buf.String()
if actual != test.ExpectedLogMessage {
t.Errorf("Log message was\n%s\ninstead of expected \n%s", actual, test.ExpectedLogMessage)
}
}
}
3 changes: 2 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func main() {
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")

flagSet.Bool("request-logging", true, "Log requests to stdout")
flagSet.String("request-logging-format", defaultRequestLoggingFormat, "Template for log lines")

flagSet.String("provider", "google", "OAuth provider")
flagSet.String("login-url", "", "Authentication endpoint")
Expand Down Expand Up @@ -124,7 +125,7 @@ func main() {
}

s := &Server{
Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging),
Handler: LoggingHandler(os.Stdout, oauthproxy, opts.RequestLogging, opts.RequestLoggingFormat),
Opts: opts,
}
s.ListenAndServe()
Expand Down
38 changes: 20 additions & 18 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ type Options struct {
Scope string `flag:"scope" cfg:"scope"`
ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"`

RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
RequestLogging bool `flag:"request-logging" cfg:"request_logging"`
RequestLoggingFormat string `flag:"request-logging-format" cfg:"request_logging_format"`

SignatureKey string `flag:"signature-key" cfg:"signature_key" env:"OAUTH2_PROXY_SIGNATURE_KEY"`

Expand All @@ -90,23 +91,24 @@ type SignatureData struct {

func NewOptions() *Options {
return &Options{
ProxyPrefix: "/oauth2",
HttpAddress: "127.0.0.1:4180",
HttpsAddress: ":443",
DisplayHtpasswdForm: true,
CookieName: "_oauth2_proxy",
CookieSecure: true,
CookieHttpOnly: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(0),
SetXAuthRequest: false,
SkipAuthPreflight: false,
PassBasicAuth: true,
PassUserHeaders: true,
PassAccessToken: false,
PassHostHeader: true,
ApprovalPrompt: "force",
RequestLogging: true,
ProxyPrefix: "/oauth2",
HttpAddress: "127.0.0.1:4180",
HttpsAddress: ":443",
DisplayHtpasswdForm: true,
CookieName: "_oauth2_proxy",
CookieSecure: true,
CookieHttpOnly: true,
CookieExpire: time.Duration(168) * time.Hour,
CookieRefresh: time.Duration(0),
SetXAuthRequest: false,
SkipAuthPreflight: false,
PassBasicAuth: true,
PassUserHeaders: true,
PassAccessToken: false,
PassHostHeader: true,
ApprovalPrompt: "force",
RequestLogging: true,
RequestLoggingFormat: defaultRequestLoggingFormat,
}
}

Expand Down