Skip to content

Commit 251ebe2

Browse files
authored
Merge pull request #6 from aaronvb/add-log-output
Add log to file option
2 parents 570e6a0 + e53cd0c commit 251ebe2

File tree

13 files changed

+304
-41
lines changed

13 files changed

+304
-41
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
.DS_Store
22
dist/
3+
requests.http
4+
*.log
5+
request_hole

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Flags:
4545
-a, --address string sets the address for the endpoint (default "localhost")
4646
--details shows header details in the request
4747
-h, --help help for rh
48+
--log string writes incoming requests to the specified log file (example: --log rh.log)
4849
-p, --port int sets the port for the endpoint (default 8080)
4950
-r, --response_code int sets the response code (default 200)
5051
@@ -65,6 +66,13 @@ $ rh http --details
6566
```
6667
<img width="785" alt="Request Hole CLI details" src="https://user-images.githubusercontent.com/100900/120266674-1d48c000-c23e-11eb-8107-50db997ac3cc.png">
6768

69+
### Log to file
70+
This option will write the CLI output to the specified log file. Works with other options such as `--details`.
71+
```
72+
$ rh http --log rh.log
73+
```
74+
<img width="787" alt="Request Hole CLI log" src="https://user-images.githubusercontent.com/100900/120877567-fac2e980-c552-11eb-8ec0-8075bc6c0cd8.png">
75+
6876
### Exposing Request Hole to the internet
6977
Sometimes we need to expose `rh` to the internet to test applications or webhooks from outside of our local dev env. The best way to do this is to use a tunneling service such as [ngrok](https://ngrok.com).
7078
```

cmd/protocol.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"github.com/spf13/cobra"
77
)
88

9-
var getCmd = &cobra.Command{
9+
var httpCmd = &cobra.Command{
1010
Use: "http",
1111
Short: "Creates an http endpoint",
1212
Long: `rh: http
@@ -16,11 +16,29 @@ Create an endpoint that accepts http connections.
1616
}
1717

1818
func init() {
19-
rootCmd.AddCommand(getCmd)
19+
rootCmd.AddCommand(httpCmd)
2020
}
2121

2222
func http(cmd *cobra.Command, args []string) {
23-
renderer := &renderer.Printer{Port: Port, Addr: Address, BuildInfo: BuildInfo, Details: Details}
24-
httpServer := server.Http{Addr: Address, Port: Port, ResponseCode: ResponseCode, Output: renderer}
23+
logOutput := &renderer.Logger{
24+
File: LogFile,
25+
Port: Port,
26+
Addr: Address,
27+
}
28+
output := &renderer.Printer{
29+
Port: Port,
30+
Addr: Address,
31+
BuildInfo: BuildInfo,
32+
LogFile: LogFile,
33+
Details: Details}
34+
35+
httpServer := server.Http{
36+
Addr: Address,
37+
Port: Port,
38+
ResponseCode: ResponseCode,
39+
Output: output,
40+
LogOutput: logOutput,
41+
Details: Details}
42+
2543
httpServer.Start()
2644
}

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: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package renderer
22

33
import (
44
"fmt"
5-
"log"
65
"os"
76
"sort"
87
"strings"
@@ -24,7 +23,12 @@ type Printer struct {
2423
// Contains build info
2524
BuildInfo map[string]string
2625

27-
// 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.
2832
Details bool
2933
}
3034

@@ -55,16 +59,13 @@ func (p *Printer) startText() string {
5559
if p.Details {
5660
text = fmt.Sprintf("%s\nDetails: %t", text, p.Details)
5761
}
62+
if p.LogFile != "" {
63+
text = fmt.Sprintf("%s\nLog: %s", text, p.LogFile)
64+
}
5865

5966
return text
6067
}
6168

62-
// ErrorLogger will create a printerLog which interfaces with Logger.
63-
func (p *Printer) ErrorLogger() *log.Logger {
64-
errorLog := log.New(&printerLog{prefix: pterm.Error}, "", p.Port)
65-
return errorLog
66-
}
67-
6869
// Fatal will use the Error prefix to render the error and then exit the CLI.
6970
func (p *Printer) Fatal(err error) {
7071
p.Spinner.Stop()
@@ -73,7 +74,7 @@ func (p *Printer) Fatal(err error) {
7374
}
7475

7576
// IncomingRequest handles the output for incoming requests to the server.
76-
func (p *Printer) IncomingRequest(fields logrequest.RequestFields, params string, headers map[string][]string) {
77+
func (p *Printer) IncomingRequest(fields logrequest.RequestFields, params string) {
7778
p.Spinner.Stop()
7879
prefix := pterm.Prefix{
7980
Text: fields.Method,
@@ -83,10 +84,15 @@ func (p *Printer) IncomingRequest(fields logrequest.RequestFields, params string
8384
text := p.incomingRequestText(fields, params)
8485
pterm.Info.WithPrefix(prefix).Println(text)
8586

86-
if p.Details {
87-
table := p.incomingRequestHeadersTable(headers)
88-
pterm.Printf("%s\n\n", table)
89-
}
87+
p.startSpinner()
88+
}
89+
90+
// IncomingRequestHeader handles the output for incoming requests headers to the server.
91+
func (p *Printer) IncomingRequestHeaders(headers map[string][]string) {
92+
p.Spinner.Stop()
93+
94+
table := p.incomingRequestHeadersTable(headers)
95+
pterm.Printf("%s\n\n", table)
9096

9197
p.startSpinner()
9298
}

pkg/renderer/printerLog.go renamed to pkg/renderer/printer_log.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ package renderer
33
import "github.com/pterm/pterm"
44

55
// printerLog is our interface to Logger and accepts a pterm prefix
6-
type printerLog struct {
7-
prefix pterm.PrefixPrinter
6+
type PrinterLog struct {
7+
Prefix pterm.PrefixPrinter
88
}
99

1010
// Write will be used by the function calling our printerLog.
1111
// If no prefix is passed, we default to Info
12-
func (pl printerLog) Write(b []byte) (n int, err error) {
13-
if pl.prefix.Prefix.Text == "" {
14-
pl.prefix = pterm.Info
12+
func (pl *PrinterLog) Write(b []byte) (n int, err error) {
13+
if pl.Prefix.Prefix.Text == "" {
14+
pl.Prefix = pterm.Info
1515
}
16-
pl.prefix.WithShowLineNumber(false).Println(string(b))
16+
17+
str := pl.Prefix.WithShowLineNumber(false).Sprint(string(b))
18+
pterm.Println(str)
19+
1720
return len(b), nil
1821
}

pkg/renderer/printer_log_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package renderer
2+
3+
import (
4+
"testing"
5+
6+
"github.com/pterm/pterm"
7+
)
8+
9+
func TestWrite(t *testing.T) {
10+
pterm.DisableOutput()
11+
12+
pl := PrinterLog{}
13+
b := []byte("foobar")
14+
i, _ := pl.Write(b)
15+
16+
if i != len(b) {
17+
t.Errorf("Expected str len %d, got %d", len(b), i)
18+
}
19+
}

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{}

0 commit comments

Comments
 (0)