Files
Olares/cli/cmd/ctl/os/logs.go
Eliott van Nuffel df38148149
Some checks failed
Native Verification / Build Docs (pull_request) Successful in 1m12s
Native Verification / Build App-Service Native (pull_request) Successful in 1m27s
Native Verification / Build Daemon Native (pull_request) Successful in 42s
Lint and Test Charts / lint-test (pull_request_target) Failing after 19s
Lint and Test Charts / test-version (pull_request_target) Successful in 0s
Lint and Test Charts / push-image (pull_request_target) Failing after 15s
Lint and Test Charts / upload-cli (pull_request_target) Failing after 1m15s
Lint and Test Charts / upload-daemon (pull_request_target) Failing after 1m12s
Lint and Test Charts / push-deps (pull_request_target) Has been skipped
Lint and Test Charts / push-deps-arm64 (pull_request_target) Has been skipped
Lint and Test Charts / push-image-arm64 (pull_request_target) Has been cancelled
Lint and Test Charts / upload-package (pull_request_target) Has been cancelled
Lint and Test Charts / install-test (pull_request_target) Has been cancelled
continue beOS rebrand and add native verification
2026-03-10 13:48:45 +01:00

684 lines
20 KiB
Go

package os
import (
"archive/tar"
"compress/gzip"
"context"
"fmt"
"io"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"github.com/beclab/beos/cli/pkg/common"
"github.com/beclab/beos/cli/pkg/core/util"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// LogCollectOptions holds options for collecting logs
type LogCollectOptions struct {
// Time duration to collect logs for (empty means all available logs)
Since string
// Maximum number of lines to collect per log source
MaxLines int
// Output directory for collected logs
OutputDir string
// Components to collect logs from (empty means all)
Components []string
// Whether to ignore errors from kubectl commands
IgnoreKubeErrors bool
// Skip retrieving logs from kube-apiserver
SkipKubeAPISserver bool
}
var servicesToCollectLogs = []string{"k3s", "containerd", "beosd", "olaresd", "kubelet", "juicefs", "redis", "minio", "etcd", "NetworkManager"}
// setSkipIfK8sNotReachable checks if the Kubernetes API server port is reachable
// and automatically sets skip-kube-apiserver to true if not reachable
func setSkipIfK8sNotReachable(options *LogCollectOptions) {
// if the env is not set explicitly by user
// fallback to k3s config path as it's a non-standard path
if os.Getenv(clientcmd.RecommendedConfigPathEnvVar) == "" {
os.Setenv(clientcmd.RecommendedConfigPathEnvVar, "/etc/rancher/k3s/k3s.yaml")
}
config, err := ctrl.GetConfig()
if err != nil {
fmt.Printf("Warning: failed to get kubeconfig: %v\n", err)
fmt.Println("Automatically setting skip-kube-apiserver option")
options.SkipKubeAPISserver = true
return
}
url, _, err := rest.DefaultServerUrlFor(config)
if err != nil {
fmt.Printf("Warning: failed to parse server url in kubeconfig: %v\n", err)
fmt.Println("Automatically setting skip-kube-apiserver option")
options.SkipKubeAPISserver = true
return
}
timeout := config.Timeout
if timeout == 0 {
timeout = 5 * time.Second
}
conn, err := net.DialTimeout("tcp", url.Host, timeout)
if err != nil {
fmt.Printf("Warning: Kubernetes API server at %s is not reachable: %v\n", config.Host, err)
fmt.Println("Automatically setting skip-kube-apiserver option")
options.SkipKubeAPISserver = true
return
}
conn.Close()
}
func collectLogs(options *LogCollectOptions) error {
if os.Getuid() != 0 {
return fmt.Errorf("os: please run as root")
}
if !options.SkipKubeAPISserver {
setSkipIfK8sNotReachable(options)
}
if err := os.MkdirAll(options.OutputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %v", err)
}
timestamp := time.Now().Format("20060102-150405")
archiveName := filepath.Join(options.OutputDir, fmt.Sprintf("olares-logs-%s.tar.gz", timestamp))
archive, err := os.Create(archiveName)
if err != nil {
return fmt.Errorf("failed to create archive: %v", err)
}
defer archive.Close()
gw := gzip.NewWriter(archive)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
// collect systemd service logs
if err := collectSystemdLogs(tw, options); err != nil {
return fmt.Errorf("failed to collect systemd logs: %v", err)
}
fmt.Println("collecting dmesg logs ...")
if err := collectDmesgLogs(tw, options); err != nil {
return fmt.Errorf("failed to collect dmesg logs: %v", err)
}
fmt.Println("collecting logs from kubernetes cluster...")
if err := collectKubernetesLogs(tw, options); err != nil {
return fmt.Errorf("failed to collect kubernetes logs: %v", err)
}
fmt.Println("collecting beos-cli logs...")
if err := collectBeOSCLILogs(tw, options); err != nil {
return fmt.Errorf("failed to collect BeOSCLI logs: %v", err)
}
fmt.Println("collecting network configs...")
if err := collectNetworkConfigs(tw, options); err != nil {
return fmt.Errorf("failed to collect network configs: %v", err)
}
fmt.Printf("logs have been collected and archived in: %s\n", archiveName)
return nil
}
func collectBeOSCLILogs(tw *tar.Writer, options *LogCollectOptions) error {
basedir, err := getBaseDir()
if err != nil {
return err
}
cliLogDir := filepath.Join(basedir, "logs")
if _, err := os.Stat(cliLogDir); err != nil {
fmt.Printf("warning: directory %s does not exist, skipping collecting beos-cli logs\n", cliLogDir)
return nil
}
err = filepath.Walk(cliLogDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
srcFile, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open file %s: %v", path, err)
}
defer srcFile.Close()
relPath, err := filepath.Rel(cliLogDir, path)
if err != nil {
return fmt.Errorf("failed to get relative path: %v", err)
}
header := &tar.Header{
Name: filepath.Join("beos-cli", relPath),
Mode: 0644,
Size: info.Size(),
ModTime: info.ModTime(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write header for %s: %v", path, err)
}
// stream file contents to tar
if _, err := io.CopyN(tw, srcFile, header.Size); err != nil {
return fmt.Errorf("failed to write data for %s: %v", path, err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to collect beos-cli logs from %s: %v", cliLogDir, err)
}
return nil
}
func collectSystemdLogs(tw *tar.Writer, options *LogCollectOptions) error {
// Create temp directory for log files
tempDir, err := os.MkdirTemp("", "olares-logs-*")
if err != nil {
return fmt.Errorf("failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
var services []string
if len(options.Components) > 0 {
services = options.Components
} else {
services = servicesToCollectLogs
}
for _, service := range services {
if !checkServiceExists(service) {
if len(options.Components) > 0 {
fmt.Printf("warning: required service %s not found\n", service)
}
continue
}
fmt.Printf("collecting logs for service: %s\n", service)
// create temp file for this service's logs
tempFile := filepath.Join(tempDir, fmt.Sprintf("%s.log", service))
logFile, err := os.Create(tempFile)
if err != nil {
return fmt.Errorf("failed to create temp file for %s: %v", service, err)
}
args := []string{"-u", service}
if options.Since != "" {
if !strings.HasPrefix(options.Since, "-") {
options.Since = "-" + options.Since
}
args = append(args, "--since", options.Since)
}
if options.MaxLines > 0 {
args = append(args, "-n", fmt.Sprintf("%d", options.MaxLines))
}
if options.Since != "" && options.MaxLines > 0 {
// this is a journalctl bug
// where -S and -n combined results in the latest logs truncated
// rather than the old logs
// a -r corrects the truncate behavior
args = append(args, "-r")
}
// execute journalctl and write directly to temp file
// don't just use the command output because that's too memory-consuming
// the same logic goes to the os.Open and io.Copy rather than os.ReadFile
cmd := exec.Command("journalctl", args...)
cmd.Stdout = logFile
if err := cmd.Run(); err != nil {
logFile.Close()
return fmt.Errorf("failed to collect logs for %s: %v", service, err)
}
logFile.Close()
// get file info for the tar header
fi, err := os.Stat(tempFile)
if err != nil {
return fmt.Errorf("failed to stat temp file for %s: %v", service, err)
}
logFile, err = os.Open(tempFile)
if err != nil {
return fmt.Errorf("failed to open temp file for %s: %v", service, err)
}
defer logFile.Close()
header := &tar.Header{
Name: fmt.Sprintf("%s.log", service),
Mode: 0644,
Size: fi.Size(),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write header for %s: %v", service, err)
}
if _, err := io.CopyN(tw, logFile, header.Size); err != nil {
return fmt.Errorf("failed to write logs for %s: %v", service, err)
}
}
return nil
}
func collectDmesgLogs(tw *tar.Writer, options *LogCollectOptions) error {
cmd := exec.Command("dmesg", "-T")
output, err := cmd.Output()
if err != nil {
return err
}
header := &tar.Header{
Name: "dmesg.log",
Mode: 0644,
Size: int64(len(output)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write dmesg header: %v", err)
}
if _, err := tw.Write(output); err != nil {
return fmt.Errorf("failed to write dmesg data: %v", err)
}
return nil
}
func collectKubernetesLogs(tw *tar.Writer, options *LogCollectOptions) error {
podsLogDir := "/var/log/pods"
if _, err := os.Stat(podsLogDir); err != nil {
fmt.Printf("warning: directory %s does not exist, skipping collecting pod logs\n", podsLogDir)
} else {
err := filepath.Walk(podsLogDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
srcFile, err := os.Open(path)
if err != nil {
return fmt.Errorf("failed to open file %s: %v", path, err)
}
defer srcFile.Close()
relPath, err := filepath.Rel(podsLogDir, path)
if err != nil {
return fmt.Errorf("failed to get relative path: %v", err)
}
header := &tar.Header{
Name: filepath.Join("pods", relPath),
Mode: 0644,
Size: info.Size(),
ModTime: info.ModTime(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write header for %s: %v", path, err)
}
// stream file contents to tar
if _, err := io.CopyN(tw, srcFile, header.Size); err != nil {
return fmt.Errorf("failed to write data for %s: %v", path, err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to collect pod logs from /var/log/pods: %v", err)
}
}
if options.SkipKubeAPISserver {
return nil
}
if _, err := util.GetCommand("kubectl"); err != nil {
fmt.Printf("warning: kubectl not found, skipping collecting cluster info from kube-apiserver\n")
return nil
}
var cmd *exec.Cmd
var output []byte
var err error
cmd = exec.Command("kubectl", "get", "pods", "--all-namespaces", "-o", "wide")
output, err = tryKubectlCommand(cmd, "get pods", options)
if err != nil && !options.IgnoreKubeErrors {
return err
}
if err == nil {
header := &tar.Header{
Name: "pods-list.txt",
Mode: 0644,
Size: int64(len(output)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write pods list header: %v", err)
}
if _, err := tw.Write(output); err != nil {
return fmt.Errorf("failed to write pods list data: %v", err)
}
}
resourceTypes := []string{"node", "pod", "statefulset", "deployment", "replicaset", "service", "configmap"}
for _, res := range resourceTypes {
cmd = exec.Command("kubectl", "describe", res, "--all-namespaces")
output, err = tryKubectlCommand(cmd, fmt.Sprintf("describe %s", res), options)
if err != nil && !options.IgnoreKubeErrors {
return err
}
if err == nil {
header := &tar.Header{
Name: fmt.Sprintf("%s-describe.txt", res),
Mode: 0644,
Size: int64(len(output)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write %s description header: %v", res, err)
}
if _, err := tw.Write(output); err != nil {
return fmt.Errorf("failed to write %s description data: %v", res, err)
}
}
}
if err := collectNginxLogsFromLabeledPods(tw); err != nil {
if !options.IgnoreKubeErrors {
return fmt.Errorf("failed to collect nginx logs from labeled pods: %v", err)
}
}
return nil
}
func collectNginxLogsFromLabeledPods(tw *tar.Writer) error {
if _, err := util.GetCommand("kubectl"); err != nil {
fmt.Printf("warning: kubectl not found, skipping collecting nginx logs from labeled pods\n")
return nil
}
cfg, err := ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %v", err)
}
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("failed to create kube client: %v", err)
}
type selectorSpec struct {
LabelSelector string
ContainerName string
}
selectors := []selectorSpec{
{LabelSelector: "app=l4-bfl-proxy", ContainerName: ""},
{LabelSelector: "tier=bfl", ContainerName: "ingress"},
}
type targetPod struct {
Namespace string
Name string
ContainerName string
}
var targets []targetPod
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for _, sel := range selectors {
podList, err := clientset.CoreV1().Pods(corev1.NamespaceAll).List(ctx, metav1.ListOptions{LabelSelector: sel.LabelSelector})
if err != nil {
return fmt.Errorf("failed to list pods by label %q: %v", sel.LabelSelector, err)
}
for _, pod := range podList.Items {
targets = append(targets, targetPod{
Namespace: pod.Namespace,
Name: pod.Name,
ContainerName: sel.ContainerName,
})
}
}
if len(targets) == 0 {
return nil
}
// simplest approach: use kubectl cp (it already implements copy via tar over exec)
tempDir, err := os.MkdirTemp("", "olares-nginx-logs-*")
if err != nil {
return fmt.Errorf("failed to create temp directory for nginx logs: %v", err)
}
defer os.RemoveAll(tempDir)
files := []string{"/var/log/nginx/access.log", "/var/log/nginx/error.log"}
for _, target := range targets {
for _, remotePath := range files {
base := filepath.Base(remotePath)
archivePath := filepath.Join("nginx", target.Namespace, target.Name, base)
dest := filepath.Join(tempDir, fmt.Sprintf("%s__%s__%s", target.Namespace, target.Name, base))
err := kubectlCopyFile(target.Namespace, target.Name, target.ContainerName, remotePath, dest)
if err != nil {
return fmt.Errorf("failed to kubectl cp %s/%s:%s: %v", target.Namespace, target.Name, remotePath, err)
}
fi, err := os.Stat(dest)
if err != nil {
return fmt.Errorf("failed to stat copied nginx log %s: %v", dest, err)
}
f, err := os.Open(dest)
if err != nil {
return fmt.Errorf("failed to open copied nginx log %s: %v", dest, err)
}
defer f.Close()
header := &tar.Header{
Name: archivePath,
Mode: 0644,
Size: fi.Size(),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write header for %s: %v", archivePath, err)
}
if _, err := io.CopyN(tw, f, header.Size); err != nil {
return fmt.Errorf("failed to write data for %s: %v", archivePath, err)
}
}
}
return nil
}
func kubectlCopyFile(namespace, pod, container, remotePath, destPath string) error {
args := []string{"-n", namespace, "cp"}
if container != "" {
args = append(args, "-c", container)
}
args = append(args, fmt.Sprintf("%s:%s", pod, remotePath), destPath)
cmd := exec.Command("kubectl", args...)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("kubectl %s failed: %v, output: %s", strings.Join(args, " "), err, strings.TrimSpace(string(out)))
}
return nil
}
func collectNetworkConfigs(tw *tar.Writer, options *LogCollectOptions) error {
if _, err := util.GetCommand("ip"); err == nil {
cmd := exec.Command("ip", "address")
output, err := cmd.Output()
if err != nil {
return err
}
header := &tar.Header{
Name: "ip-address.txt",
Mode: 0644,
Size: int64(len(output)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write ip address header: %v", err)
}
if _, err := tw.Write(output); err != nil {
return fmt.Errorf("failed to write ip address data: %v", err)
}
cmd = exec.Command("ip", "route")
output, err = cmd.Output()
if err != nil {
return err
}
header = &tar.Header{
Name: "ip-route.txt",
Mode: 0644,
Size: int64(len(output)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write ip route header: %v", err)
}
if _, err := tw.Write(output); err != nil {
return fmt.Errorf("failed to write ip route data: %v", err)
}
}
if _, err := util.GetCommand("iptables-save"); err == nil {
cmd := exec.Command("iptables-save")
output, err := cmd.Output()
if err != nil {
return err
}
header := &tar.Header{
Name: "iptables.txt",
Mode: 0644,
Size: int64(len(output)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write iptables header: %v", err)
}
if _, err := tw.Write(output); err != nil {
return fmt.Errorf("failed to write iptables data: %v", err)
}
}
if _, err := util.GetCommand("nft"); err == nil {
cmd := exec.Command("nft", "list", "ruleset")
output, err := cmd.Output()
if err != nil {
return err
}
header := &tar.Header{
Name: "nftables.txt",
Mode: 0644,
Size: int64(len(output)),
ModTime: time.Now(),
}
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("failed to write nftables header: %v", err)
}
if _, err := tw.Write(output); err != nil {
return fmt.Errorf("failed to write nftables data: %v", err)
}
}
return nil
}
func getBaseDir() (string, error) {
basedir := viper.GetString(common.FlagBaseDir)
if basedir != "" {
return basedir, nil
}
homeDir, err := util.Home()
if err != nil {
return "", fmt.Errorf("failed to get home dir: %v", err)
}
return filepath.Join(homeDir, ".olares"), nil
}
func tryKubectlCommand(cmd *exec.Cmd, description string, options *LogCollectOptions) ([]byte, error) {
output, err := cmd.Output()
if err != nil {
if options.IgnoreKubeErrors {
fmt.Printf("warning: failed to %s: %v\n", description, err)
return nil, err
}
return nil, fmt.Errorf("failed to %s: %v", description, err)
}
return output, nil
}
// checkService verifies if a systemd service exists
func checkServiceExists(service string) bool {
if !strings.HasSuffix(service, ".service") {
service += ".service"
}
cmd := exec.Command("systemctl", "list-unit-files", "--no-legend", service)
return cmd.Run() == nil
}
func NewCmdLogs() *cobra.Command {
options := &LogCollectOptions{
Since: "7d",
MaxLines: 20000,
OutputDir: "./olares-logs",
IgnoreKubeErrors: false,
}
cmd := &cobra.Command{
Use: "logs",
Short: "Collect logs from all beOS Pro system components",
Long: `Collect logs from various beOS Pro system components, that may or may not be installed on this machine, including:
- K3s/Kubelet logs
- Containerd logs
- JuiceFS logs
- Redis logs
- MinIO logs
- etcd logs
- BeOSd logs
- beos-cli logs
- network configurations
- Kubernetes pod info and logs
- Kubernetes node info`,
Run: func(cmd *cobra.Command, args []string) {
if err := collectLogs(options); err != nil {
log.Fatalf("error: %v", err)
}
},
}
cmd.Flags().StringVar(&options.Since, "since", options.Since, "Only return logs newer than a relative duration like 5s, 2m, or 3h, to limit the log file")
cmd.Flags().IntVar(&options.MaxLines, "max-lines", options.MaxLines, "Maximum number of lines to collect per log source, to limit the log file size")
cmd.Flags().StringVar(&options.OutputDir, "output-dir", options.OutputDir, "Directory to store collected logs, will be created if not existing")
cmd.Flags().StringSliceVar(&options.Components, "components", nil, "Specific components (systemd service) to collect logs from (comma-separated). If empty, collects from all beOS-related components that can be found")
cmd.Flags().BoolVar(&options.IgnoreKubeErrors, "ignore-kube-errors", options.IgnoreKubeErrors, "Continue collecting logs even if kubectl commands fail")
cmd.Flags().BoolVar(&options.SkipKubeAPISserver, "skip-kube-apiserver", options.SkipKubeAPISserver, "Skip retrieving logs from kube-apiserver, it's automatically set if apiserver is not reachable. To tolerate other cases, set the ignore-kube-errors")
return cmd
}