feat: use a debug logger for agent

This commit is contained in:
Aarnav Tale 2025-04-08 14:51:28 -04:00
parent bbc535d39e
commit b090354d50
No known key found for this signature in database
9 changed files with 151 additions and 79 deletions

View File

@ -2,39 +2,30 @@ package main
import ( import (
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
"github.com/tale/headplane/agent/config" "github.com/tale/headplane/agent/internal/config"
"github.com/tale/headplane/agent/tsnet" "github.com/tale/headplane/agent/internal/hpagent"
"github.com/tale/headplane/agent/hpagent" "github.com/tale/headplane/agent/internal/tsnet"
"log" "github.com/tale/headplane/agent/internal/util"
) )
func main() { func main() {
log := util.GetLogger()
cfg, err := config.Load() cfg, err := config.Load()
if err != nil { if err != nil {
log.Fatalf("Failed to load configuration: %s", err) log.Fatal("Failed to load config: %s", err)
} }
agent := tsnet.NewAgent( log.SetDebug(cfg.Debug)
cfg.Hostname, agent := tsnet.NewAgent(cfg)
cfg.TSControlURL,
cfg.TSAuthKey,
cfg.Debug,
)
agent.StartAndFetchID() agent.Connect()
defer agent.Shutdown() defer agent.Shutdown()
ws, err := hpagent.NewSocket( ws, err := hpagent.NewSocket(agent, cfg)
agent,
cfg.HPControlURL,
cfg.HPAuthKey,
cfg.Debug,
)
if err != nil { if err != nil {
log.Fatalf("Failed to create websocket: %s", err) log.Fatal("Failed to create websocket: %s", err)
} }
defer ws.StopListening() defer ws.StopListening()
ws.StartListening() ws.FollowMaster()
} }

View File

@ -1,10 +1,6 @@
package config package config
import ( import "os"
"os"
_ "github.com/joho/godotenv/autoload"
)
// Config represents the configuration for the agent. // Config represents the configuration for the agent.
type Config struct { type Config struct {

View File

@ -38,8 +38,7 @@ func validateTSReady(config *Config) error {
testURL = testURL[:len(testURL)-1] testURL = testURL[:len(testURL)-1]
} }
// TODO: Consequences of switching to /health (headscale only) testURL = fmt.Sprintf("%s/health", testURL)
testURL = fmt.Sprintf("%s/key?v=109", testURL)
resp, err := http.Get(testURL) resp, err := http.Get(testURL)
if err != nil { if err != nil {
return fmt.Errorf("Failed to connect to TS control server: %s", err) return fmt.Errorf("Failed to connect to TS control server: %s", err)

View File

@ -2,38 +2,41 @@ package hpagent
import ( import (
"encoding/json" "encoding/json"
"log"
"sync" "sync"
"github.com/tale/headplane/agent/internal/util"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
// Represents messages from the Headplane master // Represents messages from the Headplane master
type RecvMessage struct { type RecvMessage struct {
NodeIDs []string `json:omitempty` NodeIDs []string
} }
// Starts listening for messages from the Headplane master // Starts listening for messages from the Headplane master
func (s *Socket) StartListening() { func (s *Socket) FollowMaster() {
log := util.GetLogger()
for { for {
_, message, err := s.ReadMessage() _, message, err := s.ReadMessage()
if err != nil { if err != nil {
log.Printf("error reading message: %v", err) log.Error("Error reading message: %s", err)
return return
} }
var msg RecvMessage var msg RecvMessage
err = json.Unmarshal(message, &msg) err = json.Unmarshal(message, &msg)
if err != nil { if err != nil {
log.Printf("error unmarshalling message: %v", err) log.Error("Unable to unmarshal message: %s", err)
log.Debug("Full Error: %v", err)
continue continue
} }
if s.Debug { log.Debug("Recieved message from master: %v", message)
log.Printf("got message: %s", message)
}
if len(msg.NodeIDs) == 0 { if len(msg.NodeIDs) == 0 {
log.Printf("got a message with no node IDs? %s", message) log.Debug("Message recieved had no node IDs")
log.Debug("Full message: %s", message)
continue continue
} }
@ -48,11 +51,12 @@ func (s *Socket) StartListening() {
defer wg.Done() defer wg.Done()
result, err := s.Agent.GetStatusForPeer(nodeID) result, err := s.Agent.GetStatusForPeer(nodeID)
if err != nil { if err != nil {
log.Printf("error getting status: %v", err) log.Error("Unable to get status for node %s: %s", nodeID, err)
return return
} }
if result == nil { if result == nil {
log.Debug("No status for node %s", nodeID)
return return
} }
@ -65,15 +69,12 @@ func (s *Socket) StartListening() {
wg.Wait() wg.Wait()
// Send the results back to the Headplane master // Send the results back to the Headplane master
log.Debug("Sending status back to master: %v", results)
err = s.SendStatus(results) err = s.SendStatus(results)
if err != nil { if err != nil {
log.Printf("error sending status: %v", err) log.Error("Error sending status: %s", err)
return return
} }
if s.Debug {
log.Printf("sent status: %s", results)
}
} }
} }

View File

@ -2,46 +2,50 @@ package hpagent
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/url" "net/url"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/tale/headplane/agent/tsnet" "github.com/tale/headplane/agent/internal/config"
"github.com/tale/headplane/agent/internal/tsnet"
"github.com/tale/headplane/agent/internal/util"
) )
type Socket struct { type Socket struct {
*websocket.Conn *websocket.Conn
Debug bool
Agent *tsnet.TSAgent Agent *tsnet.TSAgent
} }
// Creates a new websocket connection to the Headplane server. // Creates a new websocket connection to the Headplane server.
func NewSocket(agent *tsnet.TSAgent, controlURL, authKey string, debug bool) (*Socket, error) { func NewSocket(agent *tsnet.TSAgent, cfg *config.Config) (*Socket, error) {
wsURL, err := httpToWs(controlURL) log := util.GetLogger()
wsURL, err := httpToWs(cfg.HPControlURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
headers := http.Header{} headers := http.Header{}
headers.Add("X-Headplane-Tailnet-ID", agent.ID) headers.Add("X-Headplane-Tailnet-ID", agent.ID)
auth := fmt.Sprintf("Bearer %s", cfg.HPAuthKey)
auth := fmt.Sprintf("Bearer %s", authKey)
headers.Add("Authorization", auth) headers.Add("Authorization", auth)
log.Printf("dialing websocket at %s", wsURL) log.Info("Dialing WebSocket with master: %s", wsURL)
ws, _, err := websocket.DefaultDialer.Dial(wsURL, headers) ws, _, err := websocket.DefaultDialer.Dial(wsURL, headers)
if err != nil { if err != nil {
log.Debug("Failed to dial WebSocket: %s", err)
return nil, err return nil, err
} }
return &Socket{ws, debug, agent}, nil return &Socket{ws, agent}, nil
} }
// We need to convert the control URL to a websocket URL // We need to convert the control URL to a websocket URL
func httpToWs(controlURL string) (string, error) { func httpToWs(controlURL string) (string, error) {
log := util.GetLogger()
u, err := url.Parse(controlURL) u, err := url.Parse(controlURL)
if err != nil { if err != nil {
log.Debug("Failed to parse control URL: %s", err)
return "", err return "", err
} }

View File

@ -2,9 +2,11 @@ package tsnet
import ( import (
"context" "context"
"encoding/hex"
"fmt" "fmt"
"log"
"strings" "strings"
"github.com/tale/headplane/agent/internal/util"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
@ -13,26 +15,41 @@ import (
// Returns the raw hostinfo for a peer based on node ID. // Returns the raw hostinfo for a peer based on node ID.
func (s *TSAgent) GetStatusForPeer(id string) (*tailcfg.HostinfoView, error) { func (s *TSAgent) GetStatusForPeer(id string) (*tailcfg.HostinfoView, error) {
log := util.GetLogger()
if !strings.HasPrefix(id, "nodekey:") { if !strings.HasPrefix(id, "nodekey:") {
log.Debug("Node ID with missing prefix: %s", id)
return nil, fmt.Errorf("invalid node ID: %s", id) return nil, fmt.Errorf("invalid node ID: %s", id)
} }
if s.Debug { log.Debug("Querying status of peer: %s", id)
log.Printf("querying peer state for %s", id)
}
status, err := s.Lc.Status(context.Background()) status, err := s.Lc.Status(context.Background())
if err != nil { if err != nil {
log.Debug("Failed to get status: %s", err)
return nil, fmt.Errorf("failed to get status: %w", err) return nil, fmt.Errorf("failed to get status: %w", err)
} }
nodeKey, err := key.ParseNodePublicUntyped(mem.S(id[8:])) // We need to convert from 64 char hex to 32 byte raw.
bytes, err := hex.DecodeString(id[8:])
if err != nil {
log.Debug("Failed to decode hex: %s", err)
return nil, fmt.Errorf("failed to decode hex: %w", err)
}
raw := mem.B(bytes)
if raw.Len() != 32 {
log.Debug("Invalid node ID length: %d", raw.Len())
return nil, fmt.Errorf("invalid node ID length: %d", raw.Len())
}
nodeKey := key.NodePublicFromRaw32(raw)
peer := status.Peer[nodeKey] peer := status.Peer[nodeKey]
if peer == nil { if peer == nil {
// Check if we are on Self. // Check if we are on Self.
if status.Self.PublicKey == nodeKey { if status.Self.PublicKey == nodeKey {
peer = status.Self peer = status.Self
} else { } else {
log.Debug("Peer not found in status: %s", id)
return nil, nil return nil, nil
} }
} }
@ -40,8 +57,10 @@ func (s *TSAgent) GetStatusForPeer(id string) (*tailcfg.HostinfoView, error) {
ip := peer.TailscaleIPs[0].String() ip := peer.TailscaleIPs[0].String()
whois, err := s.Lc.WhoIs(context.Background(), ip) whois, err := s.Lc.WhoIs(context.Background(), ip)
if err != nil { if err != nil {
log.Debug("Failed to get whois: %s", err)
return nil, fmt.Errorf("failed to get whois: %w", err) return nil, fmt.Errorf("failed to get whois: %w", err)
} }
log.Debug("Got whois for peer %s: %v", id, whois)
return &whois.Node.Hostinfo, nil return &whois.Node.Hostinfo, nil
} }

View File

@ -2,10 +2,8 @@ package tsnet
import ( import (
"context" "context"
"fmt" "github.com/tale/headplane/agent/internal/config"
"log" "github.com/tale/headplane/agent/internal/util"
"os"
"tailscale.com/client/tailscale" "tailscale.com/client/tailscale"
"tailscale.com/tsnet" "tailscale.com/tsnet"
) )
@ -15,43 +13,41 @@ type TSAgent struct {
*tsnet.Server *tsnet.Server
Lc *tailscale.LocalClient Lc *tailscale.LocalClient
ID string ID string
Debug bool
} }
// Creates a new tsnet agent and returns an instance of the server. // Creates a new tsnet agent and returns an instance of the server.
func NewAgent(hostname, controlURL, authKey string, debug bool) *TSAgent { func NewAgent(cfg *config.Config) *TSAgent {
s := &tsnet.Server{ server := &tsnet.Server{
Hostname: hostname, Hostname: cfg.Hostname,
ControlURL: controlURL, ControlURL: cfg.TSControlURL,
AuthKey: authKey, AuthKey: cfg.TSAuthKey,
Logf: func(string, ...interface{}) {}, // Disabled by default Logf: func(string, ...interface{}) {}, // Disabled by default
} }
if debug { if cfg.Debug {
s.Logf = log.New( log := util.GetLogger()
os.Stderr, server.Logf = log.Debug
fmt.Sprintf("[DBG:%s] ", hostname),
log.LstdFlags,
).Printf
} }
return &TSAgent{s, nil, "", debug} return &TSAgent{server, nil, ""}
} }
// Starts the tsnet agent and sets the node ID. // Starts the tsnet agent and sets the node ID.
func (s *TSAgent) StartAndFetchID() { func (s *TSAgent) Connect() {
log := util.GetLogger()
// Waits until the agent is up and running. // Waits until the agent is up and running.
status, err := s.Up(context.Background()) status, err := s.Up(context.Background())
if err != nil { if err != nil {
log.Fatalf("Failed to start agent: %v", err) log.Fatal("Failed to connect to Tailnet: %s", err)
} }
s.Lc, err = s.LocalClient() s.Lc, err = s.LocalClient()
if err != nil { if err != nil {
log.Fatalf("Failed to create local client: %v", err) log.Fatal("Failed to initialize local Tailscale client: %s", err)
} }
log.Printf("Agent running with ID: %s", status.Self.PublicKey) log.Info("Connected to Tailnet (PublicKey: %s)", status.Self.PublicKey)
s.ID = string(status.Self.ID) s.ID = string(status.Self.ID)
} }

View File

@ -0,0 +1,66 @@
package util
import (
"log"
"os"
"sync"
)
type Logger struct {
debug *log.Logger
info *log.Logger
error *log.Logger
}
var lock = &sync.Mutex{}
var logger *Logger
func GetLogger() *Logger {
if logger == nil {
lock.Lock()
defer lock.Unlock()
if logger == nil {
logger = NewLogger()
}
}
return logger
}
func NewLogger() *Logger {
// Create a new Logger for stdout and stderr
// Errors still go to both stdout and stderr
return &Logger{
debug: nil,
info: log.New(os.Stdout, "[INFO] ", log.LstdFlags),
error: log.New(os.Stderr, "[ERROR] ", log.LstdFlags),
}
}
func (logger *Logger) SetDebug(debug bool) {
if debug {
logger.Info("Enabling Debug logging for headplane-agent")
logger.Info("Be careful, this will spam a lot of information")
logger.debug = log.New(os.Stdout, "[DEBUG] ", log.LstdFlags)
} else {
logger.debug = nil
}
}
func (logger *Logger) Info(fmt string, v ...any) {
logger.info.Printf(fmt, v...)
}
func (logger *Logger) Debug(fmt string, v ...any) {
if logger.debug != nil {
logger.debug.Printf(fmt, v...)
}
}
func (logger *Logger) Error(fmt string, v ...any) {
logger.error.Printf(fmt, v...)
}
func (logger *Logger) Fatal(fmt string, v ...any) {
logger.error.Fatalf(fmt, v...)
}