Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions cmd/cli/client/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package client

import (
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/storacha/piri/pkg/admin"
)

var LogCmd = &cobra.Command{
Use: "log",
Short: "Manage logging subsystems and levels",
}

func init() {
logListCmd := &cobra.Command{
Use: "list",
Short: "List all logging subsystems and their levels",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
client := admin.NewClient(viper.GetString("node_url"))

// Get the current log levels for all subsystems
levels, err := client.ListLogLevels(cmd.Context())
if err != nil {
return fmt.Errorf("failed to get log levels: %w", err)
}

// Print each subsystem with its level
for subsystem, level := range levels.Levels {
cmd.Printf("%-30s %s\n", subsystem, level)
}

return nil
},
}

logSetLevelCmd := &cobra.Command{
Use: "set-level <level>",
Short: "Set log level for a subsystem or all subsystems",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
level := args[0]
systems, err := cmd.Flags().GetStringSlice("system")
if err != nil {
return err
}

client := admin.NewClient(viper.GetString("node_url"))

if len(systems) == 0 {
// If no systems are specified, get all subsystems from the server
levels, err := client.ListLogLevels(cmd.Context())
if err != nil {
return fmt.Errorf("failed to get logging subsystems: %w", err)
}
// Extract subsystem names from the levels map keys
for subsystem := range levels.Levels {
systems = append(systems, subsystem)
}
}

for _, system := range systems {
if err := client.SetLogLevel(cmd.Context(), system, level); err != nil {
return err
}
}

return nil
},
}

LogCmd.AddCommand(logListCmd)
LogCmd.AddCommand(logSetLevelCmd)
logSetLevelCmd.Flags().StringSlice("system", []string{}, "Subsystem to target. Pass multiple times for multiple systems.")
}
189 changes: 189 additions & 0 deletions cmd/cli/client/log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package client

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"sort"
"strings"
"testing"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

// setTestNodeURL configures viper with the host:port of the provided server
func setTestNodeURL(t *testing.T, ts *httptest.Server) func() {
t.Helper()
parsed, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("failed to parse test server URL: %v", err)
}
prev := viper.GetString("node_url")
viper.Set("node_url", parsed.Host)
return func() { viper.Set("node_url", prev) }
}

func TestLogList_PrintsSubsystemsAndLevels(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/log/level", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatalf("unexpected method: %s", r.Method)
}
_, _ = w.Write([]byte(`{"levels":{"alpha":"Info","beta":"Debug"}}`))
})
ts := httptest.NewServer(mux)
defer ts.Close()

restore := setTestNodeURL(t, ts)
defer restore()

var out bytes.Buffer
Cmd.SetOut(&out)
Cmd.SetErr(&out)
Cmd.SetArgs([]string{"log", "list"})

if err := Cmd.Execute(); err != nil {
t.Fatalf("log list failed: %v", err)
}

got := out.String()
if !strings.Contains(got, "alpha") || !strings.Contains(got, "Info") {
t.Fatalf("output missing alpha Info: %q", got)
}
if !strings.Contains(got, "beta") || !strings.Contains(got, "Debug") {
t.Fatalf("output missing beta Debug: %q", got)
}
}

func TestLogSetLevel_WithExplicitSystems(t *testing.T) {
var posted []string

mux := http.NewServeMux()
mux.HandleFunc("/log/level", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
defer r.Body.Close()
var req struct {
Subsystem string `json:"subsystem"`
Level string `json:"level"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("failed decoding request: %v", err)
}
if req.Level != "Warn" {
t.Fatalf("unexpected level: %s", req.Level)
}
posted = append(posted, req.Subsystem)
w.WriteHeader(http.StatusOK)
case http.MethodGet:
// Not expected in this test, but keep handler simple
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"levels":{}}`))
default:
t.Fatalf("unexpected method: %s", r.Method)
}
})

ts := httptest.NewServer(mux)
defer ts.Close()

restore := setTestNodeURL(t, ts)
defer restore()

var out bytes.Buffer
Cmd.SetOut(&out)
Cmd.SetErr(&out)
Cmd.SetArgs([]string{"log", "set-level", "Warn", "--system", "alpha", "--system", "beta"})

if err := Cmd.Execute(); err != nil {
t.Fatalf("set-level failed: %v", err)
}

sort.Strings(posted)
if len(posted) != 2 || posted[0] != "alpha" || posted[1] != "beta" {
t.Fatalf("unexpected POSTed subsystems: %v", posted)
}
}

func TestLogSetLevel_AllSystemsWhenNoneSpecified(t *testing.T) {
var posted []string

mux := http.NewServeMux()
mux.HandleFunc("/log/level", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
defer r.Body.Close()
var req struct {
Subsystem string `json:"subsystem"`
Level string `json:"level"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("failed decoding request: %v", err)
}
if req.Level != "Error" {
t.Fatalf("unexpected level: %s", req.Level)
}
posted = append(posted, req.Subsystem)
w.WriteHeader(http.StatusOK)
case http.MethodGet:
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"levels":{"alpha":"Info","beta":"Debug","gamma":"Warn"}}`))
default:
t.Fatalf("unexpected method: %s", r.Method)
}
})

ts := httptest.NewServer(mux)
defer ts.Close()

restore := setTestNodeURL(t, ts)
defer restore()

var out bytes.Buffer
Cmd.SetOut(&out)
Cmd.SetErr(&out)

// Ensure any prior test-provided --system values are cleared explicitly on the underlying command
var logCmd, setLevelCmd *cobra.Command
for _, c := range Cmd.Commands() {
if c.Use == "log" {
logCmd = c
break
}
}
if logCmd == nil {
t.Fatalf("log command not found")
}
for _, c := range logCmd.Commands() {
if strings.HasPrefix(c.Use, "set-level") {
setLevelCmd = c
break
}
}
if setLevelCmd == nil {
t.Fatalf("set-level command not found")
}
// Reset the slice flag to empty using pflag's SliceValue API to avoid state leakage across tests
if f := setLevelCmd.Flags().Lookup("system"); f != nil {
if sv, ok := f.Value.(pflag.SliceValue); ok {
_ = sv.Replace([]string{})
}
}

Cmd.SetArgs([]string{"log", "set-level", "Error"})

if err := Cmd.Execute(); err != nil {
t.Fatalf("set-level failed: %v", err)
}

sort.Strings(posted)
expected := []string{"alpha", "beta", "gamma"}
sort.Strings(expected)
if strings.Join(posted, ",") != strings.Join(expected, ",") {
t.Fatalf("unexpected POSTed subsystems: %v (expected %v)", posted, expected)
}
}
1 change: 1 addition & 0 deletions cmd/cli/client/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ func init() {

Cmd.AddCommand(ucan.Cmd)
Cmd.AddCommand(pdp.Cmd)
Cmd.AddCommand(LogCmd)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ require (
github.com/samber/lo v1.39.0
github.com/snadrus/must v0.0.0-20240605044437-98cedd57f8eb
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
github.com/storacha/go-libstoracha v0.2.0
github.com/storacha/go-ucanto v0.5.0
Expand Down Expand Up @@ -175,7 +176,6 @@ require (
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/supranational/blst v0.3.14 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
Expand Down Expand Up @@ -296,7 +296,7 @@ require (
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.39.0
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2235,8 +2235,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
Expand Down
58 changes: 58 additions & 0 deletions pkg/admin/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package admin

import (
"net/http"

logging "github.com/ipfs/go-log/v2"
"github.com/labstack/echo/v4"
)

// RegisterAdminRoutes registers the admin API routes on the given echo server.
func RegisterAdminRoutes(e *echo.Echo) {
e.GET("/log/level", listLogLevels)
e.POST("/log/level", setLogLevel)
}

// ListLogLevelsResponse defines the response for the list log levels endpoint.
type ListLogLevelsResponse struct {
Levels map[string]string `json:"levels"`
}

// SetLogLevelRequest defines the request for the set log level endpoint.
type SetLogLevelRequest struct {
Subsystem string `json:"subsystem"`
Level string `json:"level"`
}

func listLogLevels(c echo.Context) error {
// Get all subsystems and their current log levels
subsystems := logging.GetSubsystems()
levels := make(map[string]string, len(subsystems))

// Map each subsystem to its current log level
for _, subsystem := range subsystems {
levels[subsystem] = logging.Logger(subsystem).Level().String()
}

return c.JSON(http.StatusOK, &ListLogLevelsResponse{Levels: levels})
}

func setLogLevel(c echo.Context) error {
var req SetLogLevelRequest
if err := c.Bind(&req); err != nil {
return err
}

if req.Subsystem == "" {
return c.String(http.StatusBadRequest, "subsystem is required")
}
if req.Level == "" {
return c.String(http.StatusBadRequest, "level is required")
}

if err := logging.SetLogLevel(req.Subsystem, req.Level); err != nil {
return c.String(http.StatusBadRequest, err.Error())
}

return c.NoContent(http.StatusOK)
}
Loading
Loading