diff --git a/Makefile b/Makefile index 4ed7a98..d297d90 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ lint: .PHONY: proto proto: - test -z $(shell protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative internal/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 internal/api/api.proto .PHONY: dist dist: diff --git a/cmd/agent/describe.go b/cmd/agent/describe.go index 6d54799..d114e28 100644 --- a/cmd/agent/describe.go +++ b/cmd/agent/describe.go @@ -57,6 +57,7 @@ func runDescribeCmd(cmd *cobra.Command, args []string) { t := tablewriter.NewWriter(os.Stdout) t.Header([]string{"Property", "Value"}) _ = t.Append([]string{"Hostname", desc.Hostname}) + _ = t.Append([]string{"Node", desc.Node}) if (len(desc.Labels)) > 0 { _ = t.Append([]string{"Labels", strings.Join(desc.Labels, ", ")}) } @@ -65,6 +66,9 @@ func runDescribeCmd(cmd *cobra.Command, args []string) { if (len(desc.Logs.Files)) > 0 { _ = t.Append([]string{"Files", strings.Join(desc.Logs.Files, "\n")}) } + if (len(desc.Logs.Events)) > 0 { + _ = t.Append([]string{"Events", strings.Join(desc.Logs.Events, "\n")}) + } _ = t.Append([]string{"Metrics", fmt.Sprintf("%v", desc.Metrics.Enable)}) if (len(desc.Metrics.Targets)) > 0 { _ = t.Append([]string{"Metrics targets", strings.Join(desc.Metrics.Targets, "\n")}) diff --git a/cmd/agent/register.go b/cmd/agent/register.go index febf810..70cb1e5 100644 --- a/cmd/agent/register.go +++ b/cmd/agent/register.go @@ -53,6 +53,14 @@ func init() { registerCmd.Flags().String("agent.config", "finch-agent.cfg", "Path to the configuration file") registerCmd.Flags().String("agent.file", "", "Path to a file containing agent data") + + registerCmd.Flags().StringSlice("agent.logs.events", nil, "Collect windows log events") + _ = viper.BindPFlag("logs.events", registerCmd.Flags().Lookup("agent.logs.events")) + + registerCmd.Flags().String("agent.node", "unix", "Node type of the agent (unix, windows)") + _ = viper.BindPFlag("agent.node", registerCmd.Flags().Lookup("agent.node")) + + _ = registerCmd.RegisterFlagCompletionFunc("agent.node", completion.CompleteNodeName) } func runRegisterPreCmd(cmd *cobra.Command, args []string) { @@ -104,10 +112,18 @@ func parseFlags(formatType target.Format) *agent.RegisterData { } } + logEvents := viper.GetStringSlice("logs.events") + if len(logEvents) != 0 { + for _, event := range logEvents { + logSources = append(logSources, "event://"+event) + } + } + if len(logSources) == 0 { errors.CheckErr("at least one log source must be enabled", formatType) } + node := viper.GetString("agent.node") labels := viper.GetStringSlice("labels") metrics := viper.GetBool("metrics.enable") metricsTargets := viper.GetStringSlice("metrics.targets") @@ -124,6 +140,7 @@ func parseFlags(formatType target.Format) *agent.RegisterData { MetricsTargets: metricsTargets, Profiles: profiles, Labels: labels, + Node: node, } return data diff --git a/cmd/completion/completion.go b/cmd/completion/completion.go index a95a11f..bd66866 100644 --- a/cmd/completion/completion.go +++ b/cmd/completion/completion.go @@ -23,3 +23,11 @@ func CompleteStackName(cmd *cobra.Command, args []string, toComplete string) ([] return stacks, cobra.ShellCompDirectiveNoFileComp } + +func CompleteNodeName(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + return []string{"unix", "windows"}, cobra.ShellCompDirectiveNoFileComp +} diff --git a/contrib/install-latest-alloy.ps1 b/contrib/install-latest-alloy.ps1 new file mode 100644 index 0000000..7731c05 --- /dev/null +++ b/contrib/install-latest-alloy.ps1 @@ -0,0 +1,36 @@ +$ErrorActionPreference = "Stop" +$installDir = "C:\Program Files\Alloy" +$githubRepo = "grafana/alloy" + +Write-Host "`nFetching latest Alloy release info from GitHub..." +$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$githubRepo/releases/latest" ` + -Headers @{ "User-Agent" = "PowerShell" } + +$asset = $release.assets | Where-Object { $_.name -eq "alloy-windows-amd64.exe.zip" } | Select-Object -First 1 + +if (-not $asset) { + Write-Error "Could not find alloy-windows-amd64.exe.zip in the latest release!" + exit 1 +} + +$tempZip = "$env:TEMP\alloy-latest.zip" +Write-Host "Downloading: $($asset.browser_download_url)" +Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $tempZip + +Write-Host "Extracting to $installDir ..." +if (!(Test-Path $installDir)) { + New-Item -ItemType Directory -Path $installDir | Out-Null +} +Expand-Archive -Path $tempZip -DestinationPath $installDir -Force + +$sysPath = [System.Environment]::GetEnvironmentVariable("Path", "Machine") +if ($sysPath -notmatch [regex]::Escape($installDir)) { + [System.Environment]::SetEnvironmentVariable("Path", "$sysPath;$installDir", "Machine") + Write-Host "Added $installDir to system PATH." +} else { + Write-Host "$installDir is already in PATH." +} + +Write-Host "`nAlloy binary is now installed at $installDir." +Write-Host "You can run it with: alloy.exe" +Write-Host "To start as service or autostart, please configure according to your environment." diff --git a/internal/agent/assets/com.github.tschaefer.finch.agent.plist b/internal/agent/assets/com.github.tschaefer.finch.agent.plist new file mode 100644 index 0000000..e4fb014 --- /dev/null +++ b/internal/agent/assets/com.github.tschaefer.finch.agent.plist @@ -0,0 +1,26 @@ + + + + + Label + com.github.tschaefer.finch.agent + ProgramArguments + + /usr/local/bin/alloy + run + --storage.path=/var/lib/alloy/data + --disable-reporting=true + /etc/alloy/alloy.config + + WorkingDirectory + /var/lib/alloy + RunAtLoad + + KeepAlive + + StandardOutPath + /var/log/alloy.log + StandardErrorPath + /var/log/alloy.log + + diff --git a/internal/agent/deploy.go b/internal/agent/deploy.go index 3089ee0..d6baff9 100644 --- a/internal/agent/deploy.go +++ b/internal/agent/deploy.go @@ -99,6 +99,32 @@ func (a *Agent) __deployCopyRcServiceFile() error { return nil } +func (a *Agent) __deployCopyLaunchdServiceFile() error { + dest := "/Library/LaunchDaemons/com.github.tschaefer.finch.agent.plist" + + content, err := fs.ReadFile(Assets, "com.github.tschaefer.finch.agent.plist") + if err != nil { + return &DeployAgentError{Message: err.Error(), Reason: ""} + } + + f, err := os.CreateTemp("", "com.github.tschaefer.finch.agent.plist") + if err != nil { + return &DeployAgentError{Message: err.Error(), Reason: ""} + } + defer func() { + _ = os.Remove(f.Name()) + }() + if _, err := f.Write(content); err != nil { + return &DeployAgentError{Message: err.Error(), Reason: ""} + } + + if err := a.target.Copy(f.Name(), dest, "444", "0:0"); err != nil { + return &DeployAgentError{Message: err.Error(), Reason: ""} + } + + return nil +} + func (a *Agent) __deployDownloadRelease(release string, version string, tmpdir string) (string, error) { var url string if version != "latest" { @@ -210,8 +236,17 @@ func (a *Agent) __deployUnzipRelease(release string, file string) (string, error return binary.Name(), nil } -func (a *Agent) __deployInstallBinary(binary string) error { - err := a.target.Copy(binary, "/usr/bin/alloy", "755", "0:0") +func (a *Agent) __deployInstallBinary(binary string, machine *MachineInfo) error { + path := "/usr/bin/alloy" + if machine.Kernel == "darwin" { + path = "/usr/local/bin/alloy" + out, err := a.target.Run("sudo mkdir -p " + filepath.Dir(path)) + if err != nil { + return &DeployAgentError{Message: err.Error(), Reason: string(out)} + } + } + + err := a.target.Copy(binary, path, "755", "0:0") if err != nil { return &DeployAgentError{Message: err.Error(), Reason: ""} } @@ -241,6 +276,20 @@ func (a *Agent) __deployEnableRcService() error { return nil } +func (a *Agent) __deployEnableLaunchdService() error { + out, err := a.target.Run("sudo launchctl bootstrap system /Library/LaunchDaemons/com.github.tschaefer.finch.agent.plist") + if err != nil { + return &DeployAgentError{Message: err.Error(), Reason: string(out)} + } + + out, err = a.target.Run("sudo launchctl kickstart -k system/com.github.tschaefer.finch.agent") + if err != nil { + return &DeployAgentError{Message: err.Error(), Reason: string(out)} + } + + return nil +} + func (a *Agent) __helperPrintProgress(message string) { username := "unknown" user, err := user.Current() @@ -279,7 +328,7 @@ func (a *Agent) deployAgent(machine *MachineInfo, alloyVersion string) error { return err } - if err := a.__deployInstallBinary(binary); err != nil { + if err := a.__deployInstallBinary(binary, machine); err != nil { return err } @@ -298,6 +347,13 @@ func (a *Agent) deployAgent(machine *MachineInfo, alloyVersion string) error { if err := a.__deployEnableRcService(); err != nil { return err } + case "darwin": + if err := a.__deployCopyLaunchdServiceFile(); err != nil { + return err + } + if err := a.__deployEnableLaunchdService(); err != nil { + return err + } default: // no-op } diff --git a/internal/agent/describe.go b/internal/agent/describe.go index 24e84fe..f2432a6 100644 --- a/internal/agent/describe.go +++ b/internal/agent/describe.go @@ -22,6 +22,7 @@ type DescribeLogsDocker struct { } type DescribeLogs struct { + Events []string `json:"events"` Files []string `json:"files"` Journal DescribeLogsJournal `json:"journal"` Docker DescribeLogsDocker `json:"docker"` @@ -39,6 +40,7 @@ type DescribeProfiles struct { type DescribeData struct { ResourceID string `json:"rid"` Hostname string `json:"hostname"` + Node string `json:"node"` Labels []string `json:"labels"` Logs DescribeLogs `json:"logs"` Metrics DescribeMetrics `json:"metrics"` @@ -67,6 +69,7 @@ func (a *Agent) describeAgent(service, rid string) (*DescribeData, error) { journal := false docker := false files := []string{} + events := []string{} for _, src := range data.LogSources { url, err := url.Parse(src) if err != nil { @@ -76,13 +79,13 @@ func (a *Agent) describeAgent(service, rid string) (*DescribeData, error) { switch url.Scheme { case "journal": journal = true - continue case "docker": docker = true - continue + case "file": + files = append(files, url.Path) + case "event": + events = append(events, url.Host) } - - files = append(files, url.Path) } labels := data.Labels @@ -98,9 +101,11 @@ func (a *Agent) describeAgent(service, rid string) (*DescribeData, error) { return &DescribeData{ ResourceID: data.ResourceId, Hostname: data.Hostname, + Node: data.Node, Labels: labels, Logs: DescribeLogs{ - Files: files, + Events: events, + Files: files, Journal: DescribeLogsJournal{ Enable: journal, }, diff --git a/internal/agent/machine.go b/internal/agent/machine.go index bbd5260..270a832 100644 --- a/internal/agent/machine.go +++ b/internal/agent/machine.go @@ -77,11 +77,10 @@ func (a *Agent) machineInfo() (*MachineInfo, error) { return nil, fmt.Errorf("unsupported target init system: %w", err) } case "darwin": - _, err = a.__machineGetDarwinArch(machine) + arch, err = a.__machineGetDarwinArch(machine) if err != nil { return nil, err } - return nil, fmt.Errorf("yet unsupported target kernel: %s", kernel) case "freebsd": arch, err = a.__machineGetFreebsdArch(machine) if err != nil { diff --git a/internal/agent/register.go b/internal/agent/register.go index 944b461..4e3bee6 100644 --- a/internal/agent/register.go +++ b/internal/agent/register.go @@ -19,6 +19,7 @@ type RegisterData struct { MetricsTargets []string `json:"metrics_targets"` Profiles bool `json:"profiles"` Labels []string `json:"labels"` + Node string `json:"node"` } func (a *Agent) registerAgent(service string, data *RegisterData) ([]byte, error) { @@ -40,6 +41,7 @@ func (a *Agent) registerAgent(service string, data *RegisterData) ([]byte, error MetricsTargets: data.MetricsTargets, Profiles: data.Profiles, Labels: data.Labels, + Node: data.Node, }) if err != nil { return nil, &RegisterAgentError{Message: err.Error(), Reason: ""} diff --git a/internal/agent/teardown.go b/internal/agent/teardown.go index 9fb061c..2c8c10a 100644 --- a/internal/agent/teardown.go +++ b/internal/agent/teardown.go @@ -42,19 +42,35 @@ func (a *Agent) __teardownRcService() error { return nil } +func (a *Agent) __teardownLaunchdService() error { + out, err := a.target.Run("sudo launchctl bootout system/com.github.tschaefer.finch.agent || true") + if err != nil { + return &TeardownAgentError{Message: err.Error(), Reason: string(out)} + } + + out, err = a.target.Run("sudo rm -f /Library/LaunchDaemons/com.github.tschaefer.finch.agent.plist") + if err != nil { + return &TeardownAgentError{Message: err.Error(), Reason: string(out)} + } + + return nil +} + func (a *Agent) teardownAgent(machine *MachineInfo) error { + var err error switch machine.Kernel { case "linux": - if err := a.__teardownSystemdService(); err != nil { - return err - } + err = a.__teardownSystemdService() case "freebsd": - if err := a.__teardownRcService(); err != nil { - return err - } + err = a.__teardownRcService() + case "darwin": + err = a.__teardownLaunchdService() default: // no-op } + if err != nil { + return err + } out, err := a.target.Run("sudo rm -rf /etc/alloy") if err != nil { @@ -66,7 +82,12 @@ func (a *Agent) teardownAgent(machine *MachineInfo) error { return &TeardownAgentError{Message: err.Error(), Reason: string(out)} } - out, err = a.target.Run("sudo rm -f /usr/bin/alloy") + path := "/usr/bin/alloy" + if machine.Kernel == "darwin" { + path = "/usr/local/bin/alloy" + } + + out, err = a.target.Run("sudo rm -f " + path) if err != nil { return &TeardownAgentError{Message: err.Error(), Reason: string(out)} } diff --git a/internal/agent/update.go b/internal/agent/update.go index 4de7682..ab824e8 100644 --- a/internal/agent/update.go +++ b/internal/agent/update.go @@ -44,7 +44,7 @@ func (a *Agent) __updateServiceBinaryGetLatestTag() (string, error) { return data.(map[string]any)["tag_name"].(string), nil } -func (a *Agent) __updateServiceBinaryIsNeeded(version string) (bool, error) { +func (a *Agent) __updateServiceBinaryIsNeeded(version string, machine *MachineInfo) (bool, error) { if a.dryRun { target.PrintProgress( fmt.Sprintf("Skipping Alloy update check for version '%s' due to dry-run mode", version), @@ -62,7 +62,12 @@ func (a *Agent) __updateServiceBinaryIsNeeded(version string) (bool, error) { } } - out, err := a.target.Run("alloy --version | grep -o -E 'v[0-9\\.]+'") + path := "/usr/bin/alloy" + if machine.Kernel == "darwin" { + path = "/usr/local/bin/alloy" + } + + out, err := a.target.Run(path + " --version | grep -o -E 'v[0-9\\.]+'") if err != nil { return false, &UpdateAgentError{Message: err.Error(), Reason: string(out)} } @@ -72,7 +77,7 @@ func (a *Agent) __updateServiceBinaryIsNeeded(version string) (bool, error) { } func (a *Agent) __updateServiceBinary(machine *MachineInfo, version string) error { - ok, err := a.__updateServiceBinaryIsNeeded(version) + ok, err := a.__updateServiceBinaryIsNeeded(version, machine) if err != nil { return err } @@ -100,7 +105,7 @@ func (a *Agent) __updateServiceBinary(machine *MachineInfo, version string) erro return convertError(err, &UpdateAgentError{}) } - if err := a.__deployInstallBinary(binary); err != nil { + if err := a.__deployInstallBinary(binary, machine); err != nil { return convertError(err, &UpdateAgentError{}) } @@ -131,6 +136,11 @@ func (a *Agent) updateAgent(machine *MachineInfo, skipConfig bool, skipBinaries if err != nil { return &UpdateAgentError{Message: err.Error(), Reason: string(out)} } + case "darwin": + out, err := a.target.Run("sudo launchctl kickstart -k system/com.github.tschaefer.finch.agent") + if err != nil { + return &UpdateAgentError{Message: err.Error(), Reason: string(out)} + } default: // no-op } diff --git a/internal/api/api.pb.go b/internal/api/api.pb.go index a25b546..a6142fa 100644 --- a/internal/api/api.pb.go +++ b/internal/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_internal_api_api_proto protoreflect.FileDescriptor const file_internal_api_api_proto_rawDesc = "" + "\n" + - "\x16internal/api/api.proto\x12\x05finch\"\xca\x01\n" + + "\x16internal/api/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_internal_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_internal_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/internal/api/api.proto b/internal/api/api.proto index 5fe998a..ba38082 100644 --- a/internal/api/api.proto +++ b/internal/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 {}