/* Copyright © 2026 Thomas von Dein This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package es import ( "context" "fmt" "log/slog" "strconv" "strings" "time" "codeberg.org/scip/esctl/pkg/cfg" "codeberg.org/scip/esctl/pkg/printer" "github.com/dustin/go-humanize" ) func NodeList(conf *cfg.Config) error { // get nodes nodes, err := conf.DefaultCluster.ES().Cat.Nodes().Do(context.Background()) if err != nil { return fmt.Errorf("failed to get nodes: %w", esErrorString(err)) } slog.Debug("ES result", "nodes", nodes) table := printer.NewTable(conf, 7, len(nodes)) table.Addheaders("name", "ip", "load1m", "load5m", "load15m", "ram %", "heap %") for idx, node := range nodes { table.Entries[idx] = []any{ *node.Name, *node.Ip, *node.Load1M, *node.Load5M, *node.Load15M, node.RamPercent, node.HeapPercent, } } table.Sort() if err := table.Print(); err != nil { return err } return nil } func NodeNames(conf *cfg.Config) ([]string, error) { nodes, err := conf.DefaultCluster.ES().Cat.Nodes(). Do(context.Background()) if err != nil { return nil, fmt.Errorf("failed to get nodes: %w", esErrorString(err)) } slog.Debug("ES result", "nodes", nodes) nodelist := make([]string, len(nodes)) for idx, node := range nodes { nodelist[idx] = *node.Name } return nodelist, err } // FIXME: adding the settings metric leads to json unmarshall error: // https://github.com/elastic/go-elasticsearch/issues/1524 func NodeShow(conf *cfg.Config, nodename string) error { res, err := conf.DefaultCluster.ES().Nodes.Info(). NodeId(nodename). Metric("os, jvm, thread_pool, remote_cluster_server"). Do(context.Background()) if err != nil { return fmt.Errorf("failed to get node info: %w", esErrorString(err)) } slog.Debug("ES result", "node", res) stats, err := conf.DefaultCluster.ES().Nodes.Stats(). NodeId(nodename). Do(context.Background()) if err != nil { return fmt.Errorf("failed to get node stats: %w", esErrorString(err)) } slog.Debug("ES result", "stat", stats) for id, info := range res.Nodes { stat := stats.Nodes[id] table := printer.NewTable(conf, 2, 0) table.Addheaders(nodename+" property", "value") roles := make([]string, len(info.Roles)) for idx, role := range info.Roles { roles[idx] = role.Name } k8snode := info.Attributes["k8s_node_name"] table.Entries = [][]any{ {"Id", id}, {"Name", nodename}, {"Kubernetes node", k8snode}, {"Ip address", info.Ip}, {"Node rank", *stat.AdaptiveSelection[id].Rank}, {"JVM", info.Jvm.VmName + " " + info.Jvm.Version}, {"JVM Started", time.UnixMilli(info.Jvm.StartTimeInMillis)}, {"OS", info.Os.PrettyName + " " + info.Os.Version}, {"Node roles", roles}, {"Node version", info.Version}, {"HTTP clients", *stat.Http.CurrentOpen}, {"CPUs", *info.Os.AllocatedProcessors}, {"Load 15m/5m/1m", fmt.Sprintf("%.2f/%.2f/%.2f", stat.Os.Cpu.LoadAverage["15m"], stat.Os.Cpu.LoadAverage["5m"], stat.Os.Cpu.LoadAverage["1m"], )}, {"Open FD's", *stat.Process.OpenFileDescriptors}, {"HTTP sesssions current/total", fmt.Sprintf("%d/%d", *stat.Http.CurrentOpen, *stat.Http.TotalOpened, )}, {"Traffic rx/tx", printer.Bytes(*stat.Transport.RxSizeInBytes).String() + " / " + printer.Bytes(*stat.Transport.TxSizeInBytes).String()}, {"Response time avg", time.Duration(*stat.AdaptiveSelection[id].AvgResponseTimeNs)}, {"Memory usage (used/avail)", humanize.Bytes(uint64(*stat.Os.Mem.UsedInBytes)) + " / " + humanize.Bytes(uint64(*stat.Os.Mem.TotalInBytes))}, {"Search queries current/total", fmt.Sprintf("%d/%d", stat.Indices.Search.QueryCurrent, stat.Indices.Search.QueryTotal, )}, {"Search efficiency", stat.Indices.Search.QueryTimeInMillis / stat.Indices.Search.QueryTotal}, {"Docs count", stat.Indices.Docs.Count}, {"Merges current/total", fmt.Sprintf("%d/%d", stat.Indices.Merges.Current, stat.Indices.Merges.Total, )}, {"Merge docs count current/total", fmt.Sprintf("%d/%d", stat.Indices.Merges.CurrentDocs, stat.Indices.Merges.TotalDocs, )}, {"Merge size current/total", fmt.Sprintf("%s/%s", printer.Bytes(stat.Indices.Merges.CurrentSizeInBytes).String(), printer.Bytes(stat.Indices.Merges.TotalSizeInBytes).String(), )}, {"CircuitBreaker trip count", *stat.Breakers["fielddata"].Tripped}, } if len(stat.Fs.Data) > 0 { fs := stat.Fs.Data[0] table.AddRow("Storage usage (used/avail)", printer.Bytes(*fs.AvailableInBytes).String()+" / "+printer.Bytes(*fs.TotalInBytes).String()) table.AddRow("Storage mount", *fs.Mount) } if err := table.Print(); err != nil { return err } } return nil } func NodeClients(conf *cfg.Config, nodename string) error { stats, err := conf.DefaultCluster.ES().Nodes.Stats(). NodeId(nodename). Do(context.Background()) if err != nil { return fmt.Errorf("failed to get node stats: %w", esErrorString(err)) } slog.Debug("ES result", "stat", stats) table := printer.NewTable(conf, 5, 0) table.Addheaders("agent", "id", "when", "from host", "url") for _, stat := range stats.Nodes { for _, client := range stat.Http.Clients { if client.ClosedTimeMillis == nil { agent := "" if client.Agent != nil { agent = *client.Agent } uri := "" if client.LastUri != nil { uri = *client.LastUri if !conf.All { parts := strings.Split(uri, "?") uri = parts[0] } } table.AddRow( agent, strconv.FormatInt(*client.Id, 10), time.UnixMilli(*client.LastRequestTimeMillis).String(), *client.RemoteAddress, uri, ) } } break } table.Sort() return table.Print() }