Skip to content

Commit

Permalink
Add logging to file option
Browse files Browse the repository at this point in the history
Accepts a CLI flag `--log <filename>` which will write the CLI
output to a log file.
  • Loading branch information
aaronvb committed Jun 5, 2021
1 parent 77995e7 commit d5c77f0
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 7 deletions.
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var (
ResponseCode int
BuildInfo map[string]string
Details bool
LogFile string
)

var rootCmd = &cobra.Command{
Expand All @@ -28,4 +29,5 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&Address, "address", "a", "localhost", "sets the address for the endpoint")
rootCmd.PersistentFlags().IntVarP(&ResponseCode, "response_code", "r", 200, "sets the response code")
rootCmd.PersistentFlags().BoolVar(&Details, "details", false, "shows header details in the request")
rootCmd.PersistentFlags().StringVar(&LogFile, "log", "", "writes incoming requests to the specified log file (example: --log rh.log)")
}
110 changes: 110 additions & 0 deletions pkg/renderer/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package renderer

import (
"fmt"
"os"
"sort"
"strings"
"time"

"github.com/aaronvb/logrequest"
"github.com/pterm/pterm"
)

// Logger outputs to a log file.
type Logger struct {
// File points to the file that we write to.
File string

// Fields for startText
Port int
Addr string
}

// Start writes the initial server start to the log file.
func (l *Logger) Start() {
if l.File == "" {
return
}

f, err := os.OpenFile(l.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
l.Fatal(err)
}

defer f.Close()

str := fmt.Sprintf("%s: %s\n", time.Now().Format("2006/02/01 15:04:05"), l.startText())
f.WriteString(str)
}

func (l *Logger) startText() string {
return fmt.Sprintf("Listening on http:https://%s:%d", l.Addr, l.Port)
}

// Fatal will use the Error prefix to render the error and then exit the CLI.
func (l *Logger) Fatal(err error) {
pterm.Error.WithShowLineNumber(false).Println(err)
os.Exit(1)
}

// IncomingRequest writes the incoming requests to the log file.
func (l *Logger) IncomingRequest(fields logrequest.RequestFields, params string) {
if l.File == "" {
return
}

f, err := os.OpenFile(l.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
l.Fatal(err)
}

defer f.Close()

str := fmt.Sprintf("%s: %s\n", time.Now().Format("2006/02/01 15:04:05"), l.incomingRequestText(fields, params))
f.WriteString(str)
}

func (l *Logger) incomingRequestText(fields logrequest.RequestFields, params string) string {
return fmt.Sprintf("%s %s %s", fields.Method, fields.Url, params)
}

// IncomingRequestHeaders writes the incoming request headers to the log file
func (l *Logger) IncomingRequestHeaders(headers map[string][]string) {
if l.File == "" {
return
}

f, err := os.OpenFile(l.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
l.Fatal(err)
}

defer f.Close()

headersWithJoinedValues, keys := l.incomingRequestHeaders(headers)

for _, key := range keys {
str := fmt.Sprintf("%s: %s: %s\n", time.Now().Format("2006/02/01 15:04:05"), key, headersWithJoinedValues[key])
f.WriteString(str)
}
}

// incomingRequestHeaders takes the headers from the request, sorts them alphabetically,
// joins the values, and creates a new map
func (l *Logger) incomingRequestHeaders(headers map[string][]string) (map[string]string, []string) {
headersWithJoinedValues := make(map[string]string)
for key, val := range headers {
value := strings.Join(val, ",")
headersWithJoinedValues[key] = value
}

keys := make([]string, 0, len(headers))
for key := range headers {
keys = append(keys, key)
}

sort.Strings(keys)

return headersWithJoinedValues, keys
}
59 changes: 59 additions & 0 deletions pkg/renderer/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package renderer

import (
"fmt"
"reflect"
"testing"

"github.com/aaronvb/logrequest"
)

func TestLoggerStartText(t *testing.T) {
logger := Logger{Port: 123, Addr: "foo.bar"}
text := logger.startText()
expected := fmt.Sprintf("Listening on http:https://%s:%d", logger.Addr, logger.Port)

if text != expected {
t.Errorf("Expected %s, got %s", expected, text)
}
}

func TestLoggerIncomingRequest(t *testing.T) {
logger := Logger{}
fields := logrequest.RequestFields{
Method: "GET",
Url: "/foobar",
}
params := "{\"foo\" => \"bar\"}"
text := logger.incomingRequestText(fields, params)
expected := fmt.Sprintf("%s %s %s", fields.Method, fields.Url, params)

if text != expected {
t.Errorf("Expected %s, got %s", expected, text)
}
}

func TestIncomingRequestHeadersText(t *testing.T) {
logger := Logger{}
headers := map[string][]string{
"hello": {"world", "foobar"},
"foo": {"bar"},
}
exepectedHeaders := map[string]string{
"hello": "world,foobar",
"foo": "bar",
}
expectedSortedKeys := []string{
"foo", "hello",
}

headersWithJoinedValues, keys := logger.incomingRequestHeaders(headers)

if !reflect.DeepEqual(headersWithJoinedValues, exepectedHeaders) {
t.Errorf("Expected %s, got %s", exepectedHeaders, headersWithJoinedValues)
}

if !reflect.DeepEqual(keys, expectedSortedKeys) {
t.Errorf("Expected %s, got %s", expectedSortedKeys, keys)
}
}
12 changes: 10 additions & 2 deletions pkg/renderer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ type Printer struct {
// Contains build info
BuildInfo map[string]string

// Determines if header details should be shown with the request
// Log file location for the CLI header that shows the user
// the entered log file location. Not used for writing to
LogFile string

// Details used in the header to show the user if they passed
// the flag.
Details bool
}

Expand Down Expand Up @@ -54,6 +59,9 @@ func (p *Printer) startText() string {
if p.Details {
text = fmt.Sprintf("%s\nDetails: %t", text, p.Details)
}
if p.LogFile != "" {
text = fmt.Sprintf("%s\nLog: %s", text, p.LogFile)
}

return text
}
Expand All @@ -66,7 +74,7 @@ func (p *Printer) Fatal(err error) {
}

// IncomingRequest handles the output for incoming requests to the server.
func (p *Printer) IncomingRequest(fields logrequest.RequestFields, params string, headers map[string][]string) {
func (p *Printer) IncomingRequest(fields logrequest.RequestFields, params string) {
p.Spinner.Stop()
prefix := pterm.Prefix{
Text: fields.Method,
Expand Down
17 changes: 17 additions & 0 deletions pkg/renderer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ func TestStartTextWithDetails(t *testing.T) {
}
}

func TestStartTextWithLogFile(t *testing.T) {
pterm.DisableColor()
printer := Printer{
Addr: "localhost",
Port: 8080,
BuildInfo: map[string]string{"version": "dev"},
LogFile: "rh.log"}
result := printer.startText()
expected := fmt.Sprintf(
"Request Hole %s\nListening on http:https://%s:%d\nLog: %s", "dev",
printer.Addr, printer.Port, printer.LogFile)

if result != expected {
t.Errorf("Expected %s, got %s", expected, result)
}
}

func TestIncomingRequestText(t *testing.T) {
pterm.DisableColor()
printer := Printer{}
Expand Down
11 changes: 10 additions & 1 deletion pkg/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ type Http struct {

// Output is the Renderer interface.
Output renderer.Renderer

// LogOutput
LogOutput renderer.Renderer

// Determines if header details should be shown with the request
Details bool
}

// Start will start the HTTP server.
func (s *Http) Start() {
s.Output.Start()
s.LogOutput.Start()

addr := fmt.Sprintf("%s:%d", s.Addr, s.Port)
errorLog := log.New(&renderer.PrinterLog{Prefix: pterm.Error}, "", 0)

Expand Down Expand Up @@ -68,7 +74,10 @@ func (s *Http) logRequest(next http.Handler) http.Handler {
lr := logrequest.LogRequest{Request: r, Writer: w, Handler: next}
fields := lr.ToFields()
params := logparams.LogParams{Request: r, HidePrefix: true}
s.Output.IncomingRequest(fields, params.ToString(), r.Header)

s.Output.IncomingRequest(fields, params.ToString())
s.LogOutput.IncomingRequest(fields, params.ToString())

if s.Details {
s.Output.IncomingRequestHeaders(r.Header)
s.LogOutput.IncomingRequestHeaders(r.Header)
Expand Down
7 changes: 3 additions & 4 deletions pkg/server/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ type MockPrinter struct {
headers map[string][]string
}

func (mp *MockPrinter) IncomingRequest(fields logrequest.RequestFields, params string, headers map[string][]string) {
func (mp *MockPrinter) Fatal(error) {}
func (mp *MockPrinter) Start() {}

Expand All @@ -33,7 +32,7 @@ func TestResponseCodeFlag(t *testing.T) {

renderer := &MockPrinter{}
for _, respCode := range tests {
httpServer := Http{ResponseCode: respCode, Output: renderer}
httpServer := Http{ResponseCode: respCode, Output: renderer, LogOutput: renderer}
srv := httptest.NewServer(httpServer.routes())
req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
if err != nil {
Expand Down Expand Up @@ -66,7 +65,7 @@ func TestLogRequest(t *testing.T) {
{http.MethodGet, "/foo/bar?hello=world", "", "{\"hello\" => \"world\"}"},
}
renderer := &MockPrinter{}
httpServer := Http{ResponseCode: 200, Output: renderer}
httpServer := Http{ResponseCode: 200, Output: renderer, LogOutput: renderer}
srv := httptest.NewServer(httpServer.routes())
defer srv.Close()

Expand Down Expand Up @@ -110,7 +109,7 @@ func TestLogRequestHeaders(t *testing.T) {
}

renderer := &MockPrinter{}
httpServer := Http{ResponseCode: 200, Output: renderer}
httpServer := Http{ResponseCode: 200, Output: renderer, LogOutput: renderer, Details: true}
srv := httptest.NewServer(httpServer.routes())
defer srv.Close()

Expand Down

0 comments on commit d5c77f0

Please sign in to comment.