github.com/netdata/go.d.plugin@v0.58.1/agent/discovery/sd/hostsocket/net.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package hostsocket
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"log/slog"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/netdata/go.d.plugin/agent/discovery/sd/model"
    19  	"github.com/netdata/go.d.plugin/logger"
    20  
    21  	"github.com/ilyam8/hashstructure"
    22  )
    23  
    24  type netSocketTargetGroup struct {
    25  	provider string
    26  	source   string
    27  	targets  []model.Target
    28  }
    29  
    30  func (g *netSocketTargetGroup) Provider() string        { return g.provider }
    31  func (g *netSocketTargetGroup) Source() string          { return g.source }
    32  func (g *netSocketTargetGroup) Targets() []model.Target { return g.targets }
    33  
    34  type NetSocketTarget struct {
    35  	model.Base
    36  
    37  	hash uint64
    38  
    39  	Protocol string
    40  	Address  string
    41  	Port     string
    42  	Comm     string
    43  	Cmdline  string
    44  }
    45  
    46  func (t *NetSocketTarget) TUID() string { return t.tuid() }
    47  func (t *NetSocketTarget) Hash() uint64 { return t.hash }
    48  func (t *NetSocketTarget) tuid() string {
    49  	return fmt.Sprintf("%s_%s_%d", strings.ToLower(t.Protocol), t.Port, t.hash)
    50  }
    51  
    52  func NewNetSocketDiscoverer(cfg NetworkSocketConfig) (*NetDiscoverer, error) {
    53  	tags, err := model.ParseTags(cfg.Tags)
    54  	if err != nil {
    55  		return nil, fmt.Errorf("parse tags: %v", err)
    56  	}
    57  
    58  	dir := os.Getenv("NETDATA_PLUGINS_DIR")
    59  	if dir == "" {
    60  		dir, _ = os.Getwd()
    61  	}
    62  
    63  	d := &NetDiscoverer{
    64  		Logger: logger.New().With(
    65  			slog.String("component", "discovery sd hostsocket"),
    66  		),
    67  		interval: time.Second * 60,
    68  		ll: &localListenersExec{
    69  			binPath: filepath.Join(dir, "local-listeners"),
    70  			timeout: time.Second * 5,
    71  		},
    72  	}
    73  	d.Tags().Merge(tags)
    74  
    75  	return d, nil
    76  }
    77  
    78  type (
    79  	NetDiscoverer struct {
    80  		*logger.Logger
    81  		model.Base
    82  
    83  		interval time.Duration
    84  		ll       localListeners
    85  	}
    86  	localListeners interface {
    87  		discover(ctx context.Context) ([]byte, error)
    88  	}
    89  )
    90  
    91  func (d *NetDiscoverer) Discover(ctx context.Context, in chan<- []model.TargetGroup) {
    92  	if err := d.discoverLocalListeners(ctx, in); err != nil {
    93  		d.Error(err)
    94  		return
    95  	}
    96  
    97  	tk := time.NewTicker(d.interval)
    98  	defer tk.Stop()
    99  
   100  	for {
   101  		select {
   102  		case <-ctx.Done():
   103  			return
   104  		case <-tk.C:
   105  			if err := d.discoverLocalListeners(ctx, in); err != nil {
   106  				d.Error(err)
   107  				return
   108  			}
   109  		}
   110  	}
   111  }
   112  
   113  func (d *NetDiscoverer) discoverLocalListeners(ctx context.Context, in chan<- []model.TargetGroup) error {
   114  	bs, err := d.ll.discover(ctx)
   115  	if err != nil {
   116  		if errors.Is(err, context.Canceled) {
   117  			return nil
   118  		}
   119  		return err
   120  	}
   121  
   122  	tggs, err := d.parseLocalListeners(bs)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	select {
   128  	case <-ctx.Done():
   129  	case in <- tggs:
   130  	}
   131  	return nil
   132  }
   133  
   134  func (d *NetDiscoverer) parseLocalListeners(bs []byte) ([]model.TargetGroup, error) {
   135  	var tgts []model.Target
   136  
   137  	sc := bufio.NewScanner(bytes.NewReader(bs))
   138  	for sc.Scan() {
   139  		text := strings.TrimSpace(sc.Text())
   140  		if text == "" {
   141  			continue
   142  		}
   143  
   144  		// Protocol|Address|Port|Cmdline
   145  		parts := strings.SplitN(text, "|", 4)
   146  		if len(parts) != 4 {
   147  			return nil, fmt.Errorf("unexpected data: '%s'", text)
   148  		}
   149  
   150  		tgt := NetSocketTarget{
   151  			Protocol: parts[0],
   152  			Address:  parts[1],
   153  			Port:     parts[2],
   154  			Comm:     extractComm(parts[3]),
   155  			Cmdline:  parts[3],
   156  		}
   157  
   158  		hash, err := calcHash(tgt)
   159  		if err != nil {
   160  			continue
   161  		}
   162  
   163  		tgt.hash = hash
   164  		tgt.Tags().Merge(d.Tags())
   165  
   166  		tgts = append(tgts, &tgt)
   167  	}
   168  
   169  	tgg := &netSocketTargetGroup{
   170  		provider: "hostsocket",
   171  		source:   "net",
   172  		targets:  tgts,
   173  	}
   174  
   175  	return []model.TargetGroup{tgg}, nil
   176  }
   177  
   178  type localListenersExec struct {
   179  	binPath string
   180  	timeout time.Duration
   181  }
   182  
   183  func (e *localListenersExec) discover(ctx context.Context) ([]byte, error) {
   184  	execCtx, cancel := context.WithTimeout(ctx, e.timeout)
   185  	defer cancel()
   186  
   187  	cmd := exec.CommandContext(execCtx, e.binPath, "tcp") // TODO: tcp6?
   188  
   189  	bs, err := cmd.Output()
   190  	if err != nil {
   191  		return nil, fmt.Errorf("error on '%s': %v", cmd, err)
   192  	}
   193  
   194  	return bs, nil
   195  }
   196  
   197  func extractComm(s string) string {
   198  	i := strings.IndexByte(s, ' ')
   199  	if i <= 0 {
   200  		return ""
   201  	}
   202  	_, comm := filepath.Split(s[:i])
   203  	return comm
   204  }
   205  
   206  func calcHash(obj any) (uint64, error) {
   207  	return hashstructure.Hash(obj, nil)
   208  }