Skip to content

Commit

Permalink
Merge pull request #10 from google/martian-logger
Browse files Browse the repository at this point in the history
General request/response logging to stdout as a modifier.
  • Loading branch information
bramhaghosh committed Jul 7, 2015
2 parents ad9e63c + e852391 commit 1626c34
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ flag:

go run examples/main.go -- -v=0

For logging of requests and responses a [logging modifier](https://github.com/google/martian/wiki/Modifier-Reference#logging) is available

By default, Martian will be running on port 8080. The port can be specified via
a flag:

Expand Down
29 changes: 29 additions & 0 deletions examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,35 @@ matches "www.example.com"
the "modifier" key within the "url.Filter" JSON object contains another
modifier message of the type header.Modifier to run iff the filter passes
groups may also be used to run multiple modifiers sequentially; for example to
log requests and responses after adding the "Martian-Test" header to the
request, but only when the host matches www.example.com:
{
"url.Filter": {
"host": "www.example.com",
"modifier": {
"fifo.Group": {
"modifiers": [
{
"header.Modifier": {
"scope": ["request"],
"name": "Martian-Test",
"value": "true"
}
},
{
"log.Logger": { }
}
]
}
}
}
}
modifiers are designed to be composed together in ways that allow the user to
write a single JSON structure to accomplish a variety of functionality
GET host:port/martian/verify
retrieves the verifications errors as JSON with the following structure:
Expand Down
129 changes: 129 additions & 0 deletions log/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package log provides a Martian modifier that logs the request and response.
package log

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/google/martian"
"github.com/google/martian/parse"
)

// Logger is a modifier that logs requests and responses.
type Logger struct {
log func(line string)
}

type loggerJSON struct {
Scope []parse.ModifierType `json:"scope"`
}

func init() {
parse.Register("log.Logger", loggerFromJSON)
}

// NewLogger returns a logger that logs requests and responses. Log function defaults to martian.Infof.
func NewLogger() *Logger {
return &Logger{
log: func(line string) {
martian.Infof(line)
},
}
}

// SetLogFunc sets the logging function for the logger.
func (l *Logger) SetLogFunc(logFunc func(line string)) {
l.log = logFunc
}

// ModifyRequest logs the request. Note that the body of the request is not logged.
//
// The format logged is:
// --------------------------------------------------------------------------------
// Request to http://www.google.com/path?querystring
// --------------------------------------------------------------------------------
// GET /path?querystring HTTP/1.1
// Host: www.google.com
// Connection: close
// Other-Header: values
// --------------------------------------------------------------------------------
func (l *Logger) ModifyRequest(ctx *martian.Context, req *http.Request) error {
b := &bytes.Buffer{}

fmt.Fprintln(b, "")
fmt.Fprintln(b, strings.Repeat("-", 80))
fmt.Fprintf(b, "Request to %s\n", req.URL)
fmt.Fprintln(b, strings.Repeat("-", 80))
fmt.Fprintf(b, "%s %s %s\r\n", req.Method, req.RequestURI, req.Proto)
fmt.Fprintf(b, "Host: %s\r\n", req.Host)

if req.Close {
fmt.Fprint(b, "Connection: close\r\n")
}

req.Header.Write(b)
fmt.Fprintln(b, strings.Repeat("-", 80))

l.log(b.String())

return nil
}

// ModifyResponse logs the response. Note that the body of the response is not logged.
//
// The format logged is:
// --------------------------------------------------------------------------------
// Response from http://www.google.com/path?querystring
// --------------------------------------------------------------------------------
// HTTP/1.1 200 OK
// Date: Tue, 15 Nov 1994 08:12:31 GMT
// Other-Header: values
// --------------------------------------------------------------------------------
func (l *Logger) ModifyResponse(ctx *martian.Context, res *http.Response) error {
b := &bytes.Buffer{}
fmt.Fprintln(b, "")
fmt.Fprintln(b, strings.Repeat("-", 80))
fmt.Fprintf(b, "Response from %s\n", res.Request.URL)
fmt.Fprintln(b, strings.Repeat("-", 80))
fmt.Fprintf(b, "%s %s\r\n", res.Proto, res.Status)
res.Header.Write(b)
fmt.Fprintln(b, strings.Repeat("-", 80))

l.log(b.String())

return nil
}

// loggerFromJSON builds a logger from JSON.
//
// Example JSON:
// {
// "log.Logger": {
// "scope": ["request", "response"]
// }
// }
func loggerFromJSON(b []byte) (*parse.Result, error) {
msg := &loggerJSON{}
if err := json.Unmarshal(b, msg); err != nil {
return nil, err
}

return parse.NewResult(NewLogger(), msg.Scope)
}
103 changes: 103 additions & 0 deletions log/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package log

import (
"fmt"
"net/http"
"strings"
"testing"

"github.com/google/martian"
"github.com/google/martian/parse"
"github.com/google/martian/proxyutil"
)

func ExampleLogger() {
l := NewLogger()
l.SetLogFunc(func(line string) {
// Remove \r to make it easier to test with examples.
fmt.Print(strings.Replace(line, "\r", "", -1))
})

req, err := http.NewRequest("GET", "http://example.com/path?querystring", strings.NewReader("body"))
if err != nil {
fmt.Println(err)
return
}
req.RequestURI = req.URL.RequestURI()
req.Header.Set("Other-Header", "values")
req.Close = true

if err := l.ModifyRequest(martian.NewContext(), req); err != nil {
fmt.Println(err)
return
}

res := proxyutil.NewResponse(200, strings.NewReader("body"), req)
res.Header.Set("Date", "Tue, 15 Nov 1994 08:12:31 GMT")
res.Header.Set("Other-Header", "values")

if err := l.ModifyResponse(martian.NewContext(), res); err != nil {
fmt.Println(err)
return
}
// Output:
// --------------------------------------------------------------------------------
// Request to http://example.com/path?querystring
// --------------------------------------------------------------------------------
// GET /path?querystring HTTP/1.1
// Host: example.com
// Connection: close
// Other-Header: values
// --------------------------------------------------------------------------------
//
// --------------------------------------------------------------------------------
// Response from http://example.com/path?querystring
// --------------------------------------------------------------------------------
// HTTP/1.1 200 OK
// Date: Tue, 15 Nov 1994 08:12:31 GMT
// Other-Header: values
// --------------------------------------------------------------------------------
}

func TestLoggerFromJSON(t *testing.T) {
msg := []byte(`{
"log.Logger": {
"scope": ["request", "response"]
}
}`)

r, err := parse.FromJSON(msg)
if err != nil {
t.Fatalf("parse.FromJSON(): got %v, want no error", err)
}

reqmod := r.RequestModifier()
if reqmod == nil {
t.Fatal("r.RequestModifier(): got nil, want not nil")
}
if _, ok := reqmod.(*Logger); !ok {
t.Error("reqmod.(*Logger): got !ok, want ok")
}

resmod := r.ResponseModifier()
if resmod == nil {
t.Fatal("r.ResponseModifier(): got nil, want not nil")
}
if _, ok := resmod.(*Logger); !ok {
t.Error("resmod.(*Logger); got !ok, want ok")
}
}

0 comments on commit 1626c34

Please sign in to comment.