From c4dcacb245739745d548ffa36e4e3ffdb0b7382e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Sun, 15 Feb 2026 17:58:08 +0100 Subject: [PATCH 1/2] feature: enable windows agent --- Makefile | 2 +- api/api.pb.go | 30 ++++++++++++++++++++++------ api/api.proto | 2 ++ internal/controller/agent.go | 1 + internal/controller/agent_config.go | 30 ++++++++++++++++++++++++++++ internal/controller/agent_marshal.go | 3 ++- internal/grpc/server.go | 2 ++ internal/model/agent.go | 1 + 8 files changed, 63 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index f933936..0d16cf1 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ test: .PHONY: proto proto: - test -z $(protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api/api.proto >/dev/null 2>&1 || echo 1) || (echo "[WARN] Fix proto generation issues" && exit 1) + protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api/api.proto .PHONY: dist dist: diff --git a/api/api.pb.go b/api/api.pb.go index 3ea6ed9..0c6f8c0 100644 --- a/api/api.pb.go +++ b/api/api.pb.go @@ -29,6 +29,7 @@ type RegisterAgentRequest struct { Metrics bool `protobuf:"varint,4,opt,name=metrics,proto3" json:"metrics,omitempty"` MetricsTargets []string `protobuf:"bytes,5,rep,name=metrics_targets,json=metricsTargets,proto3" json:"metrics_targets,omitempty"` Profiles bool `protobuf:"varint,6,opt,name=profiles,proto3" json:"profiles,omitempty"` + Node string `protobuf:"bytes,7,opt,name=node,proto3" json:"node,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -105,6 +106,13 @@ func (x *RegisterAgentRequest) GetProfiles() bool { return false } +func (x *RegisterAgentRequest) GetNode() string { + if x != nil { + return x.Node + } + return "" +} + type RegisterAgentResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Rid string `protobuf:"bytes,1,opt,name=rid,proto3" json:"rid,omitempty"` @@ -283,6 +291,7 @@ type GetAgentResponse struct { MetricsTargets []string `protobuf:"bytes,6,rep,name=metrics_targets,json=metricsTargets,proto3" json:"metrics_targets,omitempty"` Profiles bool `protobuf:"varint,7,opt,name=profiles,proto3" json:"profiles,omitempty"` CreatedAt string `protobuf:"bytes,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + Node string `protobuf:"bytes,9,opt,name=node,proto3" json:"node,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -373,6 +382,13 @@ func (x *GetAgentResponse) GetCreatedAt() string { return "" } +func (x *GetAgentResponse) GetNode() string { + if x != nil { + return x.Node + } + return "" +} + type ListAgentsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -827,7 +843,7 @@ func (*UpdateAgentResponse) Descriptor() ([]byte, []int) { type GetDashboardTokenRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - SessionTimeout *int32 `protobuf:"varint,1,opt,name=session_timeout,json=sessionTimeout,proto3,oneof" json:"session_timeout,omitempty"` // Session timeout in seconds (default: 1800) + SessionTimeout *int32 `protobuf:"varint,1,opt,name=session_timeout,json=sessionTimeout,proto3,oneof" json:"session_timeout,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -872,7 +888,7 @@ func (x *GetDashboardTokenRequest) GetSessionTimeout() int32 { type GetDashboardTokenResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` - ExpiresAt string `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // ISO8601 timestamp + ExpiresAt string `protobuf:"bytes,2,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` DashboardUrl string `protobuf:"bytes,3,opt,name=dashboard_url,json=dashboardUrl,proto3" json:"dashboard_url,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -933,7 +949,7 @@ var File_api_api_proto protoreflect.FileDescriptor const file_api_api_proto_rawDesc = "" + "\n" + - "\rapi/api.proto\x12\x05finch\"\xca\x01\n" + + "\rapi/api.proto\x12\x05finch\"\xde\x01\n" + "\x14RegisterAgentRequest\x12\x1a\n" + "\bhostname\x18\x01 \x01(\tR\bhostname\x12\x16\n" + "\x06labels\x18\x02 \x03(\tR\x06labels\x12\x1f\n" + @@ -941,14 +957,15 @@ const file_api_api_proto_rawDesc = "" + "logSources\x12\x18\n" + "\ametrics\x18\x04 \x01(\bR\ametrics\x12'\n" + "\x0fmetrics_targets\x18\x05 \x03(\tR\x0emetricsTargets\x12\x1a\n" + - "\bprofiles\x18\x06 \x01(\bR\bprofiles\")\n" + + "\bprofiles\x18\x06 \x01(\bR\bprofiles\x12\x12\n" + + "\x04node\x18\a \x01(\tR\x04node\")\n" + "\x15RegisterAgentResponse\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\"*\n" + "\x16DeregisterAgentRequest\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\"\x19\n" + "\x17DeregisterAgentResponse\"#\n" + "\x0fGetAgentRequest\x12\x10\n" + - "\x03rid\x18\x01 \x01(\tR\x03rid\"\x86\x02\n" + + "\x03rid\x18\x01 \x01(\tR\x03rid\"\x9a\x02\n" + "\x10GetAgentResponse\x12\x1f\n" + "\vresource_id\x18\x01 \x01(\tR\n" + "resourceId\x12\x1a\n" + @@ -960,7 +977,8 @@ const file_api_api_proto_rawDesc = "" + "\x0fmetrics_targets\x18\x06 \x03(\tR\x0emetricsTargets\x12\x1a\n" + "\bprofiles\x18\a \x01(\bR\bprofiles\x12\x1d\n" + "\n" + - "created_at\x18\b \x01(\tR\tcreatedAt\"\x13\n" + + "created_at\x18\b \x01(\tR\tcreatedAt\x12\x12\n" + + "\x04node\x18\t \x01(\tR\x04node\"\x13\n" + "\x11ListAgentsRequest\"=\n" + "\rAgentListItem\x12\x10\n" + "\x03rid\x18\x01 \x01(\tR\x03rid\x12\x1a\n" + diff --git a/api/api.proto b/api/api.proto index 5fe998a..ba38082 100644 --- a/api/api.proto +++ b/api/api.proto @@ -28,6 +28,7 @@ message RegisterAgentRequest { bool metrics = 4; repeated string metrics_targets = 5; bool profiles = 6; + string node = 7; } message RegisterAgentResponse { @@ -53,6 +54,7 @@ message GetAgentResponse { repeated string metrics_targets = 6; bool profiles = 7; string created_at = 8; + string node = 9; } message ListAgentsRequest {} diff --git a/internal/controller/agent.go b/internal/controller/agent.go index 906ba34..4b62b8c 100644 --- a/internal/controller/agent.go +++ b/internal/controller/agent.go @@ -26,6 +26,7 @@ type Agent struct { Metrics bool `json:"metrics"` MetricsTargets []string `json:"metrics_targets"` Profiles bool `json:"profiles"` + Node string `json:"node"` } func (c *Controller) RegisterAgent(data *Agent) (string, error) { diff --git a/internal/controller/agent_config.go b/internal/controller/agent_config.go index e289ae2..7395050 100644 --- a/internal/controller/agent_config.go +++ b/internal/controller/agent_config.go @@ -129,6 +129,15 @@ loki.source.file "file" { } {{ end -}} +{{ if .LogSources.Events }} +{{ range $index, $source := .LogSources.Events }} +loki.source.windowsevent "event_{{ $index }}" { + eventlog_name = "{{ $source }}" + forward_to = [loki.write.default.receiver] +} +{{ end -}} +{{ end -}} + {{ if .Metrics }} prometheus.remote_write "default" { @@ -151,6 +160,7 @@ prometheus.remote_write "default" { } } +{{ if eq .Node "unix" }} prometheus.exporter.unix "node" { include_exporter_metrics = true enable_collectors = [ @@ -163,6 +173,18 @@ prometheus.scrape "node" { forward_to = [prometheus.remote_write.default.receiver] scrape_interval = "15s" } +{{ end -}} + +{{ if eq .Node "windows" }} +prometheus.exporter.windows "node" {} + +prometheus.scrape "node" { + targets = prometheus.exporter.windows.node.targets + forward_to = [prometheus.remote_write.default.receiver] + scrape_interval = "15s" +} +{{ end -}} + prometheus.receive_http "default" { http { @@ -220,6 +242,7 @@ pyroscope.write "backend" { type alloyConfigData struct { ServiceName string Hostname string + Node string Token string TokenExpiry string ResourceId string @@ -227,6 +250,7 @@ type alloyConfigData struct { Journal bool Docker bool Files []string + Events []string } Metrics bool MetricsTargets []struct { @@ -255,6 +279,7 @@ func (c *Controller) generateAlloyConfig(agent *model.Agent) (*alloyConfigData, data := &alloyConfigData{ Hostname: agent.Hostname, + Node: agent.Node, ServiceName: c.config.Hostname(), Token: token, TokenExpiry: expiresAt.Format("2006-01-02 15:04:05 MST"), @@ -263,10 +288,12 @@ func (c *Controller) generateAlloyConfig(agent *model.Agent) (*alloyConfigData, Journal bool Docker bool Files []string + Events []string }{ Journal: false, Docker: false, Files: make([]string, 0), + Events: make([]string, 0), }, Metrics: agent.Metrics, MetricsTargets: make([]struct { @@ -291,6 +318,9 @@ func (c *Controller) generateAlloyConfig(agent *model.Agent) (*alloyConfigData, case "file": files = append(files, fmt.Sprintf("{__path__ = \"%s\"}", uri.Path)) data.LogSources.Files = files + case "event": + events := append(data.LogSources.Events, uri.Host) + data.LogSources.Events = events default: continue } diff --git a/internal/controller/agent_marshal.go b/internal/controller/agent_marshal.go index 6ec65e6..b025139 100644 --- a/internal/controller/agent_marshal.go +++ b/internal/controller/agent_marshal.go @@ -27,6 +27,7 @@ func (c *Controller) marshalNewAgent(data *Agent) (*model.Agent, error) { agent := &model.Agent{ Hostname: data.Hostname, + Node: data.Node, LogSources: effectiveLogSources, Metrics: data.Metrics, MetricsTargets: effectiveMetricsTargets, @@ -66,7 +67,7 @@ func (c *Controller) __parseLogSources(data *Agent) ([]string, error) { if err != nil { continue } - if !slices.Contains([]string{"journal", "docker", "file"}, uri.Scheme) { + if !slices.Contains([]string{"journal", "docker", "file", "event"}, uri.Scheme) { continue } diff --git a/internal/grpc/server.go b/internal/grpc/server.go index 743d6d0..e602933 100644 --- a/internal/grpc/server.go +++ b/internal/grpc/server.go @@ -71,6 +71,7 @@ func (s *AgentServer) RegisterAgent(ctx context.Context, req *api.RegisterAgentR Metrics: req.Metrics, MetricsTargets: req.MetricsTargets, Profiles: req.Profiles, + Node: req.Node, } rid, err := s.controller.RegisterAgent(agent) @@ -122,6 +123,7 @@ func (s *AgentServer) GetAgent(ctx context.Context, req *api.GetAgentRequest) (* MetricsTargets: agent.MetricsTargets, Profiles: agent.Profiles, CreatedAt: agent.CreatedAt.Format(time.RFC3339), + Node: agent.Node, }, nil } diff --git a/internal/model/agent.go b/internal/model/agent.go index 950c4cf..3df1fa8 100644 --- a/internal/model/agent.go +++ b/internal/model/agent.go @@ -25,6 +25,7 @@ type Agent struct { RegisteredAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP" json:"registered_at"` ResourceId string `gorm:"not null;unique;uniqueIndex:uidx_agents_resource_id" json:"resource_id"` Labels []string `gorm:"serializer:json" json:"labels"` + Node string `gorm:"not null;default:'unix'" json:"node"` } var ( From 9247c4d71d5d65328fbf2f4a6846b224dde820bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Sun, 15 Feb 2026 18:04:44 +0100 Subject: [PATCH 2/2] ci: fix db columns test --- internal/controller/agent_marshal.go | 4 ++++ internal/controller/agent_test.go | 17 ++++++++++++++++- internal/database/database_test.go | 1 + internal/grpc/server_test.go | 4 ++++ internal/http/handlers_test.go | 5 +++++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/controller/agent_marshal.go b/internal/controller/agent_marshal.go index b025139..b11d59f 100644 --- a/internal/controller/agent_marshal.go +++ b/internal/controller/agent_marshal.go @@ -18,6 +18,10 @@ func (c *Controller) marshalNewAgent(data *Agent) (*model.Agent, error) { return nil, fmt.Errorf("hostname must not be empty") } + if data.Node == "" || !slices.Contains([]string{"windows", "unix"}, data.Node) { + return nil, fmt.Errorf("node must be either 'windows' or 'unix'") + } + effectiveLogSources, err := c.__parseLogSources(data) if err != nil { return nil, err diff --git a/internal/controller/agent_test.go b/internal/controller/agent_test.go index f1af7f3..22a1b59 100644 --- a/internal/controller/agent_test.go +++ b/internal/controller/agent_test.go @@ -44,6 +44,7 @@ func Test_RegisterAgentReturnsError_InvalidParameters(t *testing.T) { data := Agent{ Hostname: "", + Node: "unix", Labels: nil, LogSources: nil, Metrics: false, @@ -64,6 +65,11 @@ func Test_RegisterAgentReturnsError_InvalidParameters(t *testing.T) { _, err = ctrl.RegisterAgent(&data) expected = "no valid log source specified" assert.EqualError(t, err, expected, "register agent with invalid log source") + + data.Node = "invalid" + _, err = ctrl.RegisterAgent(&data) + expected = "node must be either 'windows' or 'unix'" + assert.EqualError(t, err, expected, "register agent with invalid node type") } func Test_RegisterAgentReturnsResourceId(t *testing.T) { @@ -74,6 +80,7 @@ func Test_RegisterAgentReturnsResourceId(t *testing.T) { data := Agent{ Hostname: "test-host", + Node: "unix", Labels: []string{"key=value", "env=prod"}, LogSources: []string{"file:///var/log/syslog"}, Metrics: false, @@ -114,6 +121,7 @@ func Test_DeregisterAgentSucceeds(t *testing.T) { data := Agent{ Hostname: "test-host", + Node: "unix", Labels: []string{"key=value"}, LogSources: []string{"file:///var/log/syslog"}, Metrics: false, @@ -147,6 +155,7 @@ func Test_CreateAgentConfigReturnsConfig(t *testing.T) { data := Agent{ Hostname: "test-host", + Node: "unix", Labels: []string{"key=value", "statement"}, LogSources: []string{"file:///var/log/syslog", "journal://", "docker://"}, Metrics: false, @@ -181,6 +190,7 @@ func Test_GetAgentReturnsAgent(t *testing.T) { data := Agent{ Hostname: "test-host", + Node: "unix", Labels: []string{"key=value"}, LogSources: []string{"file:///var/log/syslog"}, Metrics: false, @@ -215,6 +225,7 @@ func Test_ListAgentsReturnsAgents(t *testing.T) { data := Agent{ Hostname: "test-host-1", + Node: "unix", Labels: []string{"key=value"}, LogSources: []string{"file:///var/log/syslog"}, Metrics: false, @@ -227,8 +238,9 @@ func Test_ListAgentsReturnsAgents(t *testing.T) { data = Agent{ Hostname: "test-host-2", + Node: "windows", Labels: []string{"env=dev"}, - LogSources: []string{"file:///var/log/syslog"}, + LogSources: []string{"event://System"}, Metrics: false, MetricsTargets: nil, Profiles: false, @@ -252,6 +264,7 @@ func Test_UpdateAgentReturnsError_AgentNotFound(t *testing.T) { data := Agent{ Hostname: "non-existent-rid", + Node: "unix", Labels: []string{"key=value"}, LogSources: []string{"file:///var/log/syslog"}, Metrics: false, @@ -272,6 +285,7 @@ func Test_UpdateAgentSucceeds(t *testing.T) { data := Agent{ Hostname: "test-host-update", + Node: "unix", Labels: []string{"key=value"}, LogSources: []string{"file:///var/log/syslog"}, Metrics: false, @@ -284,6 +298,7 @@ func Test_UpdateAgentSucceeds(t *testing.T) { updatedData := Agent{ Labels: []string{"env=staging"}, + Node: "unix", LogSources: []string{"journal://"}, Metrics: true, MetricsTargets: []string{"http://localhost:9100/metrics"}, diff --git a/internal/database/database_test.go b/internal/database/database_test.go index e6d1c16..1dad3c3 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -79,6 +79,7 @@ func Test_MigrateSucceeds(t *testing.T) { "registered_at", "resource_id", "updated_at", + "node", } assert.Equal(t, len(results), len(columns), "agents table should have correct number of columns") diff --git a/internal/grpc/server_test.go b/internal/grpc/server_test.go index ec4f863..95c1db1 100644 --- a/internal/grpc/server_test.go +++ b/internal/grpc/server_test.go @@ -46,6 +46,7 @@ func newController(t *testing.T) *controller.Controller { func registerAgent(t *testing.T, server *AgentServer, hostname string) *api.RegisterAgentResponse { req := &api.RegisterAgentRequest{ Hostname: hostname, + Node: "unix", LogSources: []string{"journal://"}, } resp, err := server.RegisterAgent(context.Background(), req) @@ -62,6 +63,7 @@ func TestRegisterAgentReturnsResourceId(t *testing.T) { req := &api.RegisterAgentRequest{ Hostname: "test-host", + Node: "unix", LogSources: []string{"journal://"}, } resp, err := server.RegisterAgent(context.Background(), req) @@ -77,6 +79,7 @@ func TestRegisterAgentReturnsError_AgentAlreadyExists(t *testing.T) { req := &api.RegisterAgentRequest{ Hostname: "existing", + Node: "unix", LogSources: []string{"journal://"}, } resp, err := server.RegisterAgent(context.Background(), req) @@ -92,6 +95,7 @@ func TestRegisterAgentReturnsError_InvalidArguments(t *testing.T) { req := &api.RegisterAgentRequest{ Hostname: "", + Node: "unix", LogSources: []string{"journal://"}, } diff --git a/internal/http/handlers_test.go b/internal/http/handlers_test.go index 517894e..e3961dc 100644 --- a/internal/http/handlers_test.go +++ b/internal/http/handlers_test.go @@ -241,6 +241,7 @@ func TestWebSocketHandlesDownloadConfigMessage(t *testing.T) { agentData := &controller.Agent{ Hostname: "test-host", + Node: "unix", LogSources: []string{"journal://"}, } rid, err := ctrl.RegisterAgent(agentData) @@ -283,6 +284,7 @@ func TestWebSocketHandlesGetTokenMessage(t *testing.T) { agentData := &controller.Agent{ Hostname: "test-host", + Node: "unix", LogSources: []string{"journal://"}, } rid, err := ctrl.RegisterAgent(agentData) @@ -327,6 +329,7 @@ func TestAgentListDataPagination(t *testing.T) { for i := range 11 { agentData := &controller.Agent{ Hostname: "test-host-" + string(rune('a'+i)), + Node: "unix", LogSources: []string{"journal://"}, } _, err := ctrl.RegisterAgent(agentData) @@ -365,6 +368,7 @@ func TestAgentListDataSearch(t *testing.T) { prodAgent := &controller.Agent{ Hostname: "prod-server", + Node: "unix", LogSources: []string{"journal://"}, } _, err := ctrl.RegisterAgent(prodAgent) @@ -372,6 +376,7 @@ func TestAgentListDataSearch(t *testing.T) { devAgent := &controller.Agent{ Hostname: "dev-server", + Node: "unix", LogSources: []string{"journal://"}, } _, err = ctrl.RegisterAgent(devAgent)