Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Expand logging support for HTTP traffic (#60)
- Respect the GH_DEBUG environment variable by default, but add the option to skip it; - Add option to opt into colorized logging; - Add option to explicitly log HTTP headers and bodies—the new default is off; - Colorize JSON payloads when logging HTTP bodies; - Log form-encoded payloads; - Pretty-print GraphQL queries when logging HTTP requests. Co-authored-by: Sam Coe <samcoe@users.noreply.github.com>
- Loading branch information
Showing
8 changed files
with
341 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package api | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/cli/go-gh/pkg/jsonpretty" | ||
) | ||
|
||
type graphqlBody struct { | ||
Query string `json:"query"` | ||
OperationName string `json:"operationName"` | ||
Variables json.RawMessage `json:"variables"` | ||
} | ||
|
||
// jsonFormatter is a httpretty.Formatter that prettifies JSON payloads and GraphQL queries. | ||
type jsonFormatter struct { | ||
colorize bool | ||
} | ||
|
||
func (f *jsonFormatter) Format(w io.Writer, src []byte) error { | ||
var graphqlQuery graphqlBody | ||
// TODO: find more precise way to detect a GraphQL query from the JSON payload alone | ||
if err := json.Unmarshal(src, &graphqlQuery); err == nil && graphqlQuery.Query != "" && len(graphqlQuery.Variables) > 0 { | ||
colorHighlight := "\x1b[35;1m" | ||
colorReset := "\x1b[m" | ||
if !f.colorize { | ||
colorHighlight = "" | ||
colorReset = "" | ||
} | ||
if _, err := fmt.Fprintf(w, "%sGraphQL query:%s\n%s\n", colorHighlight, colorReset, strings.ReplaceAll(strings.TrimSpace(graphqlQuery.Query), "\t", " ")); err != nil { | ||
return err | ||
} | ||
if _, err := fmt.Fprintf(w, "%sGraphQL variables:%s %s\n", colorHighlight, colorReset, string(graphqlQuery.Variables)); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
return jsonpretty.Format(w, bytes.NewReader(src), " ", f.colorize) | ||
} | ||
|
||
func (f *jsonFormatter) Match(t string) bool { | ||
return jsonTypeRE.MatchString(t) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// Package jsonpretty implements a terminal pretty-printer for JSON. | ||
package jsonpretty | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"strings" | ||
) | ||
|
||
const ( | ||
colorDelim = "\x1b[1;38m" // bright white | ||
colorKey = "\x1b[1;34m" // bright blue | ||
colorNull = "\x1b[36m" // cyan | ||
colorString = "\x1b[32m" // green | ||
colorBool = "\x1b[33m" // yellow | ||
colorReset = "\x1b[m" | ||
) | ||
|
||
// Format reads JSON from r and writes a prettified version of it to w. | ||
func Format(w io.Writer, r io.Reader, indent string, colorize bool) error { | ||
dec := json.NewDecoder(r) | ||
dec.UseNumber() | ||
|
||
c := func(ansi string) string { | ||
if !colorize { | ||
return "" | ||
} | ||
return ansi | ||
} | ||
|
||
var idx int | ||
var stack []json.Delim | ||
|
||
for { | ||
t, err := dec.Token() | ||
if err == io.EOF { | ||
break | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
|
||
switch tt := t.(type) { | ||
case json.Delim: | ||
switch tt { | ||
case '{', '[': | ||
stack = append(stack, tt) | ||
idx = 0 | ||
if _, err := fmt.Fprint(w, c(colorDelim), tt, c(colorReset)); err != nil { | ||
return err | ||
} | ||
if dec.More() { | ||
if _, err := fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack))); err != nil { | ||
return err | ||
} | ||
} | ||
continue | ||
case '}', ']': | ||
stack = stack[:len(stack)-1] | ||
idx = 0 | ||
if _, err := fmt.Fprint(w, c(colorDelim), tt, c(colorReset)); err != nil { | ||
return err | ||
} | ||
} | ||
default: | ||
b, err := marshalJSON(tt) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
isKey := len(stack) > 0 && stack[len(stack)-1] == '{' && idx%2 == 0 | ||
idx++ | ||
|
||
var color string | ||
if isKey { | ||
color = colorKey | ||
} else if tt == nil { | ||
color = colorNull | ||
} else { | ||
switch t.(type) { | ||
case string: | ||
color = colorString | ||
case bool: | ||
color = colorBool | ||
} | ||
} | ||
|
||
if color != "" { | ||
if _, err := fmt.Fprint(w, c(color)); err != nil { | ||
return err | ||
} | ||
} | ||
if _, err := w.Write(b); err != nil { | ||
return err | ||
} | ||
if color != "" { | ||
if _, err := fmt.Fprint(w, c(colorReset)); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if isKey { | ||
if _, err := fmt.Fprint(w, c(colorDelim), ":", c(colorReset), " "); err != nil { | ||
return err | ||
} | ||
continue | ||
} | ||
} | ||
|
||
if dec.More() { | ||
if _, err := fmt.Fprint(w, c(colorDelim), ",", c(colorReset), "\n", strings.Repeat(indent, len(stack))); err != nil { | ||
return err | ||
} | ||
} else if len(stack) > 0 { | ||
if _, err := fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack)-1)); err != nil { | ||
return err | ||
} | ||
} else { | ||
if _, err := fmt.Fprint(w, "\n"); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// marshalJSON works like json.Marshal, but with HTML-escaping disabled. | ||
func marshalJSON(v interface{}) ([]byte, error) { | ||
buf := bytes.Buffer{} | ||
enc := json.NewEncoder(&buf) | ||
enc.SetEscapeHTML(false) | ||
if err := enc.Encode(v); err != nil { | ||
return nil, err | ||
} | ||
bb := buf.Bytes() | ||
// omit trailing newline added by json.Encoder | ||
if len(bb) > 0 && bb[len(bb)-1] == '\n' { | ||
return bb[:len(bb)-1], nil | ||
} | ||
return bb, nil | ||
} |
Oops, something went wrong.