mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
* chore: apply go fix modernizers from Go 1.26
automated refactoring: interface{} to any, slices.Contains,
and other idiomatic updates.
* feat(ci): add `go fix` check to Go analysis workflow
ensures Go 1.26 modernizers are applied, fails CI if `go fix ./...`
produces any changes (similar to existing `go fmt` enforcement)
827 lines
28 KiB
Go
827 lines
28 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ipfs/kubo/test/cli/harness"
|
|
. "github.com/ipfs/kubo/test/cli/testutils"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestLogLevel(t *testing.T) {
|
|
|
|
t.Run("CLI", func(t *testing.T) {
|
|
t.Run("level '*' shows all subsystems", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
expectedSubsystems := getExpectedSubsystems(t, node)
|
|
|
|
res := node.IPFS("log", "level", "*")
|
|
assert.NoError(t, res.Err)
|
|
assert.Empty(t, res.Stderr.Lines())
|
|
|
|
actualSubsystems := parseCLIOutput(t, res.Stdout.String())
|
|
|
|
// Should show all subsystems plus the (default) entry
|
|
assert.GreaterOrEqual(t, len(actualSubsystems), len(expectedSubsystems))
|
|
|
|
validateAllSubsystemsPresentCLI(t, expectedSubsystems, actualSubsystems, "CLI output")
|
|
|
|
// Should have the (default) entry
|
|
_, hasDefault := actualSubsystems["(default)"]
|
|
assert.True(t, hasDefault, "Should have '(default)' entry")
|
|
})
|
|
|
|
t.Run("level 'all' shows all subsystems (alias for '*')", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
expectedSubsystems := getExpectedSubsystems(t, node)
|
|
|
|
res := node.IPFS("log", "level", "all")
|
|
assert.NoError(t, res.Err)
|
|
assert.Empty(t, res.Stderr.Lines())
|
|
|
|
actualSubsystems := parseCLIOutput(t, res.Stdout.String())
|
|
|
|
// Should show all subsystems plus the (default) entry
|
|
assert.GreaterOrEqual(t, len(actualSubsystems), len(expectedSubsystems))
|
|
|
|
validateAllSubsystemsPresentCLI(t, expectedSubsystems, actualSubsystems, "CLI output")
|
|
|
|
// Should have the (default) entry
|
|
_, hasDefault := actualSubsystems["(default)"]
|
|
assert.True(t, hasDefault, "Should have '(default)' entry")
|
|
})
|
|
|
|
t.Run("get level for specific subsystem", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
node.IPFS("log", "level", "core", "debug")
|
|
res := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res.Err)
|
|
assert.Empty(t, res.Stderr.Lines())
|
|
|
|
output := res.Stdout.String()
|
|
lines := SplitLines(output)
|
|
|
|
assert.Equal(t, 1, len(lines))
|
|
|
|
line := strings.TrimSpace(lines[0])
|
|
assert.Equal(t, "debug", line)
|
|
})
|
|
|
|
t.Run("get level with no args returns default level", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
res1 := node.IPFS("log", "level", "*", "fatal")
|
|
assert.NoError(t, res1.Err)
|
|
assert.Empty(t, res1.Stderr.Lines())
|
|
|
|
res := node.IPFS("log", "level")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, 0, len(res.Stderr.Lines()))
|
|
|
|
output := res.Stdout.String()
|
|
lines := SplitLines(output)
|
|
|
|
assert.Equal(t, 1, len(lines))
|
|
|
|
line := strings.TrimSpace(lines[0])
|
|
assert.Equal(t, "fatal", line)
|
|
})
|
|
|
|
t.Run("get level reflects runtime log level changes", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon("--offline")
|
|
defer node.StopDaemon()
|
|
|
|
node.IPFS("log", "level", "core", "debug")
|
|
res := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res.Err)
|
|
|
|
output := res.Stdout.String()
|
|
lines := SplitLines(output)
|
|
|
|
assert.Equal(t, 1, len(lines))
|
|
|
|
line := strings.TrimSpace(lines[0])
|
|
assert.Equal(t, "debug", line)
|
|
})
|
|
|
|
t.Run("get level with non-existent subsystem returns error", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
res := node.RunIPFS("log", "level", "non-existent-subsystem")
|
|
assert.Error(t, res.Err)
|
|
assert.NotEqual(t, 0, len(res.Stderr.Lines()))
|
|
})
|
|
|
|
t.Run("set level to 'default' keyword", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// First set a specific subsystem to a different level
|
|
res1 := node.IPFS("log", "level", "core", "debug")
|
|
assert.NoError(t, res1.Err)
|
|
assert.Contains(t, res1.Stdout.String(), "Changed log level of 'core' to 'debug'")
|
|
|
|
// Verify it was set to debug
|
|
res2 := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res2.Err)
|
|
assert.Equal(t, "debug", strings.TrimSpace(res2.Stdout.String()))
|
|
|
|
// Get the current default level (should be 'error' since unchanged)
|
|
res3 := node.IPFS("log", "level")
|
|
assert.NoError(t, res3.Err)
|
|
defaultLevel := strings.TrimSpace(res3.Stdout.String())
|
|
assert.Equal(t, "error", defaultLevel, "Default level should be 'error' when unchanged")
|
|
|
|
// Now set the subsystem back to default
|
|
res4 := node.IPFS("log", "level", "core", "default")
|
|
assert.NoError(t, res4.Err)
|
|
assert.Contains(t, res4.Stdout.String(), "Changed log level of 'core' to")
|
|
|
|
// Verify it's now at the default level (should be 'error')
|
|
res5 := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res5.Err)
|
|
assert.Equal(t, "error", strings.TrimSpace(res5.Stdout.String()))
|
|
})
|
|
|
|
t.Run("set all subsystems with 'all' changes default (alias for '*')", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Initial state - default should be 'error'
|
|
res := node.IPFS("log", "level")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Set one subsystem to a different level
|
|
res = node.IPFS("log", "level", "core", "debug")
|
|
assert.NoError(t, res.Err)
|
|
|
|
// Default should still be 'error'
|
|
res = node.IPFS("log", "level")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Now use 'all' to set everything to 'info'
|
|
res = node.IPFS("log", "level", "all", "info")
|
|
assert.NoError(t, res.Err)
|
|
assert.Contains(t, res.Stdout.String(), "Changed log level of '*' to 'info'")
|
|
|
|
// Default should now be 'info'
|
|
res = node.IPFS("log", "level")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Core should also be 'info' (overwritten by 'all')
|
|
res = node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Any other subsystem should also be 'info'
|
|
res = node.IPFS("log", "level", "dht")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
|
|
})
|
|
|
|
t.Run("set all subsystems with '*' changes default", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Initial state - default should be 'error'
|
|
res := node.IPFS("log", "level")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Set one subsystem to a different level
|
|
res = node.IPFS("log", "level", "core", "debug")
|
|
assert.NoError(t, res.Err)
|
|
|
|
// Default should still be 'error'
|
|
res = node.IPFS("log", "level")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Now use '*' to set everything to 'info'
|
|
res = node.IPFS("log", "level", "*", "info")
|
|
assert.NoError(t, res.Err)
|
|
assert.Contains(t, res.Stdout.String(), "Changed log level of '*' to 'info'")
|
|
|
|
// Default should now be 'info'
|
|
res = node.IPFS("log", "level")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Core should also be 'info' (overwritten by '*')
|
|
res = node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
|
|
|
|
// Any other subsystem should also be 'info'
|
|
res = node.IPFS("log", "level", "dht")
|
|
assert.NoError(t, res.Err)
|
|
assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
|
|
})
|
|
|
|
t.Run("'all' in get mode shows (default) entry (alias for '*')", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Get all levels with 'all'
|
|
res := node.IPFS("log", "level", "all")
|
|
assert.NoError(t, res.Err)
|
|
|
|
output := res.Stdout.String()
|
|
|
|
// Should contain "(default): error" entry
|
|
assert.Contains(t, output, "(default): error", "Should show default level with (default) key")
|
|
|
|
// Should also contain various subsystems
|
|
assert.Contains(t, output, "core: error")
|
|
assert.Contains(t, output, "dht: error")
|
|
})
|
|
|
|
t.Run("'*' in get mode shows (default) entry", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Get all levels with '*'
|
|
res := node.IPFS("log", "level", "*")
|
|
assert.NoError(t, res.Err)
|
|
|
|
output := res.Stdout.String()
|
|
|
|
// Should contain "(default): error" entry
|
|
assert.Contains(t, output, "(default): error", "Should show default level with (default) key")
|
|
|
|
// Should also contain various subsystems
|
|
assert.Contains(t, output, "core: error")
|
|
assert.Contains(t, output, "dht: error")
|
|
})
|
|
|
|
t.Run("set all subsystems to 'default' using 'all' (alias for '*')", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Get the original default level (just for reference, it should be "error")
|
|
res0 := node.IPFS("log", "level")
|
|
assert.NoError(t, res0.Err)
|
|
assert.Equal(t, "error", strings.TrimSpace(res0.Stdout.String()))
|
|
|
|
// First set all subsystems to debug using 'all'
|
|
res1 := node.IPFS("log", "level", "all", "debug")
|
|
assert.NoError(t, res1.Err)
|
|
assert.Contains(t, res1.Stdout.String(), "Changed log level of '*' to 'debug'")
|
|
|
|
// Verify a specific subsystem is at debug
|
|
res2 := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res2.Err)
|
|
assert.Equal(t, "debug", strings.TrimSpace(res2.Stdout.String()))
|
|
|
|
// Verify the default level is now debug
|
|
res3 := node.IPFS("log", "level")
|
|
assert.NoError(t, res3.Err)
|
|
assert.Equal(t, "debug", strings.TrimSpace(res3.Stdout.String()))
|
|
|
|
// Now set all subsystems back to default (which is now "debug") using 'all'
|
|
res4 := node.IPFS("log", "level", "all", "default")
|
|
assert.NoError(t, res4.Err)
|
|
assert.Contains(t, res4.Stdout.String(), "Changed log level of '*' to")
|
|
|
|
// The subsystem should still be at debug (because that's what default is now)
|
|
res5 := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res5.Err)
|
|
assert.Equal(t, "debug", strings.TrimSpace(res5.Stdout.String()))
|
|
|
|
// The behavior is correct: "default" uses the current default level,
|
|
// which was changed to "debug" when we set "all" to "debug"
|
|
})
|
|
|
|
t.Run("set all subsystems to 'default' keyword", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Get the original default level (just for reference, it should be "error")
|
|
res0 := node.IPFS("log", "level")
|
|
assert.NoError(t, res0.Err)
|
|
// originalDefault := strings.TrimSpace(res0.Stdout.String())
|
|
assert.Equal(t, "error", strings.TrimSpace(res0.Stdout.String()))
|
|
|
|
// First set all subsystems to debug
|
|
res1 := node.IPFS("log", "level", "*", "debug")
|
|
assert.NoError(t, res1.Err)
|
|
assert.Contains(t, res1.Stdout.String(), "Changed log level of '*' to 'debug'")
|
|
|
|
// Verify a specific subsystem is at debug
|
|
res2 := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res2.Err)
|
|
assert.Equal(t, "debug", strings.TrimSpace(res2.Stdout.String()))
|
|
|
|
// Verify the default level is now debug
|
|
res3 := node.IPFS("log", "level")
|
|
assert.NoError(t, res3.Err)
|
|
assert.Equal(t, "debug", strings.TrimSpace(res3.Stdout.String()))
|
|
|
|
// Now set all subsystems back to default (which is now "debug")
|
|
res4 := node.IPFS("log", "level", "*", "default")
|
|
assert.NoError(t, res4.Err)
|
|
assert.Contains(t, res4.Stdout.String(), "Changed log level of '*' to")
|
|
|
|
// The subsystem should still be at debug (because that's what default is now)
|
|
res5 := node.IPFS("log", "level", "core")
|
|
assert.NoError(t, res5.Err)
|
|
assert.Equal(t, "debug", strings.TrimSpace(res5.Stdout.String()))
|
|
|
|
// The behavior is correct: "default" uses the current default level,
|
|
// which was changed to "debug" when we set "*" to "debug"
|
|
})
|
|
|
|
t.Run("shell escaping variants for '*' wildcard", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := harness.NewT(t)
|
|
node := h.NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Test different shell escaping methods work for '*'
|
|
// This tests the behavior documented in help text: '*' or "*" or \*
|
|
|
|
// Test 1: Single quotes '*' (should work)
|
|
cmd1 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level '*' info",
|
|
node.Dir, node.IPFSBin, node.APIAddr())
|
|
res1 := h.Sh(cmd1)
|
|
assert.NoError(t, res1.Err)
|
|
assert.Contains(t, res1.Stdout.String(), "Changed log level of '*' to 'info'")
|
|
|
|
// Test 2: Double quotes "*" (should work)
|
|
cmd2 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level \"*\" debug",
|
|
node.Dir, node.IPFSBin, node.APIAddr())
|
|
res2 := h.Sh(cmd2)
|
|
assert.NoError(t, res2.Err)
|
|
assert.Contains(t, res2.Stdout.String(), "Changed log level of '*' to 'debug'")
|
|
|
|
// Test 3: Backslash escape \* (should work)
|
|
cmd3 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level \\* warn",
|
|
node.Dir, node.IPFSBin, node.APIAddr())
|
|
res3 := h.Sh(cmd3)
|
|
assert.NoError(t, res3.Err)
|
|
assert.Contains(t, res3.Stdout.String(), "Changed log level of '*' to 'warn'")
|
|
|
|
// Test 4: Verify the final state - should show 'warn' as default
|
|
res4 := node.IPFS("log", "level")
|
|
assert.NoError(t, res4.Err)
|
|
assert.Equal(t, "warn", strings.TrimSpace(res4.Stdout.String()))
|
|
|
|
// Test 5: Get all levels using escaped '*' to verify it shows all subsystems
|
|
cmd5 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level \\*",
|
|
node.Dir, node.IPFSBin, node.APIAddr())
|
|
res5 := h.Sh(cmd5)
|
|
assert.NoError(t, res5.Err)
|
|
output := res5.Stdout.String()
|
|
assert.Contains(t, output, "(default): warn", "Should show updated default level")
|
|
assert.Contains(t, output, "core: warn", "Should show core subsystem at warn level")
|
|
})
|
|
})
|
|
|
|
t.Run("HTTP RPC", func(t *testing.T) {
|
|
t.Run("get default level returns JSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Make HTTP request to get default log level
|
|
resp, err := http.Post(node.APIURL()+"/api/v0/log/level", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Parse JSON response
|
|
var result map[string]any
|
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
require.NoError(t, err)
|
|
|
|
// Check that we have the Levels field
|
|
levels, ok := result["Levels"].(map[string]any)
|
|
require.True(t, ok, "Response should have 'Levels' field")
|
|
|
|
// Should have exactly one entry for the default level
|
|
assert.Equal(t, 1, len(levels))
|
|
|
|
// The default level should be present
|
|
defaultLevel, ok := levels[""]
|
|
require.True(t, ok, "Should have empty string key for default level")
|
|
assert.Equal(t, "error", defaultLevel, "Default level should be 'error'")
|
|
})
|
|
|
|
t.Run("get all levels using 'all' returns JSON (alias for '*')", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
expectedSubsystems := getExpectedSubsystems(t, node)
|
|
|
|
// Make HTTP request to get all log levels using 'all'
|
|
resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=all", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
levels := parseHTTPResponse(t, resp)
|
|
validateAllSubsystemsPresent(t, expectedSubsystems, levels, "JSON response")
|
|
|
|
// Should have the (default) entry
|
|
defaultLevel, ok := levels["(default)"]
|
|
require.True(t, ok, "Should have '(default)' key")
|
|
assert.Equal(t, "error", defaultLevel, "Default level should be 'error'")
|
|
})
|
|
|
|
t.Run("get all levels returns JSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
expectedSubsystems := getExpectedSubsystems(t, node)
|
|
|
|
// Make HTTP request to get all log levels
|
|
resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=*", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
levels := parseHTTPResponse(t, resp)
|
|
validateAllSubsystemsPresent(t, expectedSubsystems, levels, "JSON response")
|
|
|
|
// Should have the (default) entry
|
|
defaultLevel, ok := levels["(default)"]
|
|
require.True(t, ok, "Should have '(default)' key")
|
|
assert.Equal(t, "error", defaultLevel, "Default level should be 'error'")
|
|
})
|
|
|
|
t.Run("get specific subsystem level returns JSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// First set a specific level for a subsystem
|
|
resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=debug", "", nil)
|
|
require.NoError(t, err)
|
|
resp.Body.Close()
|
|
|
|
// Now get the level for that subsystem
|
|
resp, err = http.Post(node.APIURL()+"/api/v0/log/level?arg=core", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Parse JSON response
|
|
var result map[string]any
|
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
require.NoError(t, err)
|
|
|
|
// Check that we have the Levels field
|
|
levels, ok := result["Levels"].(map[string]any)
|
|
require.True(t, ok, "Response should have 'Levels' field")
|
|
|
|
// Should have exactly one entry
|
|
assert.Equal(t, 1, len(levels))
|
|
|
|
// Check the level for 'core' subsystem
|
|
coreLevel, ok := levels["core"]
|
|
require.True(t, ok, "Should have 'core' key")
|
|
assert.Equal(t, "debug", coreLevel, "Core level should be 'debug'")
|
|
})
|
|
|
|
t.Run("set level using 'all' returns JSON message (alias for '*')", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Set a log level using 'all'
|
|
resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=all&arg=info", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Parse JSON response
|
|
var result map[string]any
|
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
require.NoError(t, err)
|
|
|
|
// Check that we have the Message field
|
|
message, ok := result["Message"].(string)
|
|
require.True(t, ok, "Response should have 'Message' field")
|
|
|
|
// Check the message content (should show '*' in message even when 'all' was used)
|
|
assert.Contains(t, message, "Changed log level of '*' to 'info'")
|
|
})
|
|
|
|
t.Run("set level returns JSON message", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// Set a log level
|
|
resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=info", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Parse JSON response
|
|
var result map[string]any
|
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
require.NoError(t, err)
|
|
|
|
// Check that we have the Message field
|
|
message, ok := result["Message"].(string)
|
|
require.True(t, ok, "Response should have 'Message' field")
|
|
|
|
// Check the message content
|
|
assert.Contains(t, message, "Changed log level of 'core' to 'info'")
|
|
})
|
|
|
|
t.Run("set level to 'default' keyword", func(t *testing.T) {
|
|
t.Parallel()
|
|
node := harness.NewT(t).NewNode().Init().StartDaemon()
|
|
defer node.StopDaemon()
|
|
|
|
// First set a subsystem to debug
|
|
resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=debug", "", nil)
|
|
require.NoError(t, err)
|
|
resp.Body.Close()
|
|
|
|
// Now set it back to default
|
|
resp, err = http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=default", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Parse JSON response
|
|
var result map[string]any
|
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
require.NoError(t, err)
|
|
|
|
// Check that we have the Message field
|
|
message, ok := result["Message"].(string)
|
|
require.True(t, ok, "Response should have 'Message' field")
|
|
|
|
// The message should indicate the change
|
|
assert.True(t, strings.Contains(message, "Changed log level of 'core' to"),
|
|
"Message should indicate level change")
|
|
|
|
// Verify the level is back to error (default)
|
|
resp, err = http.Post(node.APIURL()+"/api/v0/log/level?arg=core", "", nil)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
var getResult map[string]any
|
|
err = json.NewDecoder(resp.Body).Decode(&getResult)
|
|
require.NoError(t, err)
|
|
|
|
levels, _ := getResult["Levels"].(map[string]any)
|
|
coreLevel, _ := levels["core"].(string)
|
|
assert.Equal(t, "error", coreLevel, "Core level should be back to 'error' (default)")
|
|
})
|
|
})
|
|
|
|
// Constants for slog interop tests
|
|
const (
|
|
slogTestLogTailTimeout = 10 * time.Second
|
|
slogTestLogWaitTimeout = 5 * time.Second
|
|
slogTestLogStartupDelay = 1 * time.Second // Wait for log tail to start
|
|
slogTestSubsystemCmdsHTTP = "cmds/http" // Native go-log subsystem
|
|
slogTestSubsystemNetIdentify = "net/identify" // go-libp2p slog subsystem
|
|
)
|
|
|
|
// logMatch represents a matched log entry for slog interop tests
|
|
type logMatch struct {
|
|
subsystem string
|
|
line string
|
|
}
|
|
|
|
// startLogMonitoring starts ipfs log tail and returns command and channel for matched logs.
|
|
startLogMonitoring := func(t *testing.T, node *harness.Node) (*exec.Cmd, chan logMatch) {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), slogTestLogTailTimeout)
|
|
t.Cleanup(cancel)
|
|
|
|
cmd := exec.CommandContext(ctx, node.IPFSBin, "log", "tail")
|
|
cmd.Env = append([]string(nil), os.Environ()...)
|
|
for k, v := range node.Runner.Env {
|
|
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
cmd.Dir = node.Runner.Dir
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
require.NoError(t, err)
|
|
require.NoError(t, cmd.Start())
|
|
|
|
matches := make(chan logMatch, 10)
|
|
|
|
go func() {
|
|
scanner := bufio.NewScanner(stdout)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
// Check for actual logger field in JSON, not just substring match
|
|
if strings.Contains(line, `"logger":"cmds/http"`) {
|
|
matches <- logMatch{slogTestSubsystemCmdsHTTP, line}
|
|
}
|
|
if strings.Contains(line, `"logger":"net/identify"`) {
|
|
matches <- logMatch{slogTestSubsystemNetIdentify, line}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return cmd, matches
|
|
}
|
|
|
|
// waitForBothSubsystems waits for both native go-log and slog subsystems to appear in logs.
|
|
waitForBothSubsystems := func(t *testing.T, matches chan logMatch, timeout time.Duration) {
|
|
t.Helper()
|
|
|
|
seen := make(map[string]struct{})
|
|
deadline := time.After(timeout)
|
|
|
|
for len(seen) < 2 {
|
|
select {
|
|
case match := <-matches:
|
|
if _, exists := seen[match.subsystem]; !exists {
|
|
t.Logf("Found %s log", match.subsystem)
|
|
seen[match.subsystem] = struct{}{}
|
|
}
|
|
case <-deadline:
|
|
t.Fatalf("Timeout waiting for logs. Seen: %v", seen)
|
|
}
|
|
}
|
|
|
|
assert.Contains(t, seen, slogTestSubsystemCmdsHTTP, "should see cmds/http (native go-log)")
|
|
assert.Contains(t, seen, slogTestSubsystemNetIdentify, "should see net/identify (slog from go-libp2p)")
|
|
}
|
|
|
|
// triggerIdentifyProtocol connects node1 to node2, triggering net/identify logs.
|
|
triggerIdentifyProtocol := func(t *testing.T, node1, node2 *harness.Node) {
|
|
t.Helper()
|
|
|
|
// Get node2's peer ID and address
|
|
node2ID := node2.PeerID().String()
|
|
addrsRes := node2.IPFS("id", "-f", "<addrs>")
|
|
require.NoError(t, addrsRes.Err)
|
|
|
|
addrs := strings.Split(strings.TrimSpace(addrsRes.Stdout.String()), "\n")
|
|
require.NotEmpty(t, addrs, "node2 should have at least one address")
|
|
|
|
// Connect node1 to node2
|
|
multiaddr := fmt.Sprintf("%s/p2p/%s", addrs[0], node2ID)
|
|
res := node1.IPFS("swarm", "connect", multiaddr)
|
|
require.NoError(t, res.Err)
|
|
}
|
|
|
|
// verifySlogInterop verifies that both native go-log and slog from go-libp2p
|
|
// appear in ipfs log tail with correct formatting and level control.
|
|
verifySlogInterop := func(t *testing.T, node1, node2 *harness.Node) {
|
|
t.Helper()
|
|
|
|
cmd, matches := startLogMonitoring(t, node1)
|
|
defer func() {
|
|
_ = cmd.Process.Kill()
|
|
}()
|
|
|
|
time.Sleep(slogTestLogStartupDelay)
|
|
|
|
// Trigger cmds/http (native go-log)
|
|
node1.IPFS("version")
|
|
|
|
// Trigger net/identify (slog from go-libp2p)
|
|
triggerIdentifyProtocol(t, node1, node2)
|
|
|
|
waitForBothSubsystems(t, matches, slogTestLogWaitTimeout)
|
|
}
|
|
|
|
// This test verifies that go-log's slog bridge works with go-libp2p's gologshim
|
|
// when log levels are set via GOLOG_LOG_LEVEL environment variable.
|
|
// It tests both native go-log loggers (cmds/http) and slog-based loggers from
|
|
// go-libp2p (net/identify), ensuring both types appear in `ipfs log tail`.
|
|
t.Run("slog interop via env var", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := harness.NewT(t)
|
|
|
|
node1 := h.NewNode().Init()
|
|
node1.Runner.Env["GOLOG_LOG_LEVEL"] = "error,cmds/http=debug,net/identify=debug"
|
|
node1.StartDaemon()
|
|
defer node1.StopDaemon()
|
|
|
|
node2 := h.NewNode().Init().StartDaemon()
|
|
defer node2.StopDaemon()
|
|
|
|
verifySlogInterop(t, node1, node2)
|
|
})
|
|
|
|
// This test verifies that go-log's slog bridge works with go-libp2p's gologshim
|
|
// when log levels are set dynamically via `ipfs log level` CLI commands.
|
|
// It tests the key feature that SetLogLevel auto-creates level entries for subsystems
|
|
// that don't exist yet, enabling `ipfs log level net/identify debug` to work even
|
|
// before the net/identify logger is created. This is critical for slog interop.
|
|
t.Run("slog interop via CLI", func(t *testing.T) {
|
|
t.Parallel()
|
|
h := harness.NewT(t)
|
|
|
|
node1 := h.NewNode().Init().StartDaemon()
|
|
defer node1.StopDaemon()
|
|
|
|
node2 := h.NewNode().Init().StartDaemon()
|
|
defer node2.StopDaemon()
|
|
|
|
// Set levels via CLI for both subsystems BEFORE triggering events
|
|
res := node1.IPFS("log", "level", slogTestSubsystemCmdsHTTP, "debug")
|
|
require.NoError(t, res.Err)
|
|
|
|
res = node1.IPFS("log", "level", slogTestSubsystemNetIdentify, "debug")
|
|
require.NoError(t, res.Err) // Auto-creates level entry for slog subsystem
|
|
|
|
verifySlogInterop(t, node1, node2)
|
|
})
|
|
|
|
}
|
|
|
|
func getExpectedSubsystems(t *testing.T, node *harness.Node) []string {
|
|
t.Helper()
|
|
lsRes := node.IPFS("log", "ls")
|
|
require.NoError(t, lsRes.Err)
|
|
expectedSubsystems := SplitLines(lsRes.Stdout.String())
|
|
assert.Greater(t, len(expectedSubsystems), 10, "Should have many subsystems")
|
|
return expectedSubsystems
|
|
}
|
|
|
|
func parseCLIOutput(t *testing.T, output string) map[string]string {
|
|
t.Helper()
|
|
lines := SplitLines(output)
|
|
actualSubsystems := make(map[string]string)
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
parts := strings.Split(line, ": ")
|
|
assert.Equal(t, 2, len(parts), "Line should have format 'subsystem: level', got: %s", line)
|
|
assert.NotEmpty(t, parts[0], "Subsystem should not be empty")
|
|
assert.NotEmpty(t, parts[1], "Level should not be empty")
|
|
actualSubsystems[parts[0]] = parts[1]
|
|
}
|
|
return actualSubsystems
|
|
}
|
|
|
|
func parseHTTPResponse(t *testing.T, resp *http.Response) map[string]any {
|
|
t.Helper()
|
|
var result map[string]any
|
|
err := json.NewDecoder(resp.Body).Decode(&result)
|
|
require.NoError(t, err)
|
|
levels, ok := result["Levels"].(map[string]any)
|
|
require.True(t, ok, "Response should have 'Levels' field")
|
|
assert.Greater(t, len(levels), 10, "Should have many subsystems")
|
|
return levels
|
|
}
|
|
|
|
func validateAllSubsystemsPresent(t *testing.T, expectedSubsystems []string, actualLevels map[string]any, context string) {
|
|
t.Helper()
|
|
for _, expectedSub := range expectedSubsystems {
|
|
expectedSub = strings.TrimSpace(expectedSub)
|
|
if expectedSub == "" {
|
|
continue
|
|
}
|
|
_, found := actualLevels[expectedSub]
|
|
assert.True(t, found, "Expected subsystem '%s' should be present in %s", expectedSub, context)
|
|
}
|
|
}
|
|
|
|
func validateAllSubsystemsPresentCLI(t *testing.T, expectedSubsystems []string, actualLevels map[string]string, context string) {
|
|
t.Helper()
|
|
for _, expectedSub := range expectedSubsystems {
|
|
expectedSub = strings.TrimSpace(expectedSub)
|
|
if expectedSub == "" {
|
|
continue
|
|
}
|
|
_, found := actualLevels[expectedSub]
|
|
assert.True(t, found, "Expected subsystem '%s' should be present in %s", expectedSub, context)
|
|
}
|
|
}
|