Skip to content
6 changes: 4 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ linters:
- nlreturn
- whitespace
- wsl
# - wsl_v5 (when linter is upgraded)

# nice to have
- usetesting
- unused
- testifylint
- perfsprint

settings:
Expand Down Expand Up @@ -105,6 +105,8 @@ linters:
disabled: true
- name: unused-receiver
disabled: true
- name: unhandled-error
disabled: true

staticcheck:
checks:
Expand Down Expand Up @@ -136,4 +138,4 @@ formatters:
paths:
- third_party$
- builtin$
- examples$
- examples$
67 changes: 62 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,70 @@
.PHONY: modernize modernize-fix modernize-check
#----------------------
# Parse makefile arguments
#----------------------
RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
$(eval $(RUN_ARGS):;@:)

MODERNIZE_CMD = go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/[email protected]
#----------------------
# Silence GNU Make
#----------------------
ifndef VERBOSE
MAKEFLAGS += --no-print-directory
endif

#----------------------
# Terminal
#----------------------

GREEN := $(shell tput -Txterm setaf 2)
WHITE := $(shell tput -Txterm setaf 7)
YELLOW := $(shell tput -Txterm setaf 3)
RESET := $(shell tput -Txterm sgr0)

#------------------------------------------------------------------
# - Add the following 'help' target to your Makefile
# - Add help text after each target name starting with '\#\#'
# - A category can be added with @category
#------------------------------------------------------------------

HELP_FUN = \
%help; \
while(<>) { \
push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \
print "\n"; \
for (sort keys %help) { \
print "${WHITE}$$_${RESET \
}\n"; \
for (@{$$help{$$_}}) { \
$$sep = " " x (32 - length $$_->[0]); \
print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \
}; \
print ""; \
}

modernize: modernize-fix
help: ##@other Show this help.
@perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)

modernize-fix:
#----------------------
# tool
#----------------------

MODERNIZE_CMD = go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/[email protected]

modernize-fix: ##@tool Run modernize fix
@echo "Running gopls modernize with -fix..."
$(MODERNIZE_CMD) -test -fix ./...

modernize-check:
modernize-check: ##@tool Run modernize check
@echo "Checking if code needs modernization..."
$(MODERNIZE_CMD) -test ./...

linter-run: ##@tool Run Go linter
@echo "Running linter..."
go run github.com/golangci/golangci-lint/v2/cmd/[email protected] run -v
@echo "Linter run complete."

run-all: ##@tool Run all tools
@echo "Running all tools..."
make modernize-check
make linter-run
@echo "All tools run complete."
8 changes: 4 additions & 4 deletions godump_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ func TestFindFirstNonInternalFrameFallback(t *testing.T) {
// Trigger the fallback by skipping deeper
file, line := newDumperT(t).findFirstNonInternalFrame(0)
// We can't assert much here reliably, but calling it adds coverage
assert.True(t, len(file) >= 0)
assert.True(t, line >= 0)
assert.Greater(t, len(file), 1)
assert.Greater(t, line, 1)
}

func TestUnreadableFieldFallback(t *testing.T) {
Expand Down Expand Up @@ -680,7 +680,7 @@ func TestFindFirstNonInternalFrame_FallbackBranch(t *testing.T) {
}

file, line := testDumper.findFirstNonInternalFrame(0)
assert.Equal(t, "", file)
assert.Empty(t, file)
assert.Equal(t, 0, line)
}

Expand All @@ -701,7 +701,7 @@ func TestPrintDumpHeader_SkipWhenNoFrame(t *testing.T) {

var b strings.Builder
testDumper.printDumpHeader(&b)
assert.Equal(t, "", b.String()) // nothing should be written
assert.Empty(t, b.String())
}

type customChan chan int
Expand Down
141 changes: 141 additions & 0 deletions http_dumper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package godump

import (
"fmt"
"net/http"
"net/http/httputil"
"os"
"sort"
"strings"
"time"
)

// HTTPDebugTransport wraps a http.RoundTripper to optionally log requests and responses.
type HTTPDebugTransport struct {
Transport http.RoundTripper
debugEnabled bool
dumper *Dumper
}

// NewHTTPDebugTransport creates a HTTPDebugTransport with debug flag cached from env.
func NewHTTPDebugTransport(inner http.RoundTripper, opts ...Option) *HTTPDebugTransport {
combinedOpts := append([]Option{WithSkipStackFrames(4)}, opts...)
return &HTTPDebugTransport{
Transport: inner,
debugEnabled: os.Getenv("HTTP_DEBUG") != "",
dumper: NewDumper(combinedOpts...),
}
}

// SetDebug allows toggling debug logging at runtime.
func (t *HTTPDebugTransport) SetDebug(enabled bool) {
t.debugEnabled = enabled
}

// Dumper returns the Dumper instance used for logging.
func (t *HTTPDebugTransport) Dumper() *Dumper {
return t.dumper
}

// RoundTrip implements the http.RoundTripper interface.
func (t *HTTPDebugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if !t.debugEnabled {
resp, err := t.Transport.RoundTrip(req)
if err != nil {
return resp, fmt.Errorf("HTTPDebugTransport: pass-through round trip failed: %w", err)
}
return resp, nil
}

start := time.Now()

// Dump Request
reqDump, err := httputil.DumpRequestOut(req, true)
if err != nil {
return nil, fmt.Errorf("HTTPDebugTransport: failed to dump request: %w", err)
}
request := parseHTTPDump("Request", string(reqDump))

// Perform request
resp, err := t.Transport.RoundTrip(req)
if err != nil {
return nil, fmt.Errorf("HTTPDebugTransport: round trip failed: %w", err)
}
duration := time.Since(start)

// Dump Response
resDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, fmt.Errorf("HTTPDebugTransport: failed to dump response: %w", err)
}
response := parseHTTPDump("Response", string(resDump))

// Combine and dump
transaction := map[string]any{
"Transaction": map[string]any{
"Request": request,
"Response": response,
"Duration": duration.String(),
},
}
t.dumper.Dump(transaction)

return resp, nil
}

// parseHTTPDump parses the raw HTTP dump into a structured map.
func parseHTTPDump(label, raw string) map[string]any {
lines := strings.Split(raw, "\n")
payload := make(map[string]any)
headers := make(map[string]string)
inBody := false
var bodyBuilder strings.Builder
for i, line := range lines {
line = strings.TrimRight(line, "\r\n")

// Skip empty lines
if i == 0 {
if label == "Request" {
payload["Request-Line"] = line
} else {
payload["Status"] = line
}
continue
}

// If we are in the body, accumulate lines
if inBody {
bodyBuilder.WriteString(line + "\n")
continue
}

// If we hit an empty line, switch to body mode
if line == "" {
inBody = true
continue
}

// Parse headers
if key, value, found := strings.Cut(line, ":"); found {
headers[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
}

// Alphabetize headers
keys := make([]string, 0, len(headers))
for k := range headers {
keys = append(keys, k)
}
sort.Strings(keys)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort.Strings is somehow deprecated

Note: as of Go 1.22, this function simply calls slices.Sort.

use slices.Sort

Suggested change
sort.Strings(keys)
slices.Sort(keys)

for _, k := range keys {
payload[k] = headers[k]
}

// Add body as raw
body := strings.TrimSpace(bodyBuilder.String())
if body != "" {
payload["Body"] = body
}
Comment on lines +134 to +138
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would expect the body to be decoded as JSON in payload["Body_JSON"] it would help, having the body is not that useful, especially when dumped on one line with truncation

Suggested change
// Add body as raw
body := strings.TrimSpace(bodyBuilder.String())
if body != "" {
payload["Body"] = body
}
// Add body as raw
body := strings.TrimSpace(bodyBuilder.String())
if body != "" {
payload["Body"] = body
var rawJSON any
if err := json.Unmarshal(body, &rawJSON); err == nil {
payload["Body_JSON"] = rawJSON
}
}

Or maybe this

Suggested change
// Add body as raw
body := strings.TrimSpace(bodyBuilder.String())
if body != "" {
payload["Body"] = body
}
// Add body as raw
body := strings.TrimSpace(bodyBuilder.String())
if body != "" {
payload["Body"] = body
var rawJSON any
if err := json.Unmarshal(body, &rawJSON); err == nil {
payload["Body"] = rawJSON
}
}

Copy link
Member Author

@Akkadius Akkadius Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code that is here doesn't make any sense. The last main remaining action item of this PR is to address body printing which I'll do at a later point. I'm not in a rush to merge this.


return payload
}
Loading