mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-04 06:04:42 +00:00
style: Extract UI constants and refactor main view rendering (#30)
- [x] Add golangci-lint configuration with gocritic ifElseChain disabled
- [x] Rename error variables to avoid shadowing (createErr, watcherErr, watchErr, etc.)
- [x] Replace `interface{}` with `any` type alias throughout codebase
- [x] Add package-level documentation comments to all internal packages
- [x] Reorder struct fields alphabetically for consistency
- [x] Extract UI constants (terminal dimensions, column widths, colors) to constants.go
- [x] Refactor BubbleTeaUI main view rendering into smaller helper functions
- [x] Simplify nested conditionals and improve code clarity
- [x] Add `isForwardDisabled()` helper method to BubbleTeaUI
- [x] Update file permissions from 0644 to 0600 in config tests
- [x] Add `#nosec` comments and error suppression where appropriate
- [x] Improve test table struct field ordering for readability
- [x] Fix resource parsing in AddForward using strings.SplitN
- [x] Add comprehensive tests for new UI helper functions and constants
This commit is contained in:
@@ -1,3 +1,15 @@
|
||||
// Package httplog provides HTTP request/response logging for port forwards.
|
||||
// It captures HTTP traffic passing through the forward proxy and stores
|
||||
// entries for viewing in the UI.
|
||||
//
|
||||
// The logger supports:
|
||||
// - Request and response capture with headers and bodies
|
||||
// - Configurable body size limits to prevent memory issues
|
||||
// - Callback-based notifications for real-time log viewing
|
||||
// - Thread-safe operation for concurrent forwards
|
||||
//
|
||||
// Bodies are truncated if they exceed the configured maximum size
|
||||
// (default: 1MB) and marked as truncated in the log entry.
|
||||
package httplog
|
||||
|
||||
import (
|
||||
@@ -11,17 +23,17 @@ import (
|
||||
// Entry represents a single HTTP log entry
|
||||
type Entry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
ForwardID string `json:"forward_id"`
|
||||
RequestID string `json:"request_id"`
|
||||
Direction string `json:"direction"` // "request" or "response"
|
||||
Direction string `json:"direction"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
StatusCode int `json:"status_code,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
BodySize int `json:"body_size"`
|
||||
Body string `json:"body,omitempty"`
|
||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
StatusCode int `json:"status_code,omitempty"`
|
||||
BodySize int `json:"body_size"`
|
||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||
}
|
||||
|
||||
// LogCallback is a function that receives log entries
|
||||
@@ -29,12 +41,12 @@ type LogCallback func(entry Entry)
|
||||
|
||||
// Logger writes HTTP log entries to an output stream
|
||||
type Logger struct {
|
||||
mu sync.Mutex
|
||||
output io.Writer
|
||||
file *os.File // Only set if we opened the file ourselves
|
||||
file *os.File
|
||||
forwardID string
|
||||
maxBodyLen int
|
||||
callbacks []LogCallback
|
||||
maxBodyLen int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewLogger creates a new HTTP logger
|
||||
|
||||
@@ -166,15 +166,15 @@ func TestLogger_Log_Error(t *testing.T) {
|
||||
func TestLogger_BodyTruncation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
maxBodyLen int
|
||||
body string
|
||||
maxBodyLen int
|
||||
expectTrunc bool
|
||||
}{
|
||||
{"body under limit", 100, "short", false},
|
||||
{"body at limit", 5, "exact", false},
|
||||
{"body over limit", 5, "this is too long", true},
|
||||
{"empty body", 100, "", false},
|
||||
{"zero max", 0, "any", true},
|
||||
{name: "body under limit", maxBodyLen: 100, body: "short", expectTrunc: false},
|
||||
{name: "body at limit", maxBodyLen: 5, body: "exact", expectTrunc: false},
|
||||
{name: "body over limit", maxBodyLen: 5, body: "this is too long", expectTrunc: true},
|
||||
{name: "empty body", maxBodyLen: 100, body: "", expectTrunc: false},
|
||||
{name: "zero max", maxBodyLen: 0, body: "any", expectTrunc: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -186,10 +186,10 @@ func TestLogger_BodyTruncation(t *testing.T) {
|
||||
output: &buf,
|
||||
}
|
||||
|
||||
l.Log(Entry{Body: tt.body})
|
||||
_ = l.Log(Entry{Body: tt.body})
|
||||
|
||||
var entry Entry
|
||||
json.Unmarshal(buf.Bytes(), &entry)
|
||||
_ = json.Unmarshal(buf.Bytes(), &entry)
|
||||
|
||||
if tt.expectTrunc {
|
||||
assert.Contains(t, entry.Body, "...(truncated)")
|
||||
@@ -219,9 +219,9 @@ func TestLogger_Callbacks(t *testing.T) {
|
||||
})
|
||||
|
||||
// Log entries
|
||||
l.Log(Entry{Direction: "request", Path: "/api/1"})
|
||||
l.Log(Entry{Direction: "response", Path: "/api/1"})
|
||||
l.Log(Entry{Direction: "request", Path: "/api/2"})
|
||||
_ = l.Log(Entry{Direction: "request", Path: "/api/1"})
|
||||
_ = l.Log(Entry{Direction: "response", Path: "/api/1"})
|
||||
_ = l.Log(Entry{Direction: "request", Path: "/api/2"})
|
||||
|
||||
mu.Lock()
|
||||
assert.Len(t, received, 3)
|
||||
@@ -244,7 +244,7 @@ func TestLogger_MultipleCallbacks(t *testing.T) {
|
||||
l.AddCallback(func(entry Entry) { count1++ })
|
||||
l.AddCallback(func(entry Entry) { count2++ })
|
||||
|
||||
l.Log(Entry{})
|
||||
_ = l.Log(Entry{})
|
||||
|
||||
assert.Equal(t, 1, count1)
|
||||
assert.Equal(t, 1, count2)
|
||||
@@ -261,12 +261,12 @@ func TestLogger_ClearCallbacks(t *testing.T) {
|
||||
count := 0
|
||||
l.AddCallback(func(entry Entry) { count++ })
|
||||
|
||||
l.Log(Entry{})
|
||||
_ = l.Log(Entry{})
|
||||
assert.Equal(t, 1, count)
|
||||
|
||||
l.ClearCallbacks()
|
||||
|
||||
l.Log(Entry{})
|
||||
_ = l.Log(Entry{})
|
||||
assert.Equal(t, 1, count) // Still 1 - callback was cleared
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ func TestLogger_Concurrent(t *testing.T) {
|
||||
wg.Add(1)
|
||||
go func(n int) {
|
||||
defer wg.Done()
|
||||
l.Log(Entry{
|
||||
_ = l.Log(Entry{
|
||||
Direction: "request",
|
||||
Path: "/api/" + string(rune('a'+n%26)),
|
||||
})
|
||||
|
||||
@@ -15,20 +15,21 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/nvm/kportal/internal/config"
|
||||
"github.com/nvm/kportal/internal/logger"
|
||||
)
|
||||
|
||||
// Proxy is an HTTP reverse proxy with logging capabilities
|
||||
type Proxy struct {
|
||||
localPort int // Port to listen on (user-facing)
|
||||
targetPort int // Port to forward to (k8s tunnel)
|
||||
listener net.Listener
|
||||
logger *Logger
|
||||
server *http.Server
|
||||
forwardID string
|
||||
filterPath string // Glob pattern for path filtering
|
||||
includeHdrs bool
|
||||
listener net.Listener
|
||||
filterPath string
|
||||
localPort int
|
||||
targetPort int
|
||||
requestCount uint64
|
||||
mu sync.Mutex
|
||||
includeHdrs bool
|
||||
running bool
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ func (p *Proxy) Start() error {
|
||||
// Start serving (blocking)
|
||||
go func() {
|
||||
if err := p.server.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
// Log error but don't crash - proxy will be replaced on reconnect
|
||||
logger.Debug("HTTP proxy serve error (will be replaced on reconnect)", map[string]any{"error": err.Error()})
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -331,7 +331,7 @@ func TestProxy_Start_PortInUse(t *testing.T) {
|
||||
}
|
||||
err := proxy1.Start()
|
||||
require.NoError(t, err)
|
||||
defer proxy1.Stop()
|
||||
defer func() { _ = proxy1.Stop() }()
|
||||
|
||||
// Get the actual port
|
||||
addr := proxy1.listener.Addr().(*net.TCPAddr)
|
||||
@@ -353,9 +353,9 @@ func TestProxy_Start_PortInUse(t *testing.T) {
|
||||
// TestFlattenHeaders_EdgeCases tests header flattening edge cases
|
||||
func TestFlattenHeaders_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
headers http.Header
|
||||
expected map[string]string
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "empty headers",
|
||||
|
||||
Reference in New Issue
Block a user