Skip to content

Commit d5c77f0

Browse files
committed
Add logging to file option
Accepts a CLI flag `--log <filename>` which will write the CLI output to a log file.
1 parent 77995e7 commit d5c77f0

File tree

7 files changed

+211
-7
lines changed

7 files changed

+211
-7
lines changed

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var (
99
ResponseCode int
1010
BuildInfo map[string]string
1111
Details bool
12+
LogFile string
1213
)
1314

1415
var rootCmd = &cobra.Command{
@@ -28,4 +29,5 @@ func init() {
2829
rootCmd.PersistentFlags().StringVarP(&Address, "address", "a", "localhost", "sets the address for the endpoint")
2930
rootCmd.PersistentFlags().IntVarP(&ResponseCode, "response_code", "r", 200, "sets the response code")
3031
rootCmd.PersistentFlags().BoolVar(&Details, "details", false, "shows header details in the request")
32+
rootCmd.PersistentFlags().StringVar(&LogFile, "log", "", "writes incoming requests to the specified log file (example: --log rh.log)")
3133
}

pkg/renderer/logger.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package renderer
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"sort"
7+
"strings"
8+
"time"
9+
10+
"github.com/aaronvb/logrequest"
11+
"github.com/pterm/pterm"
12+
)
13+
14+
// Logger outputs to a log file.
15+
type Logger struct {
16+
// File points to the file that we write to.
17+
File string
18+
19+
// Fields for startText
20+
Port int
21+
Addr string
22+
}
23+
24+
// Start writes the initial server start to the log file.
25+
func (l *Logger) Start() {
26+
if l.File == "" {
27+
return
28+
}
29+
30+
f, err := os.OpenFile(l.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
31+
if err != nil {
32+
l.Fatal(err)
33+
}
34+
35+
defer f.Close()
36+
37+
str := fmt.Sprintf("%s: %s\n", time.Now().Format("2006/02/01 15:04:05"), l.startText())
38+
f.WriteString(str)
39+
}
40+
41+
func (l *Logger) startText() string {
42+
return fmt.Sprintf("Listening on http://%s:%d", l.Addr, l.Port)
43+
}
44+
45+
// Fatal will use the Error prefix to render the error and then exit the CLI.
46+
func (l *Logger) Fatal(err error) {
47+
pterm.Error.WithShowLineNumber(false).Println(err)
48+
os.Exit(1)
49+
}
50+
51+
// IncomingRequest writes the incoming requests to the log file.
52+
func (l *Logger) IncomingRequest(fields logrequest.RequestFields, params string) {
53+
if l.File == "" {
54+
return
55+
}
56+
57+
f, err := os.OpenFile(l.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
58+
if err != nil {
59+
l.Fatal(err)
60+
}
61+
62+
defer f.Close()
63+
64+
str := fmt.Sprintf("%s: %s\n", time.Now().Format("2006/02/01 15:04:05"), l.incomingRequestText(fields, params))
65+
f.WriteString(str)
66+
}
67+
68+
func (l *Logger) incomingRequestText(fields logrequest.RequestFields, params string) string {
69+
return fmt.Sprintf("%s %s %s", fields.Method, fields.Url, params)
70+
}
71+
72+
// IncomingRequestHeaders writes the incoming request headers to the log file
73+
func (l *Logger) IncomingRequestHeaders(headers map[string][]string) {
74+
if l.File == "" {
75+
return
76+
}
77+
78+
f, err := os.OpenFile(l.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
79+
if err != nil {
80+
l.Fatal(err)
81+
}
82+
83+
defer f.Close()
84+
85+
headersWithJoinedValues, keys := l.incomingRequestHeaders(headers)
86+
87+
for _, key := range keys {
88+
str := fmt.Sprintf("%s: %s: %s\n", time.Now().Format("2006/02/01 15:04:05"), key, headersWithJoinedValues[key])
89+
f.WriteString(str)
90+
}
91+
}
92+
93+
// incomingRequestHeaders takes the headers from the request, sorts them alphabetically,
94+
// joins the values, and creates a new map
95+
func (l *Logger) incomingRequestHeaders(headers map[string][]string) (map[string]string, []string) {
96+
headersWithJoinedValues := make(map[string]string)
97+
for key, val := range headers {
98+
value := strings.Join(val, ",")
99+
headersWithJoinedValues[key] = value
100+
}
101+
102+
keys := make([]string, 0, len(headers))
103+
for key := range headers {
104+
keys = append(keys, key)
105+
}
106+
107+
sort.Strings(keys)
108+
109+
return headersWithJoinedValues, keys
110+
}

pkg/renderer/logger_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package renderer
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/aaronvb/logrequest"
9+
)
10+
11+
func TestLoggerStartText(t *testing.T) {
12+
logger := Logger{Port: 123, Addr: "foo.bar"}
13+
text := logger.startText()
14+
expected := fmt.Sprintf("Listening on http://%s:%d", logger.Addr, logger.Port)
15+
16+
if text != expected {
17+
t.Errorf("Expected %s, got %s", expected, text)
18+
}
19+
}
20+
21+
func TestLoggerIncomingRequest(t *testing.T) {
22+
logger := Logger{}
23+
fields := logrequest.RequestFields{
24+
Method: "GET",
25+
Url: "/foobar",
26+
}
27+
params := "{\"foo\" => \"bar\"}"
28+
text := logger.incomingRequestText(fields, params)
29+
expected := fmt.Sprintf("%s %s %s", fields.Method, fields.Url, params)
30+
31+
if text != expected {
32+
t.Errorf("Expected %s, got %s", expected, text)
33+
}
34+
}
35+
36+
func TestIncomingRequestHeadersText(t *testing.T) {
37+
logger := Logger{}
38+
headers := map[string][]string{
39+
"hello": {"world", "foobar"},
40+
"foo": {"bar"},
41+
}
42+
exepectedHeaders := map[string]string{
43+
"hello": "world,foobar",
44+
"foo": "bar",
45+
}
46+
expectedSortedKeys := []string{
47+
"foo", "hello",
48+
}
49+
50+
headersWithJoinedValues, keys := logger.incomingRequestHeaders(headers)
51+
52+
if !reflect.DeepEqual(headersWithJoinedValues, exepectedHeaders) {
53+
t.Errorf("Expected %s, got %s", exepectedHeaders, headersWithJoinedValues)
54+
}
55+
56+
if !reflect.DeepEqual(keys, expectedSortedKeys) {
57+
t.Errorf("Expected %s, got %s", expectedSortedKeys, keys)
58+
}
59+
}

pkg/renderer/printer.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ type Printer struct {
2323
// Contains build info
2424
BuildInfo map[string]string
2525

26-
// Determines if header details should be shown with the request
26+
// Log file location for the CLI header that shows the user
27+
// the entered log file location. Not used for writing to
28+
LogFile string
29+
30+
// Details used in the header to show the user if they passed
31+
// the flag.
2732
Details bool
2833
}
2934

@@ -54,6 +59,9 @@ func (p *Printer) startText() string {
5459
if p.Details {
5560
text = fmt.Sprintf("%s\nDetails: %t", text, p.Details)
5661
}
62+
if p.LogFile != "" {
63+
text = fmt.Sprintf("%s\nLog: %s", text, p.LogFile)
64+
}
5765

5866
return text
5967
}
@@ -66,7 +74,7 @@ func (p *Printer) Fatal(err error) {
6674
}
6775

6876
// IncomingRequest handles the output for incoming requests to the server.
69-
func (p *Printer) IncomingRequest(fields logrequest.RequestFields, params string, headers map[string][]string) {
77+
func (p *Printer) IncomingRequest(fields logrequest.RequestFields, params string) {
7078
p.Spinner.Stop()
7179
prefix := pterm.Prefix{
7280
Text: fields.Method,

pkg/renderer/printer_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ func TestStartTextWithDetails(t *testing.T) {
3636
}
3737
}
3838

39+
func TestStartTextWithLogFile(t *testing.T) {
40+
pterm.DisableColor()
41+
printer := Printer{
42+
Addr: "localhost",
43+
Port: 8080,
44+
BuildInfo: map[string]string{"version": "dev"},
45+
LogFile: "rh.log"}
46+
result := printer.startText()
47+
expected := fmt.Sprintf(
48+
"Request Hole %s\nListening on http://%s:%d\nLog: %s", "dev",
49+
printer.Addr, printer.Port, printer.LogFile)
50+
51+
if result != expected {
52+
t.Errorf("Expected %s, got %s", expected, result)
53+
}
54+
}
55+
3956
func TestIncomingRequestText(t *testing.T) {
4057
pterm.DisableColor()
4158
printer := Printer{}

pkg/server/http.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@ type Http struct {
2525

2626
// Output is the Renderer interface.
2727
Output renderer.Renderer
28+
29+
// LogOutput
30+
LogOutput renderer.Renderer
31+
2832
// Determines if header details should be shown with the request
2933
Details bool
3034
}
3135

3236
// Start will start the HTTP server.
3337
func (s *Http) Start() {
3438
s.Output.Start()
39+
s.LogOutput.Start()
40+
3541
addr := fmt.Sprintf("%s:%d", s.Addr, s.Port)
3642
errorLog := log.New(&renderer.PrinterLog{Prefix: pterm.Error}, "", 0)
3743

@@ -68,7 +74,10 @@ func (s *Http) logRequest(next http.Handler) http.Handler {
6874
lr := logrequest.LogRequest{Request: r, Writer: w, Handler: next}
6975
fields := lr.ToFields()
7076
params := logparams.LogParams{Request: r, HidePrefix: true}
71-
s.Output.IncomingRequest(fields, params.ToString(), r.Header)
77+
78+
s.Output.IncomingRequest(fields, params.ToString())
79+
s.LogOutput.IncomingRequest(fields, params.ToString())
80+
7281
if s.Details {
7382
s.Output.IncomingRequestHeaders(r.Header)
7483
s.LogOutput.IncomingRequestHeaders(r.Header)

pkg/server/http_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ type MockPrinter struct {
1515
headers map[string][]string
1616
}
1717

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

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

3433
renderer := &MockPrinter{}
3534
for _, respCode := range tests {
36-
httpServer := Http{ResponseCode: respCode, Output: renderer}
35+
httpServer := Http{ResponseCode: respCode, Output: renderer, LogOutput: renderer}
3736
srv := httptest.NewServer(httpServer.routes())
3837
req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
3938
if err != nil {
@@ -66,7 +65,7 @@ func TestLogRequest(t *testing.T) {
6665
{http.MethodGet, "/foo/bar?hello=world", "", "{\"hello\" => \"world\"}"},
6766
}
6867
renderer := &MockPrinter{}
69-
httpServer := Http{ResponseCode: 200, Output: renderer}
68+
httpServer := Http{ResponseCode: 200, Output: renderer, LogOutput: renderer}
7069
srv := httptest.NewServer(httpServer.routes())
7170
defer srv.Close()
7271

@@ -110,7 +109,7 @@ func TestLogRequestHeaders(t *testing.T) {
110109
}
111110

112111
renderer := &MockPrinter{}
113-
httpServer := Http{ResponseCode: 200, Output: renderer}
112+
httpServer := Http{ResponseCode: 200, Output: renderer, LogOutput: renderer, Details: true}
114113
srv := httptest.NewServer(httpServer.routes())
115114
defer srv.Close()
116115

0 commit comments

Comments
 (0)