mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: handle logging from the agent
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
// Config represents the configuration for the agent.
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Hostname string
|
||||
TSControlURL string
|
||||
TSAuthKey string
|
||||
WorkDir string
|
||||
}
|
||||
|
||||
const (
|
||||
DebugEnv = "HEADPLANE_AGENT_DEBUG"
|
||||
HostnameEnv = "HEADPLANE_AGENT_HOSTNAME"
|
||||
TSControlURLEnv = "HEADPLANE_AGENT_TS_SERVER"
|
||||
TSAuthKeyEnv = "HEADPLANE_AGENT_TS_AUTHKEY"
|
||||
WorkDirEnv = "HEADPLANE_AGENT_WORK_DIR"
|
||||
)
|
||||
|
||||
// Load reads the agent configuration from environment variables.
|
||||
func Load() (*Config, error) {
|
||||
c := &Config{
|
||||
Debug: false,
|
||||
Hostname: os.Getenv(HostnameEnv),
|
||||
TSControlURL: os.Getenv(TSControlURLEnv),
|
||||
TSAuthKey: os.Getenv(TSAuthKeyEnv),
|
||||
WorkDir: os.Getenv(WorkDirEnv),
|
||||
}
|
||||
|
||||
if os.Getenv(DebugEnv) == "true" {
|
||||
c.Debug = true
|
||||
}
|
||||
|
||||
if err := validateRequired(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateTSReady(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package hpagent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/tale/headplane/internal/tsnet"
|
||||
"github.com/tale/headplane/internal/util"
|
||||
)
|
||||
|
||||
// Starts listening for messages from stdin
|
||||
func FollowMaster(agent *tsnet.TSAgent) {
|
||||
log := util.GetLogger()
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
log.Info("Listening for messages from Headplane master on stdin")
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
directive := string(line)
|
||||
|
||||
log.Debug("Received directive from master: %s", directive)
|
||||
switch directive {
|
||||
case "SHUTDOWN":
|
||||
log.Debug("Received SHUTDOWN directive from master, shutting down agent")
|
||||
agent.Shutdown()
|
||||
return
|
||||
|
||||
case "START":
|
||||
log.Debug("Received START directive from master, starting agent")
|
||||
// TODO: Start the agent here instead of in main
|
||||
fmt.Println("READY " + agent.ID)
|
||||
continue
|
||||
|
||||
case "PING":
|
||||
log.Debug("Received PING directive from master, responding with PONG")
|
||||
fmt.Println("PONG " + agent.ID)
|
||||
continue
|
||||
|
||||
case "REFRESH":
|
||||
log.Debug("Received REFRESH directive from master, refreshing status for all nodes")
|
||||
err := agent.DispatchHostInfo(context.Background())
|
||||
if err != nil {
|
||||
log.Error("Error refreshing host info: %s", err)
|
||||
fmt.Println("ERR " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatal("Error reading from stdin: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Checks to make sure all required environment variables are set
|
||||
func validateRequired(config *Config) error {
|
||||
if config.Hostname == "" {
|
||||
return fmt.Errorf("%s is required", HostnameEnv)
|
||||
}
|
||||
|
||||
if config.TSControlURL == "" {
|
||||
return fmt.Errorf("%s is required", TSControlURLEnv)
|
||||
}
|
||||
|
||||
if config.TSAuthKey == "" {
|
||||
return fmt.Errorf("%s is required", TSAuthKeyEnv)
|
||||
}
|
||||
|
||||
if config.WorkDir == "" {
|
||||
return fmt.Errorf("%s is required", WorkDirEnv)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pings the Tailscale control server to make sure it's up and running
|
||||
func validateTSReady(config *Config) error {
|
||||
testURL := config.TSControlURL
|
||||
if strings.HasSuffix(testURL, "/") {
|
||||
testURL = testURL[:len(testURL)-1]
|
||||
}
|
||||
|
||||
testURL = fmt.Sprintf("%s/health", testURL)
|
||||
resp, err := http.Get(testURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect to TS control server: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("Failed to connect to TS control server: %s", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+5
-69
@@ -4,19 +4,16 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
LevelInfo LogLevel = "info"
|
||||
LevelDebug LogLevel = "debug"
|
||||
LevelError LogLevel = "error"
|
||||
LevelFatal LogLevel = "fatal"
|
||||
LevelMsg LogLevel = "msg"
|
||||
LevelInfo LogLevel = "INFO"
|
||||
LevelDebug LogLevel = "DEBUG"
|
||||
LevelError LogLevel = "ERROR"
|
||||
LevelFatal LogLevel = "FATAL"
|
||||
)
|
||||
|
||||
type LogMessage struct {
|
||||
@@ -61,19 +58,8 @@ func (l *Logger) SetDebug(enabled bool) {
|
||||
|
||||
func (l *Logger) log(level LogLevel, format string, v ...any) {
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
|
||||
// Manually construct compact JSON line for performance
|
||||
line := `{"Level":"` + string(level) +
|
||||
`","Time":"` + timestamp +
|
||||
`","Message":"` + escapeString(msg) + `"}` + "\n"
|
||||
|
||||
if level == LevelError || level == LevelFatal {
|
||||
os.Stderr.WriteString(line)
|
||||
}
|
||||
|
||||
// Always write to stdout but also write to stderr for errors
|
||||
os.Stdout.WriteString(line)
|
||||
fmt.Printf("LOG %s %s\n", level, msg)
|
||||
if level == LevelFatal {
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -88,53 +74,3 @@ func (l *Logger) Debug(format string, v ...any) {
|
||||
func (l *Logger) Info(format string, v ...any) { l.log(LevelInfo, format, v...) }
|
||||
func (l *Logger) Error(format string, v ...any) { l.log(LevelError, format, v...) }
|
||||
func (l *Logger) Fatal(format string, v ...any) { l.log(LevelFatal, format, v...) }
|
||||
|
||||
func (l *Logger) Msg(obj any) {
|
||||
entry := l.pool.Get().(*LogMessage)
|
||||
defer l.pool.Put(entry)
|
||||
|
||||
entry.Level = LevelMsg
|
||||
entry.Time = time.Now().Format(time.RFC3339)
|
||||
entry.Message = obj
|
||||
|
||||
// Because the encoder is tied to STDOUT we get a message
|
||||
_ = l.encoder.Encode(entry)
|
||||
|
||||
// Reset the entry for reuse
|
||||
entry.Level = ""
|
||||
entry.Time = ""
|
||||
entry.Message = nil
|
||||
}
|
||||
|
||||
func escapeString(s string) string {
|
||||
var b strings.Builder
|
||||
b.Grow(len(s) + 16) // pre-grow to reduce reallocs
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch c {
|
||||
case '"':
|
||||
b.WriteString(`\"`)
|
||||
case '\\':
|
||||
b.WriteString(`\\`)
|
||||
case '\b':
|
||||
b.WriteString(`\b`)
|
||||
case '\f':
|
||||
b.WriteString(`\f`)
|
||||
case '\n':
|
||||
b.WriteString(`\n`)
|
||||
case '\r':
|
||||
b.WriteString(`\r`)
|
||||
case '\t':
|
||||
b.WriteString(`\t`)
|
||||
default:
|
||||
if c < 0x20 {
|
||||
// Control characters like 0x01, 0x07 (bell), etc.
|
||||
fmt.Fprintf(&b, `\u%04x`, c)
|
||||
} else {
|
||||
b.WriteByte(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user