Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 9, 2025

Adds opt-in logging capability to record HTTP request/response details for debugging API interactions. Disabled by default.

Changes

Config

  • Added log_file field to Config struct (optional, omitempty)

Logging implementation (config.go)

  • logToFile() - writes timestamped entries, fails silently on errors
  • LogRequest() - logs HTTP method and URL
  • LogResponse() - logs status code and URL

Integration (beeminder.go)

  • Added logging to all 9 HTTP operations (FetchGoals, CreateDatapoint, etc.)

Tests (config_test.go)

  • Coverage for enabled/disabled states, file writing, appending behavior
  • Uses t.TempDir() for cross-platform compatibility

Usage

Edit ~/.buzzrc:

{
  "username": "alice",
  "auth_token": "abc123",
  "log_file": "/var/log/buzz.log"
}

Output format:

[2025-12-09 12:34:56] REQUEST: GET https://www.beeminder.com/api/v1/users/alice/goals.json
[2025-12-09 12:34:57] RESPONSE: 200 https://www.beeminder.com/api/v1/users/alice/goals.json
Original prompt

This section details on the original issue you should resolve

<issue_title>Add logging option</issue_title>
<issue_description>## Objective
Add a logging option to configure buzz to save request and response details and other logs to a log file.

Implementation Plan

Design Decision

Logging will be opt-in (disabled by default) to respect user preferences and avoid cluttering their filesystem.

Approach

1. Add logging configuration field to Config struct

Add a LogFile field to the Config struct in config.go:

type Config struct {
	Username  string `json:"username"`
	AuthToken string `json:"auth_token"`
	BaseURL   string `json:"base_url,omitempty"`
	LogFile   string `json:"log_file,omitempty"` // Optional path to log file
}

2. Implement logging functionality

Check if LogFile is set before writing logs:

func logRequest(config *Config, method, url string, body []byte) {
	if config.LogFile == "" {
		return // Logging disabled
	}
	f, err := os.OpenFile(config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return // Fail silently if can't open log
	}
	defer f.Close()
	
	// Write log entry with timestamp, method, URL, etc.
}

3. User enablement

Users enable logging by editing their ~/.buzzrc file:

{
  "username": "their_username",
  "auth_token": "their_token",
  "log_file": "/path/to/buzz.log"
}

By default (when log_file is not set), logging is disabled.

Benefits of this approach

  • ✅ Opt-in by default - respects user choice
  • ✅ Flexible - users choose log location
  • ✅ Simple - empty string naturally means "disabled"
  • ✅ No breaking changes to existing configs</issue_description>

Comments on the Issue (you are @copilot in this section)

@narthur @coderabbitai I think this shouldn't be on by default. How would I allow the user to enable and disable this feature? @narthur @coderabbitai Let's update this issue's description with our implementation plan

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI changed the title [WIP] Add logging option for request and response details feat: add optional logging for HTTP requests and responses Dec 9, 2025
Copilot AI requested a review from narthur December 9, 2025 13:05
@narthur narthur marked this pull request as ready for review December 9, 2025 13:08
Copilot AI review requested due to automatic review settings December 9, 2025 13:08
@github-actions
Copy link

github-actions bot commented Dec 9, 2025

🚀 Pre-release Build Ready

Test builds are ready! Install directly using the bin command:

# Install the pre-release
bin install https://github.com/PinePeakDigital/buzz/releases/tag/pr-191-latest buzz-pr-191
# Run the pre-release
buzz-pr-191
# Uninstall the pre-release
bin remove buzz-pr-191

Direct Download Links

Or download binaries directly from the pre-release page:

💡 No GitHub login required for downloads!

🗑️ This pre-release will be automatically deleted when the PR is closed.

@github-actions
Copy link

github-actions bot commented Dec 9, 2025

🔨 Building...

Creating cross-platform binaries for PR #191. This may take a few minutes.

  • Commit: 93c004d
  • Started: 2025-12-09T13:08:35.397Z

This comment will be updated when the build completes.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds optional HTTP request/response logging functionality to help users debug API interactions with Beeminder. The feature is opt-in and disabled by default, requiring users to explicitly configure a log file path in their ~/.buzzrc configuration.

Key changes:

  • Added log_file configuration field to enable optional logging
  • Implemented logging functions that write timestamped HTTP request/response details
  • Integrated logging calls into all 9 HTTP operations in the Beeminder API client

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.

File Description
config.go Added LogFile field to Config struct and implemented logToFile(), LogRequest(), and LogResponse() functions for opt-in HTTP logging
beeminder.go Integrated logging calls before/after all HTTP operations (FetchGoals, CreateDatapoint, CreateCharge, FetchGoal, FetchGoalWithDatapoints, FetchGoalRawJSON, CreateGoal, RefreshGoal, GetLastDatapointValue)
config_test.go Added comprehensive tests for logging functionality covering enabled/disabled states, file operations, and JSON marshaling of log_file field
README.md Added documentation section explaining the optional logging feature, configuration instructions, and example log output format
Comments suppressed due to low confidence (1)

README.md:90

  • Documentation inconsistency: The example shows auth tokens as auth_token=..., implying they are redacted in logs. However, the actual implementation logs complete URLs including full auth tokens in plain text.

This documentation should either:

  1. Accurately show that full tokens are logged (if that's the intended behavior), OR
  2. Be updated after implementing URL sanitization to redact sensitive tokens (recommended)

Given the security implications, consider updating the documentation to warn users that sensitive credentials will be stored in log files:

**Security Warning**: Log files will contain your Beeminder auth token in plain text. Ensure log files are stored securely with appropriate file permissions and are not shared or committed to version control.
[2025-12-09 12:34:56] REQUEST: GET https://www.beeminder.com/api/v1/users/alice/goals.json?auth_token=...
[2025-12-09 12:34:57] RESPONSE: 200 https://www.beeminder.com/api/v1/users/alice/goals.json?auth_token=...
</details>

t.Run("LogRequest writes to file when LogFile is set", func(t *testing.T) {
// Create a temp directory for testing
tmpDir := t.TempDir()
logFile := filepath.Join(tmpDir, "buzz_test_log.txt")
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Inconsistent indentation: This line has extra indentation (two tabs instead of one). Should align with the previous line tmpDir := t.TempDir().

Suggested change
logFile := filepath.Join(tmpDir, "buzz_test_log.txt")
logFile := filepath.Join(tmpDir, "buzz_test_log.txt")

Copilot uses AI. Check for mistakes.
t.Run("LogResponse writes to file when LogFile is set", func(t *testing.T) {
// Create a temp directory for testing
tmpDir := t.TempDir()
logFile := filepath.Join(tmpDir, "buzz_test_log_response.txt")
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Inconsistent indentation: This line has extra indentation (two tabs instead of one). Should align with the previous line tmpDir := t.TempDir().

Suggested change
logFile := filepath.Join(tmpDir, "buzz_test_log_response.txt")
logFile := filepath.Join(tmpDir, "buzz_test_log_response.txt")

Copilot uses AI. Check for mistakes.
t.Run("Multiple log entries are appended", func(t *testing.T) {
// Create a temp directory for testing
tmpDir := t.TempDir()
logFile := filepath.Join(tmpDir, "buzz_test_log_multiple.txt")
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Inconsistent indentation: This line has extra indentation (two tabs instead of one). Should align with the previous line tmpDir := t.TempDir().

Suggested change
logFile := filepath.Join(tmpDir, "buzz_test_log_multiple.txt")
logFile := filepath.Join(tmpDir, "buzz_test_log_multiple.txt")

Copilot uses AI. Check for mistakes.
return // Logging disabled
}

f, err := os.OpenFile(config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Security Issue: Log file is created with 0644 permissions, making it readable by all users on the system. Given that logs will contain sensitive auth tokens (see related security comments), this is a security vulnerability.

Recommendation: Use more restrictive permissions similar to the config file (0600):

f, err := os.OpenFile(config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)

This ensures only the file owner can read the log file.

Suggested change
f, err := os.OpenFile(config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
f, err := os.OpenFile(config.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +353
func TestLoggingFunctionality(t *testing.T) {
t.Run("LogRequest does nothing when LogFile is empty", func(t *testing.T) {
config := &Config{
Username: "test",
AuthToken: "token",
LogFile: "", // Empty means disabled
}
// Should not panic or error
LogRequest(config, "GET", "http://example.com")
LogResponse(config, 200, "http://example.com")
})

t.Run("LogRequest does nothing when config is nil", func(t *testing.T) {
// Should not panic or error
LogRequest(nil, "GET", "http://example.com")
LogResponse(nil, 200, "http://example.com")
})

t.Run("LogRequest writes to file when LogFile is set", func(t *testing.T) {
// Create a temp directory for testing
tmpDir := t.TempDir()
logFile := filepath.Join(tmpDir, "buzz_test_log.txt")

config := &Config{
Username: "test",
AuthToken: "token",
LogFile: logFile,
}

// Log a request
LogRequest(config, "GET", "http://example.com/api")

// Verify file exists
if _, err := os.Stat(logFile); os.IsNotExist(err) {
t.Error("Log file should exist after LogRequest")
}

// Read and verify content
data, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}

content := string(data)
if !strings.Contains(content, "REQUEST: GET http://example.com/api") {
t.Errorf("Log content should contain request details, got: %s", content)
}
if !strings.Contains(content, "[20") { // Check for timestamp format [20XX-XX-XX ...]
t.Errorf("Log content should contain timestamp, got: %s", content)
}
})

t.Run("LogResponse writes to file when LogFile is set", func(t *testing.T) {
// Create a temp directory for testing
tmpDir := t.TempDir()
logFile := filepath.Join(tmpDir, "buzz_test_log_response.txt")

config := &Config{
Username: "test",
AuthToken: "token",
LogFile: logFile,
}

// Log a response
LogResponse(config, 200, "http://example.com/api")

// Verify file exists
if _, err := os.Stat(logFile); os.IsNotExist(err) {
t.Error("Log file should exist after LogResponse")
}

// Read and verify content
data, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}

content := string(data)
if !strings.Contains(content, "RESPONSE: 200 http://example.com/api") {
t.Errorf("Log content should contain response details, got: %s", content)
}
})

t.Run("Multiple log entries are appended", func(t *testing.T) {
// Create a temp directory for testing
tmpDir := t.TempDir()
logFile := filepath.Join(tmpDir, "buzz_test_log_multiple.txt")

config := &Config{
Username: "test",
AuthToken: "token",
LogFile: logFile,
}

// Log multiple entries
LogRequest(config, "GET", "http://example.com/api/1")
LogResponse(config, 200, "http://example.com/api/1")
LogRequest(config, "POST", "http://example.com/api/2")
LogResponse(config, 201, "http://example.com/api/2")

// Read and verify content
data, err := os.ReadFile(logFile)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}

content := string(data)
if !strings.Contains(content, "REQUEST: GET http://example.com/api/1") {
t.Error("Log should contain first request")
}
if !strings.Contains(content, "RESPONSE: 200 http://example.com/api/1") {
t.Error("Log should contain first response")
}
if !strings.Contains(content, "REQUEST: POST http://example.com/api/2") {
t.Error("Log should contain second request")
}
if !strings.Contains(content, "RESPONSE: 201 http://example.com/api/2") {
t.Error("Log should contain second response")
}
})
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Missing test coverage for URL sanitization: The tests verify that logging works when enabled/disabled and that log content is written correctly, but there's no test to verify that sensitive data (auth tokens) are properly handled in logs.

Recommendation: Add a test case to verify auth token sanitization:

t.Run("LogRequest sanitizes auth_token from URL", func(t *testing.T) {
    tmpDir := t.TempDir()
    logFile := filepath.Join(tmpDir, "buzz_test_sanitized.txt")
    
    config := &Config{
        Username:  "test",
        AuthToken: "secret_token_123",
        LogFile:   logFile,
    }
    
    // Log a request with auth_token in URL
    LogRequest(config, "GET", "https://api.example.com/goals?auth_token=secret_token_123")
    
    // Verify token is redacted
    data, _ := os.ReadFile(logFile)
    content := string(data)
    if strings.Contains(content, "secret_token_123") {
        t.Error("Log should not contain actual auth token")
    }
    if !strings.Contains(content, "[REDACTED]") || !strings.Contains(content, "auth_token=") {
        t.Error("Log should show auth_token as redacted")
    }
})

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +165
// LogRequest logs HTTP request details to the configured log file
func LogRequest(config *Config, method, url string) {
logToFile(config, fmt.Sprintf("REQUEST: %s %s", method, url))
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Critical Security Issue: The logging implementation logs complete URLs including authentication tokens. In Beeminder API calls, auth tokens are passed as query parameters (e.g., ?auth_token=abc123), which means sensitive credentials will be written to the log file in plain text.

Risk: If the log file is compromised, attackers gain access to the user's Beeminder account credentials.

Recommendation: Before logging URLs, sanitize them to redact sensitive query parameters:

func sanitizeURL(urlStr string) string {
    u, err := url.Parse(urlStr)
    if err != nil {
        return urlStr
    }
    q := u.Query()
    if q.Get("auth_token") != "" {
        q.Set("auth_token", "[REDACTED]")
        u.RawQuery = q.Encode()
    }
    return u.String()
}

// LogRequest logs HTTP request details to the configured log file
func LogRequest(config *Config, method, url string) {
    logToFile(config, fmt.Sprintf("REQUEST: %s %s", method, sanitizeURL(url)))
}

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +170
// LogResponse logs HTTP response details to the configured log file
func LogResponse(config *Config, statusCode int, url string) {
logToFile(config, fmt.Sprintf("RESPONSE: %d %s", statusCode, url))
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Critical Security Issue: The LogResponse function also logs URLs containing authentication tokens. When logging responses, the same URLs with auth_token query parameters are exposed.

Recommendation: Apply the same URL sanitization as suggested for LogRequest to prevent auth token exposure.

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +159
defer f.Close()

timestamp := time.Now().Format("2006-01-02 15:04:05")
logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
// Intentionally ignore write errors to fail silently and not disrupt normal operations
f.WriteString(logEntry)
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

File handle may be writable as a result of data flow from a call to OpenFile and closing it may result in data loss upon failure, which is not handled explicitly.

Suggested change
defer f.Close()
timestamp := time.Now().Format("2006-01-02 15:04:05")
logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
// Intentionally ignore write errors to fail silently and not disrupt normal operations
f.WriteString(logEntry)
// Do not defer f.Close(); instead, close explicitly after writing and check error
timestamp := time.Now().Format("2006-01-02 15:04:05")
logEntry := fmt.Sprintf("[%s] %s\n", timestamp, message)
// Intentionally ignore write errors to fail silently and not disrupt normal operations
f.WriteString(logEntry)
// Explicitly close the file and ignore the error to match fail-silently policy
_ = f.Close()

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +90
When logging is enabled, buzz will append timestamped entries for each HTTP request and response to the specified log file:

```
[2025-12-09 12:34:56] REQUEST: GET https://www.beeminder.com/api/v1/users/alice/goals.json?auth_token=...
[2025-12-09 12:34:57] RESPONSE: 200 https://www.beeminder.com/api/v1/users/alice/goals.json?auth_token=...
```
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

These examples show request/response logs including the full Beeminder URL with the auth_token query parameter (?auth_token=...), which means the logging feature records API tokens in a plaintext log file. Anyone with access to the log (including other local users if the file is world-readable or systems ingesting the logs) can recover this token and impersonate the user against the Beeminder API. To avoid leaking credentials, ensure the implementation strips auth_token (and other secrets) from URLs before logging and recommend/ enforce restrictive permissions (e.g., 0600) on the log file path.

Copilot uses AI. Check for mistakes.
@narthur narthur merged commit 776aad4 into main Dec 9, 2025
2 checks passed
@narthur narthur deleted the copilot/add-logging-option branch December 9, 2025 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add logging option

2 participants