Files
netbird/client/uiwails/services/debug.go
Zoltán Papp 04a982263d Wails UI
2026-03-02 15:59:09 +01:00

197 lines
5.4 KiB
Go

//go:build !(linux && 386)
package services
import (
"context"
"fmt"
"time"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/proto"
)
// DebugService exposes debug bundle creation and log-level control to the Wails frontend.
type DebugService struct {
grpcClient GRPCClientIface
}
// NewDebugService creates a new DebugService.
func NewDebugService(g GRPCClientIface) *DebugService {
return &DebugService{grpcClient: g}
}
// DebugBundleParams holds the parameters for creating a debug bundle.
type DebugBundleParams struct {
Anonymize bool `json:"anonymize"`
SystemInfo bool `json:"systemInfo"`
Upload bool `json:"upload"`
UploadURL string `json:"uploadUrl"`
RunDurationMins int `json:"runDurationMins"`
EnablePersistence bool `json:"enablePersistence"`
}
// DebugBundleResult holds the result of creating a debug bundle.
type DebugBundleResult struct {
LocalPath string `json:"localPath"`
UploadedKey string `json:"uploadedKey"`
UploadFailureReason string `json:"uploadFailureReason"`
}
// CreateDebugBundle creates a debug bundle via the daemon.
func (s *DebugService) CreateDebugBundle(params DebugBundleParams) (*DebugBundleResult, error) {
conn, err := s.grpcClient.GetClient(time.Second)
if err != nil {
return nil, fmt.Errorf("get client: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
if params.RunDurationMins > 0 {
if err := s.configureForDebug(ctx, conn, params); err != nil {
return nil, err
}
}
req := &proto.DebugBundleRequest{
Anonymize: params.Anonymize,
SystemInfo: params.SystemInfo,
}
if params.Upload && params.UploadURL != "" {
req.UploadURL = params.UploadURL
}
resp, err := conn.DebugBundle(ctx, req)
if err != nil {
log.Errorf("DebugBundle rpc failed: %v", err)
return nil, fmt.Errorf("create debug bundle: %w", err)
}
return &DebugBundleResult{
LocalPath: resp.GetPath(),
UploadedKey: resp.GetUploadedKey(),
UploadFailureReason: resp.GetUploadFailureReason(),
}, nil
}
func (s *DebugService) configureForDebug(ctx context.Context, conn proto.DaemonServiceClient, params DebugBundleParams) error {
statusResp, err := conn.Status(ctx, &proto.StatusRequest{})
if err != nil {
return fmt.Errorf("get status: %w", err)
}
wasConnected := statusResp.Status == "Connected" || statusResp.Status == "Connecting"
logLevelResp, err := conn.GetLogLevel(ctx, &proto.GetLogLevelRequest{})
if err != nil {
return fmt.Errorf("get log level: %w", err)
}
originalLogLevel := logLevelResp.GetLevel()
// Set trace log level
if _, err := conn.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil {
return fmt.Errorf("set log level: %w", err)
}
// Bring service down then up to capture full connection logs
if _, err := conn.Down(ctx, &proto.DownRequest{}); err != nil {
log.Warnf("bring down for debug: %v", err)
}
time.Sleep(time.Second)
if params.EnablePersistence {
if _, err := conn.SetSyncResponsePersistence(ctx, &proto.SetSyncResponsePersistenceRequest{Enabled: true}); err != nil {
log.Warnf("enable sync persistence: %v", err)
}
}
if _, err := conn.Up(ctx, &proto.UpRequest{}); err != nil {
return fmt.Errorf("bring service up: %w", err)
}
time.Sleep(3 * time.Second)
if _, err := conn.StartCPUProfile(ctx, &proto.StartCPUProfileRequest{}); err != nil {
log.Warnf("start CPU profiling: %v", err)
}
// Wait for the collection duration
collectionDur := time.Duration(params.RunDurationMins) * time.Minute
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(collectionDur):
}
if _, err := conn.StopCPUProfile(ctx, &proto.StopCPUProfileRequest{}); err != nil {
log.Warnf("stop CPU profiling: %v", err)
}
// Restore original state
if !wasConnected {
if _, err := conn.Down(ctx, &proto.DownRequest{}); err != nil {
log.Warnf("restore down state: %v", err)
}
}
if originalLogLevel < proto.LogLevel_TRACE {
if _, err := conn.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: originalLogLevel}); err != nil {
log.Warnf("restore log level: %v", err)
}
}
return nil
}
// GetLogLevel returns the current daemon log level.
func (s *DebugService) GetLogLevel() (string, error) {
conn, err := s.grpcClient.GetClient(3 * time.Second)
if err != nil {
return "", fmt.Errorf("get client: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := conn.GetLogLevel(ctx, &proto.GetLogLevelRequest{})
if err != nil {
return "", fmt.Errorf("get log level rpc: %w", err)
}
return resp.GetLevel().String(), nil
}
// SetLogLevel sets the daemon log level.
func (s *DebugService) SetLogLevel(level string) error {
conn, err := s.grpcClient.GetClient(3 * time.Second)
if err != nil {
return fmt.Errorf("get client: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var protoLevel proto.LogLevel
switch level {
case "TRACE":
protoLevel = proto.LogLevel_TRACE
case "DEBUG":
protoLevel = proto.LogLevel_DEBUG
case "INFO":
protoLevel = proto.LogLevel_INFO
case "WARN", "WARNING":
protoLevel = proto.LogLevel_WARN
case "ERROR":
protoLevel = proto.LogLevel_ERROR
default:
protoLevel = proto.LogLevel_INFO
}
if _, err := conn.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: protoLevel}); err != nil {
return fmt.Errorf("set log level rpc: %w", err)
}
return nil
}