From 09adec7395366698bd20225b29eb807fe042037b Mon Sep 17 00:00:00 2001 From: bkotlowski Date: Fri, 9 Jun 2023 16:02:34 +0200 Subject: [PATCH] feat(inputs): New Intel Baseband Accelerator Input Plugin (#13397) --- plugins/inputs/all/intel_baseband.go | 5 + plugins/inputs/intel_baseband/README.md | 127 ++++++++ .../inputs/intel_baseband/intel_baseband.go | 227 +++++++++++++++ .../intel_baseband_notamd64linux.go | 31 ++ .../intel_baseband/intel_baseband_test.go | 176 +++++++++++ .../inputs/intel_baseband/log_connector.go | 273 ++++++++++++++++++ .../intel_baseband/log_connector_test.go | 260 +++++++++++++++++ plugins/inputs/intel_baseband/mock/conn.go | 146 ++++++++++ plugins/inputs/intel_baseband/sample.conf | 31 ++ .../inputs/intel_baseband/sock_connector.go | 101 +++++++ .../intel_baseband/sock_connector_test.go | 77 +++++ .../testdata/logfiles/empty.log | 0 .../testdata/logfiles/example.log | 16 + plugins/inputs/intel_baseband/utils.go | 81 ++++++ plugins/inputs/intel_baseband/utils_test.go | 183 ++++++++++++ 15 files changed, 1734 insertions(+) create mode 100644 plugins/inputs/all/intel_baseband.go create mode 100644 plugins/inputs/intel_baseband/README.md create mode 100644 plugins/inputs/intel_baseband/intel_baseband.go create mode 100644 plugins/inputs/intel_baseband/intel_baseband_notamd64linux.go create mode 100644 plugins/inputs/intel_baseband/intel_baseband_test.go create mode 100644 plugins/inputs/intel_baseband/log_connector.go create mode 100644 plugins/inputs/intel_baseband/log_connector_test.go create mode 100644 plugins/inputs/intel_baseband/mock/conn.go create mode 100644 plugins/inputs/intel_baseband/sample.conf create mode 100644 plugins/inputs/intel_baseband/sock_connector.go create mode 100644 plugins/inputs/intel_baseband/sock_connector_test.go create mode 100644 plugins/inputs/intel_baseband/testdata/logfiles/empty.log create mode 100644 plugins/inputs/intel_baseband/testdata/logfiles/example.log create mode 100644 plugins/inputs/intel_baseband/utils.go create mode 100644 plugins/inputs/intel_baseband/utils_test.go diff --git a/plugins/inputs/all/intel_baseband.go b/plugins/inputs/all/intel_baseband.go new file mode 100644 index 0000000000000..2e46666a9c81b --- /dev/null +++ b/plugins/inputs/all/intel_baseband.go @@ -0,0 +1,5 @@ +//go:build !custom || inputs || inputs.intel_baseband + +package all + +import _ "github.com/influxdata/telegraf/plugins/inputs/intel_baseband" // register plugin diff --git a/plugins/inputs/intel_baseband/README.md b/plugins/inputs/intel_baseband/README.md new file mode 100644 index 0000000000000..ccd71d6ebbf57 --- /dev/null +++ b/plugins/inputs/intel_baseband/README.md @@ -0,0 +1,127 @@ +# Intel Baseband Accelerator Input Plugin + +Intel Baseband Accelerator Input Plugin collects metrics from both dedicated and +integrated Intel devices that provide Wireless Baseband hardware acceleration. +These devices play a key role in accelerating 5G and 4G Virtualized Radio Access +Networks (vRAN) workloads, increasing the overall compute capacity of +a commercial, off-the-shelf platforms. + +Intel Baseband devices integrate various features critical for 5G and +LTE (Long Term Evolution) networks, including e.g.: + +- Forward Error Correction (FEC) processing, +- 4G Turbo FEC processing, +- 5G Low Density Parity Check (LDPC) +- a Fast Fourier Transform (FFT) block providing DFT/iDFT processing offload +for the 5G Sounding Reference Signal (SRS) + +Supported hardware: + +- Intel® vRAN Boost integrated accelerators: + - 4th Gen Intel® Xeon® Scalable processor with Intel® vRAN Boost (also known as Sapphire Rapids Edge Enhanced / SPR-EE) +- External expansion cards connected to the PCI bus: + - Intel® vRAN Dedicated Accelerator ACC100 SoC (code named Mount Bryce) + - Intel® vRAN Dedicated Accelerator ACC101 SoC (code named Mount Cirrus) + +## Prerequisites + +- Intel Baseband device installed and configured. +- Minimum Linux kernel version required is 5.7. +- [pf-bb-config](https://github.com/intel/pf-bb-config) (version >= v23.03) installed and running. + +For more information regarding system configuration, please follow DPDK +installation guides: + +- [Intel® vRAN Boost Poll Mode Driver (PMD)][VRB1] +- [Intel® ACC100 and ACC101 5G/4G FEC Poll Mode Drivers][ACC100/ACC101] + +[VRB1]: https://doc.dpdk.org/guides/bbdevs/vrb1.html#installation +[ACC100/ACC101]: https://doc.dpdk.org/guides/bbdevs/acc100.html#installation + +## Global configuration options + +In addition to the plugin-specific configuration settings, plugins support +additional global and plugin configuration settings. These settings are used to +modify metrics, tags, and field or create aliases and configure ordering, etc. +See the [CONFIGURATION.md][CONFIGURATION.md] for more details. + +[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins + +## Configuration + +```toml @sample.conf +# Intel Baseband Accelerator Input Plugin collects metrics from both dedicated and integrated +# Intel devices that provide Wireless Baseband hardware acceleration. +# This plugin ONLY supports Linux. +[[inputs.intel_baseband]] + ## Path to socket exposed by pf-bb-config for CLI interaction (mandatory). + ## In version v23.03 of pf-bb-config the path is created according to the schema: + ## "/tmp/pf_bb_config.0000\:\:..sock" where 0000\:\:. is the PCI device ID. + socket_path = "" + + ## Path to log file exposed by pf-bb-config with telemetry to read (mandatory). + ## In version v23.03 of pf-bb-config the path is created according to the schema: + ## "/var/log/pf_bb_cfg_0000\:\:..log" where 0000\:\:. is the PCI device ID. + log_file_path = "" + + ## Specifies plugin behavior regarding unreachable socket (which might not have been initialized yet). + ## Available choices: + ## - error: Telegraf will return an error on startup if socket is unreachable + ## - ignore: Telegraf will ignore error regarding unreachable socket on both startup and gather + # unreachable_socket_behavior = "error" + + ## Duration that defines how long the connected socket client will wait for + ## a response before terminating connection. + ## Since it's local socket access to a fast packet processing application, the timeout should + ## be sufficient for most users. + ## Setting the value to 0 disables the timeout (not recommended). + # socket_access_timeout = "1s" + + ## Duration that defines maximum time plugin will wait for pf-bb-config to write telemetry to the log file. + ## Timeout may differ depending on the environment. + ## Must be equal or larger than 50ms. + # wait_for_telemetry_timeout = "1s" +``` + +## Metrics + +Depending on version of Intel Baseband device and version of pf-bb-config, +subset of following measurements may be exposed: + +**The following tags and fields are supported by Intel Baseband plugin:** + +| Tag | Description | +|-------------|-------------------------------------------------------------| +| `metric` | Type of metric : "code_blocks", "data_bytes", "per_engine". | +| `operation` | Type of operation: "5GUL", "5GDL", "4GUL", "4GDL", "FFT". | +| `vf` | Virtual Function number. | +| `engine` | Engine number. | + +| Metric name (field) | Description | +|----------------------|-------------------------------------------------------------------| +| `value` | Metric value for a given operation (non-negative integer, gauge). | + +## Example Output + +```text +intel_baseband,host=ubuntu,metric=code_blocks,operation=5GUL,vf=0 value=54i 1685695885000000000 +intel_baseband,host=ubuntu,metric=code_blocks,operation=5GDL,vf=0 value=0i 1685695885000000000 +intel_baseband,host=ubuntu,metric=code_blocks,operation=FFT,vf=0 value=0i 1685695885000000000 +intel_baseband,host=ubuntu,metric=code_blocks,operation=5GUL,vf=1 value=0i 1685695885000000000 +intel_baseband,host=ubuntu,metric=code_blocks,operation=5GDL,vf=1 value=32i 1685695885000000000 +intel_baseband,host=ubuntu,metric=code_blocks,operation=FFT,vf=1 value=0i 1685695885000000000 +intel_baseband,host=ubuntu,metric=data_bytes,operation=5GUL,vf=0 value=18560i 1685695885000000000 +intel_baseband,host=ubuntu,metric=data_bytes,operation=5GDL,vf=0 value=0i 1685695885000000000 +intel_baseband,host=ubuntu,metric=data_bytes,operation=FFT,vf=0 value=0i 1685695885000000000 +intel_baseband,host=ubuntu,metric=data_bytes,operation=5GUL,vf=1 value=0i 1685695885000000000 +intel_baseband,host=ubuntu,metric=data_bytes,operation=5GDL,vf=1 value=86368i 1685695885000000000 +intel_baseband,host=ubuntu,metric=data_bytes,operation=FFT,vf=1 value=0i 1685695885000000000 +intel_baseband,engine=0,host=ubuntu,metric=per_engine,operation=5GUL value=72i 1685695885000000000 +intel_baseband,engine=1,host=ubuntu,metric=per_engine,operation=5GUL value=72i 1685695885000000000 +intel_baseband,engine=2,host=ubuntu,metric=per_engine,operation=5GUL value=72i 1685695885000000000 +intel_baseband,engine=3,host=ubuntu,metric=per_engine,operation=5GUL value=72i 1685695885000000000 +intel_baseband,engine=4,host=ubuntu,metric=per_engine,operation=5GUL value=72i 1685695885000000000 +intel_baseband,engine=0,host=ubuntu,metric=per_engine,operation=5GDL value=132i 1685695885000000000 +intel_baseband,engine=1,host=ubuntu,metric=per_engine,operation=5GDL value=130i 1685695885000000000 +intel_baseband,engine=0,host=ubuntu,metric=per_engine,operation=FFT value=0i 1685695885000000000 +``` diff --git a/plugins/inputs/intel_baseband/intel_baseband.go b/plugins/inputs/intel_baseband/intel_baseband.go new file mode 100644 index 0000000000000..c9fe9f8147a3b --- /dev/null +++ b/plugins/inputs/intel_baseband/intel_baseband.go @@ -0,0 +1,227 @@ +//go:generate ../../../tools/readme_config_includer/generator +//go:build linux && amd64 + +package intel_baseband + +import ( + _ "embed" + "errors" + "fmt" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/plugins/inputs" +) + +const ( + // plugin name. Exposed with all metrics + pluginName = "intel_baseband" + + // VF Metrics + vfCodeBlocks = "Code Blocks" + vfDataBlock = "Data (Bytes)" + + // Engine Metrics + engineBlock = "Per Engine" + + // Socket extensions + socketExtension = ".sock" + logFileExtension = ".log" + + // UnreachableSocketBehavior Values + unreachableSocketBehaviorError = "error" + unreachableSocketBehaviorIgnore = "ignore" + + defaultAccessSocketTimeout = config.Duration(time.Second) + defaultWaitForTelemetryTimeout = config.Duration(time.Second) +) + +//go:embed sample.conf +var sampleConfig string + +type Baseband struct { + // required params + SocketPath string `toml:"socket_path"` + FileLogPath string `toml:"log_file_path"` + + //optional params + UnreachableSocketBehavior string `toml:"unreachable_socket_behavior"` + SocketAccessTimeout config.Duration `toml:"socket_access_timeout"` + WaitForTelemetryTimeout config.Duration `toml:"wait_for_telemetry_timeout"` + + Log telegraf.Logger `toml:"-"` + logConn *logConnector + sockConn *socketConnector +} + +func (b *Baseband) SampleConfig() string { + return sampleConfig +} + +// Init performs one time setup of the plugin +func (b *Baseband) Init() error { + if b.SocketAccessTimeout < 0 { + return fmt.Errorf("socket_access_timeout should be positive number or equal to 0 (to disable timeouts)") + } + + waitForTelemetryDuration := time.Duration(b.WaitForTelemetryTimeout) + if waitForTelemetryDuration < 50*time.Millisecond { + return fmt.Errorf("wait_for_telemetry_timeout should be equal or larger than 50ms") + } + + // Filling default values + // Check UnreachableSocketBehavior + switch b.UnreachableSocketBehavior { + case "": + b.UnreachableSocketBehavior = unreachableSocketBehaviorError + case unreachableSocketBehaviorError, unreachableSocketBehaviorIgnore: + // Valid options, do nothing + default: + return fmt.Errorf("unknown choice for unreachable_socket_behavior: %q", b.UnreachableSocketBehavior) + } + + var err error + // Validate Socket path + if b.SocketPath, err = b.checkFilePath(b.SocketPath, socket); err != nil { + return fmt.Errorf("socket_path: %w", err) + } + + // Validate log file path + if b.FileLogPath, err = b.checkFilePath(b.FileLogPath, log); err != nil { + return fmt.Errorf("log_file_path: %w", err) + } + + // Create Log Connector + b.logConn = newLogConnector(b.FileLogPath, waitForTelemetryDuration) + + // Create Socket Connector + b.sockConn = newSocketConnector(b.SocketPath, time.Duration(b.SocketAccessTimeout)) + return nil +} + +func (b *Baseband) Gather(acc telegraf.Accumulator) error { + err := b.sockConn.dumpTelemetryToLog() + if err != nil { + return err + } + + // Read the log + err = b.logConn.readLogFile() + if err != nil { + return err + } + + err = b.logConn.readNumVFs() + if err != nil { + return fmt.Errorf("couldn't get the number of VFs: %w", err) + } + // b.numVFs less than 0 means that we are reading the file for the first time (or occurred discontinuity in file availability) + if b.logConn.getNumVFs() <= 0 { + return errors.New("error in accessing information about the amount of VF") + } + + // rawData eg: 12 0 + if err = b.gatherVFMetric(acc, vfCodeBlocks); err != nil { + return fmt.Errorf("couldn't get %q metric: %w", vfCodeBlocks, err) + } + + // rawData eg: 12 0 + if err = b.gatherVFMetric(acc, vfDataBlock); err != nil { + return fmt.Errorf("couldn't get %q metric: %w", vfDataBlock, err) + } + + // rawData eg: 12 0 0 0 0 0 + if err = b.gatherEngineMetric(acc, engineBlock); err != nil { + return fmt.Errorf("couldn't get %q metric: %w", engineBlock, err) + } + return nil +} + +func (b *Baseband) gatherVFMetric(acc telegraf.Accumulator, metricName string) error { + metrics, err := b.logConn.getMetrics(metricName) + if err != nil { + return fmt.Errorf("error accessing information about the metric %q: %w", metricName, err) + } + + for _, metric := range metrics { + if len(metric.data) != b.logConn.getNumVFs() { + return fmt.Errorf("data is inconsistent, number of metrics in the file for %d VFs, the number of VFs read is %d", + len(metric.data), b.logConn.numVFs) + } + + for i := range metric.data { + value, err := logMetricDataToValue(metric.data[i]) + if err != nil { + return err + } + + fields := map[string]interface{}{ + "value": value, + } + tags := map[string]string{ + "operation": metric.operationName, + "metric": metricNameToTagName(metricName), + "vf": fmt.Sprintf("%v", i), + } + acc.AddGauge(pluginName, fields, tags) + } + } + return nil +} + +func (b *Baseband) gatherEngineMetric(acc telegraf.Accumulator, metricName string) error { + metrics, err := b.logConn.getMetrics(metricName) + if err != nil { + return fmt.Errorf("error in accessing information about the metric %q: %w", metricName, err) + } + + for _, metric := range metrics { + for i := range metric.data { + value, err := logMetricDataToValue(metric.data[i]) + if err != nil { + return err + } + + fields := map[string]interface{}{ + "value": value, + } + tags := map[string]string{ + "operation": metric.operationName, + "metric": metricNameToTagName(metricName), + "engine": fmt.Sprintf("%v", i), + } + acc.AddGauge(pluginName, fields, tags) + } + } + return nil +} + +// Validate the provided path and return the clean version of it +// if UnreachableSocketBehavior = error -> return error, otherwise ignore the error +func (b *Baseband) checkFilePath(path string, fileType fileType) (resultPath string, err error) { + if resultPath, err = validatePath(path, fileType); err != nil { + return "", err + } + + if err = checkFile(path, fileType); err != nil { + if b.UnreachableSocketBehavior == unreachableSocketBehaviorError { + return "", err + } + b.Log.Warn(err) + } + return resultPath, nil +} + +func newBaseband() *Baseband { + return &Baseband{ + SocketAccessTimeout: defaultAccessSocketTimeout, + WaitForTelemetryTimeout: defaultWaitForTelemetryTimeout, + } +} + +func init() { + inputs.Add("intel_baseband", func() telegraf.Input { + return newBaseband() + }) +} diff --git a/plugins/inputs/intel_baseband/intel_baseband_notamd64linux.go b/plugins/inputs/intel_baseband/intel_baseband_notamd64linux.go new file mode 100644 index 0000000000000..d22753f34930a --- /dev/null +++ b/plugins/inputs/intel_baseband/intel_baseband_notamd64linux.go @@ -0,0 +1,31 @@ +//go:generate ../../../tools/readme_config_includer/generator +//go:build !linux || !amd64 + +package intel_baseband + +import ( + _ "embed" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" +) + +//go:embed sample.conf +var sampleConfig string + +type Baseband struct { + Log telegraf.Logger `toml:"-"` +} + +func (b *Baseband) Init() error { + b.Log.Warn("current platform is not supported") + return nil +} +func (*Baseband) SampleConfig() string { return sampleConfig } +func (*Baseband) Gather(_ telegraf.Accumulator) error { return nil } + +func init() { + inputs.Add("intel_baseband", func() telegraf.Input { + return &Baseband{} + }) +} diff --git a/plugins/inputs/intel_baseband/intel_baseband_test.go b/plugins/inputs/intel_baseband/intel_baseband_test.go new file mode 100644 index 0000000000000..abd6826880ba9 --- /dev/null +++ b/plugins/inputs/intel_baseband/intel_baseband_test.go @@ -0,0 +1,176 @@ +//go:build linux && amd64 + +package intel_baseband + +import ( + "net" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf/testutil" +) + +func TestInit(t *testing.T) { + t.Run("with not specified path values Init should return an error", func(t *testing.T) { + baseband := prepareBasebandEnvironment() + require.NotNil(t, baseband) + + err := baseband.Init() + + //check default variables + // check empty values + require.Empty(t, baseband.SocketPath) + require.Empty(t, baseband.FileLogPath) + + // UnreachableSocketBehavior variable should be = unreachableSocketBehaviorError + require.Equal(t, unreachableSocketBehaviorError, baseband.UnreachableSocketBehavior) + require.Error(t, err) + require.ErrorContains(t, err, "path not specified") + }) + + t.Run("with only SocketPath provided the plugin should return the error", func(t *testing.T) { + baseband := prepareBasebandEnvironment() + require.NotNil(t, baseband) + + tempSocket := newTempSocket(t) + defer tempSocket.Close() + + baseband.SocketPath = tempSocket.pathToSocket + err := baseband.Init() + require.Error(t, err) + require.ErrorContains(t, err, "log_file_path") + require.ErrorContains(t, err, "path not specified") + }) + + t.Run("with SocketAccessTimeout less then 0 provided the plugin should return the error", func(t *testing.T) { + baseband := prepareBasebandEnvironment() + require.NotNil(t, baseband) + + baseband.SocketAccessTimeout = -1 + err := baseband.Init() + require.Error(t, err) + require.ErrorContains(t, err, "socket_access_timeout should be positive number or equal to 0") + }) + + t.Run("with SocketPath and LogPath provided the plugin shouldn't return any errors", func(t *testing.T) { + baseband := prepareBasebandEnvironment() + require.NotNil(t, baseband) + + tempSocket := newTempSocket(t) + defer tempSocket.Close() + + logTempFile := newTempLogFile(t) + defer logTempFile.Close() + + baseband.SocketPath = tempSocket.pathToSocket + baseband.FileLogPath = logTempFile.pathToFile + err := baseband.Init() + + require.NoError(t, err) + }) + + t.Run("with unknown option for UnreachableSocketBehavior plugin should return the error", func(t *testing.T) { + baseband := prepareBasebandEnvironment() + require.NotNil(t, baseband) + + baseband.UnreachableSocketBehavior = "UnknownRandomString" + err := baseband.Init() + require.Error(t, err) + require.ErrorContains(t, err, "unreachable_socket_behavior") + }) + + t.Run("with error option for UnreachableSocketBehavior plugin should return error", func(t *testing.T) { + baseband := prepareBasebandEnvironment() + require.NotNil(t, baseband) + + baseband.UnreachableSocketBehavior = unreachableSocketBehaviorError + baseband.SocketPath = "/some/random/path/test.sock" + baseband.FileLogPath = "/some/random/path/test.log" + + err := baseband.Init() + require.Error(t, err) + require.ErrorContains(t, err, "socket_path") + require.ErrorContains(t, err, "provided path does not exist") + }) + + t.Run("with ignore option for UnreachableSocketBehavior plugin shouldn't return any errors", func(t *testing.T) { + baseband := prepareBasebandEnvironment() + require.NotNil(t, baseband) + + baseband.UnreachableSocketBehavior = unreachableSocketBehaviorIgnore + baseband.SocketPath = "/some/random/path/test.sock" + baseband.FileLogPath = "/some/random/path/test.log" + + err := baseband.Init() + require.NoError(t, err) + }) +} + +// Test Socket +type tempSocket struct { + pathToSocket string + socket net.Listener + + dirPath string +} + +func (ts *tempSocket) Close() { + var err error + if err = ts.socket.Close(); err != nil { + panic(err) + } + + if err = os.RemoveAll(ts.dirPath); err != nil { + panic(err) + } +} + +func newTempSocket(t *testing.T) *tempSocket { + dirPath, err := os.MkdirTemp("", "test-socket") + require.NoError(t, err) + + pathToSocket := filepath.Join(dirPath, "test"+socketExtension) + socket, err := net.Listen("unix", pathToSocket) + require.NoError(t, err) + + return &tempSocket{ + dirPath: dirPath, + pathToSocket: pathToSocket, + socket: socket, + } +} + +type tempLogFile struct { + pathToFile string + file *os.File +} + +func (tlf *tempLogFile) Close() { + var err error + if err = tlf.file.Close(); err != nil { + panic(err) + } + + if err = os.Remove(tlf.pathToFile); err != nil { + panic(err) + } +} + +func newTempLogFile(t *testing.T) *tempLogFile { + file, err := os.CreateTemp("", "*.log") + require.NoError(t, err) + + return &tempLogFile{ + pathToFile: file.Name(), + file: file, + } +} + +func prepareBasebandEnvironment() *Baseband { + b := newBaseband() + b.Log = testutil.Logger{Name: "BasebandPluginTest"} + return b +} diff --git a/plugins/inputs/intel_baseband/log_connector.go b/plugins/inputs/intel_baseband/log_connector.go new file mode 100644 index 0000000000000..ca6b96796d7f3 --- /dev/null +++ b/plugins/inputs/intel_baseband/log_connector.go @@ -0,0 +1,273 @@ +//go:build linux && amd64 + +package intel_baseband + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + "time" +) + +const ( + infoLine = "INFO:" + countersLine = "counters:" + + deviceStatusStartPrefix = "Device Status::" + deviceStatusEndPrefix = "VFs" + + clearLogCmdText = "clear_log" +) + +var errFindingSubstring = errors.New("couldn't find the substring in the log file") + +type logConnector struct { + // path to log + path string + + // Num of VFs + numVFs int + + // Log file data + lines []string + + waitForTelemetryTimeout time.Duration + lastModTime time.Time +} + +type logMetric struct { + operationName string + data []string +} + +// Try to read file and fill the .lines field. +func (lc *logConnector) readLogFile() error { + err := lc.checkLogFreshness() + if err != nil { + return err + } + file, err := os.ReadFile(lc.path) + if err != nil { + lc.numVFs = -1 + return fmt.Errorf("couldn't read log file: %w", err) + } + + // Example content of the metric file is located in testdata/example.log + // the minimum acceptable file content consists of three lines: + // - one line for number of VFs + // - two lines for operation (counters name and metrics value) + lines := strings.Split(string(file), "\n") + if len(lines) < 3 { + return errors.New("log file is incomplete") + } + + lc.lines = lines + return nil +} + +// function checks whether the data in the log file were updated by checking the last modification date and size +func (lc *logConnector) checkLogFreshness() error { + start := time.Now() + + // initial wait for telemetry being written to file + time.Sleep(50 * time.Millisecond) + + // check if it was written completely + for { + fileInfo, err := os.Stat(lc.path) + if err != nil { + return fmt.Errorf("couldn't stat log file: %w", err) + } + currModTime := fileInfo.ModTime() + + // pf-bb-config first performs log clearing (which will write clear_log line to this log just before it will be cleared), + // and then dumps the newest telemetry to this file. + // This checks if: + // - modification time has been changed + // - file is not empty + // - file doesn't contain clear_log command (it may appear for few milliseconds, just before file is cleared) + if lc.lastModTime != currModTime && fileInfo.Size() != 0 && !lc.isClearLogContainedInFile() { + //refreshing succeed + lc.lastModTime = currModTime + return nil + } + if time.Since(start) >= lc.waitForTelemetryTimeout { + if fileInfo.Size() == 0 { + return errors.New("log file is empty") + } + return errors.New("failed to refresh telemetry data") + } + time.Sleep(10 * time.Millisecond) + } +} + +func (lc *logConnector) isClearLogContainedInFile() bool { + file, err := os.ReadFile(lc.path) + if err != nil { + // for now, error means that "clear_log" line is not contained in log + return false + } + + return strings.Contains(string(file), clearLogCmdText) +} + +// Try to read file and return lines from it +func (lc *logConnector) getLogLines() []string { + return lc.lines +} + +// Try to read file and return lines from it +func (lc *logConnector) getLogLinesNum() int { + return len(lc.lines) +} + +// Return the number of VFs in the log file +func (lc *logConnector) getNumVFs() int { + return lc.numVFs +} + +// find a line which contains Device Status. Example = Thu Apr 13 13:28:40 2023:INFO:Device Status:: 2 VFs +func (lc *logConnector) readNumVFs() error { + for _, line := range lc.lines { + if !strings.Contains(line, deviceStatusStartPrefix) { + continue + } + + numVFs, err := lc.parseNumVFs(line) + if err != nil { + lc.numVFs = -1 + return err + } + lc.numVFs = numVFs + return nil + } + + return fmt.Errorf("numVFs data wasn't found in the log file") +} + +// Find a line which contains a substring in the log file +func (lc *logConnector) getSubstringLine(offsetLine int, substring string) (int, string, error) { + if len(substring) == 0 { + return 0, "", fmt.Errorf("substring is empty") + } + + for i := offsetLine; i < len(lc.lines); i++ { + if !strings.Contains(lc.lines[i], substring) { + continue + } + + return i, lc.lines[i], nil + } + return 0, "", fmt.Errorf("%q: %w", substring, errFindingSubstring) +} + +func (lc *logConnector) getMetrics(name string) (metrics []*logMetric, err error) { + offset := 0 + for { + currOffset, metric, err := lc.getMetric(offset, name) + if err != nil { + if !errors.Is(err, errFindingSubstring) || len(metrics) == 0 { + return nil, err + } + return metrics, nil + } + metrics = append(metrics, metric) + offset = currOffset + } +} + +// Example of log file: +// Thu May 18 08:45:15 2023:INFO:5GUL counters: Code Blocks +// Thu May 18 08:45:15 2023:INFO:0 0 +// Input: offsetLine, metric name (Code Blocks) +// Func will return: current offset after reading the metric (2), metric with operation name and data(5GUL, ["0", "0"]) and error +func (lc *logConnector) getMetric(offsetLine int, name string) (int, *logMetric, error) { + i, line, err := lc.getSubstringLine(offsetLine, name) + if err != nil { + return offsetLine, nil, err + } + + operationName := lc.parseOperationName(line) + if len(operationName) == 0 { + return offsetLine, nil, errors.New("valid operation name wasn't found in log") + } + + if lc.getLogLinesNum() <= i+1 { + return offsetLine, nil, + fmt.Errorf("the content of the log file is incorrect, line which contains key word %q can't be the last one in log", countersLine) + } + + // infoData eg: Thu Apr 13 13:28:40 2023:INFO:12 0 + infoData := strings.Split(lc.lines[i+1], infoLine) + if len(infoData) != 2 { + //info data must be in format : some data + keyword "INFO:" + metrics + return offsetLine, nil, fmt.Errorf("the content of the log file is incorrect, couldn't find %q separator", infoLine) + } + + dataRaw := strings.TrimSpace(infoData[1]) + if len(dataRaw) == 0 { + return offsetLine, nil, fmt.Errorf("the content of the log file is incorrect, metric's data is incorrect") + } + + data := strings.Split(dataRaw, " ") + for i := range data { + if len(data[i]) == 0 { + return offsetLine, nil, fmt.Errorf("the content of the log file is incorrect, metric's data is empty") + } + } + return i + 2, &logMetric{operationName: operationName, data: data}, nil +} + +// Example value = Thu Apr 13 13:28:40 2023:INFO:Device Status:: 2 VFs +func (lc *logConnector) parseNumVFs(s string) (int, error) { + i := strings.LastIndex(s, deviceStatusStartPrefix) + if i == -1 { + return 0, fmt.Errorf("couldn't find device status prefix in line") + } + + j := strings.Index(s[i:], deviceStatusEndPrefix) + if j == -1 { + return 0, fmt.Errorf("couldn't find device end prefix in line") + } + + startIndex := i + len(deviceStatusStartPrefix) + 1 + endIndex := i + j - 1 + if len(s) < startIndex || startIndex >= endIndex { + return 0, fmt.Errorf("incorrect format of the line") + } + + return strconv.Atoi(s[startIndex:endIndex]) +} + +// Parse Operation name +// Example = Thu Apr 13 13:28:40 2023:INFO:5GUL counters: Code Blocks +// Output: 5GUL +func (lc *logConnector) parseOperationName(s string) string { + i := strings.Index(s, infoLine) + if i >= 0 { + j := strings.Index(s[i:], countersLine) + startIndex := i + len(infoLine) + endIndex := i + j - 1 + if j >= 0 && startIndex < endIndex { + return s[startIndex:endIndex] + } + } + return "" +} + +func newLogConnector(path string, waitForTelemetryTimeout time.Duration) *logConnector { + lastModTime := time.Time{} + fileInfo, err := os.Stat(path) + if err == nil { + lastModTime = fileInfo.ModTime() + } + + return &logConnector{ + path: path, + waitForTelemetryTimeout: waitForTelemetryTimeout, + numVFs: -1, + lastModTime: lastModTime, + } +} diff --git a/plugins/inputs/intel_baseband/log_connector_test.go b/plugins/inputs/intel_baseband/log_connector_test.go new file mode 100644 index 0000000000000..423b839084310 --- /dev/null +++ b/plugins/inputs/intel_baseband/log_connector_test.go @@ -0,0 +1,260 @@ +//go:build linux && amd64 + +package intel_baseband + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestReadLogFile(t *testing.T) { + testCases := []struct { + name string + testLogPath string + err error + }{ + {"when file doesn't exist return the error", "testdata/logfiles/doesntexist", errors.New("no such file or directory")}, + {"when the file is empty return the error", "testdata/logfiles/empty.log", errors.New("log file is empty")}, + {"when the log file is correct, error should be nil", "testdata/logfiles/example.log", nil}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logConnector := prepareLogConnMock() + require.NotNil(t, logConnector) + logConnector.path = tc.testLogPath + + err := logConnector.readLogFile() + if tc.err != nil { + require.ErrorContains(t, err, tc.err.Error()) + return + } + + require.NoError(t, err) + data := logConnector.getLogLines() + require.NotEmpty(t, data) + require.NoError(t, err) + }) + } +} + +func TestGetMetric(t *testing.T) { + testCases := []struct { + name string + input []string + metricName string + expectedOperation string + expectedData []string + err error + }{ + {"with correct string no error should be returned", + []string{"Thu May 18 08:45:15 2023:INFO:5GUL counters: Code Blocks", "Thu May 18 08:45:15 2023:INFO:0 0"}, + vfCodeBlocks, "5GUL", []string{"0", "0"}, nil}, + + {"with correct string no error should be returned", + []string{"Thu May 18 08:45:15 2023:INFO:5GUL counters: Data (Bytes)", "Thu May 18 08:45:15 2023:INFO:0 0"}, + vfDataBlock, "5GUL", []string{"0", "0"}, nil}, + + {"with correct string no error should be returned", + []string{"Thu May 18 08:45:15 2023:INFO:5GUL counters: Per Engine", "Thu May 18 08:45:15 2023:INFO:0 0 3 0 50 0 200 0"}, + engineBlock, "5GUL", []string{"0", "0", "3", "0", "50", "0", "200", "0"}, nil}, + + {"when the incorrect number of lines provided, error should be returned", + []string{"Thu May 18 08:45:15 2023:INFO:5GUL counters: Per Engine"}, + engineBlock, "5GUL", []string{""}, errors.New("the content of the log file is incorrect")}, + + {"when the incorrect number of lines provided, error should be returned", + []string{"Thu May 18 08:45:15 2023:INFO:5GUL counters: Per Engine", ""}, + engineBlock, "5GUL", []string{""}, errors.New("the content of the log file is incorrect")}, + + {"when the incorrect line provided, error should be returned", []string{"Something different"}, + "", "5GUL", []string{""}, errors.New("substring is empty")}, + + {"when the incorrect line provided error should be returned", []string{"Device Status:: 1 VFs", "INFO:00counters:", "INFO:0 0"}, + "I", "", nil, errors.New("metric's data is empty")}, + + {"when the incorrect metric's line provided error should be returned", []string{"Device Status:: 1 VFs", "INFO:00counters:B", "INFO: "}, + "B", "", nil, errors.New("metric's data is incorrect")}, + + {"when the operation name wasn't found, error should be returned", []string{"Device Status:: 1 VFs", "", "INFO:countersCode Blocks"}, + "B", "", nil, errors.New("valid operation name wasn't found in log")}, + + {"when lines are empty, error should be returned", []string{""}, + "something", "5GUL", []string{""}, errors.New("couldn't find the substring")}, + + {"when lines are empty, error should be returned", nil, + "something", "5GUL", []string{""}, errors.New("couldn't find the substring")}, + } + + logConnector := prepareLogConnMock() + require.NotNil(t, logConnector) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logConnector.lines = tc.input + offset, metric, err := logConnector.getMetric(0, tc.metricName) + if tc.err != nil { + require.ErrorContains(t, err, tc.err.Error()) + return + } + + require.NoError(t, err) + require.Equal(t, 2, offset) + require.Equal(t, tc.expectedOperation, metric.operationName) + require.ElementsMatch(t, tc.expectedData, metric.data) + }) + } +} + +func TestReadAndGetMetrics(t *testing.T) { + testCases := []struct { + name string + filePath string + metricName string + expectedOperations []string + expectedData [][]string + }{ + {"with correct values no error should be returned for Code Blocks", + "testdata/logfiles/example.log", + vfCodeBlocks, []string{"5GUL", "5GDL"}, [][]string{{"0", "0"}, {"1", "0"}}}, + {"with correct values no error should be returned for Data Blocks", + "testdata/logfiles/example.log", + vfDataBlock, []string{"5GUL", "5GDL"}, [][]string{{"0", "0"}, {"2699", "0"}}}, + {"with correct values no error should be returned for Per Engine Blocks", + "testdata/logfiles/example.log", + engineBlock, []string{"5GUL", "5GDL"}, [][]string{{"0", "0", "0", "0", "0", "0", "0", "0"}, {"1", "0", "0"}}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logConnector := prepareLogConnMock() + require.NotNil(t, logConnector) + logConnector.path = tc.filePath + err := logConnector.readLogFile() + require.NoError(t, err) + metrics, err := logConnector.getMetrics(tc.metricName) + require.NoError(t, err) + require.Len(t, metrics, len(tc.expectedOperations)) + + for i := range metrics { + require.Equal(t, tc.expectedOperations[i], metrics[i].operationName) + require.ElementsMatch(t, tc.expectedData[i], metrics[i].data) + } + }) + } +} + +func TestGetMetrics(t *testing.T) { + testCases := []struct { + name string + input []string + metricName string + expectedOperations []string + expectedData [][]string + err error + }{ + {"with correct values no error should be returned", + []string{"Thu May 18 08:45:15 2023:INFO:5GUL counters: Code Blocks", "Thu May 18 08:45:15 2023:INFO:0 0", + "Thu May 18 08:45:15 2023:INFO:5GUL counters: XXXX XXXX", "Thu May 18 08:45:15 2023:INFO:0 1", "sdasadasdsa", + "Thu May 18 08:45:15 2023:INFO:5GDL counters: Code Blocks", "Thu May 18 08:45:15 2023:INFO:1 1", + "Thu May 18 08:45:15 2023:INFO:5GUL counters: XXXX XXXX", "Thu May 18 08:45:15 2023:INFO:0 1", "sdasadasdsa"}, + vfCodeBlocks, []string{"5GUL", "5GDL"}, [][]string{{"0", "0"}, {"1", "1"}}, nil}, + + {"when lines are empty, error should be returned", []string{""}, + "something", nil, nil, errors.New("couldn't find the substring in the log file")}, + + {"when lines are nil, error should be returned", nil, + "something", nil, nil, errors.New("couldn't find the substring in the log file")}, + } + + logConnector := prepareLogConnMock() + require.NotNil(t, logConnector) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logConnector.lines = tc.input + metrics, err := logConnector.getMetrics(tc.metricName) + if tc.err != nil { + require.ErrorContains(t, err, tc.err.Error()) + return + } + + require.NoError(t, err) + require.Len(t, metrics, len(tc.expectedOperations)) + + for i := range metrics { + require.Equal(t, tc.expectedOperations[i], metrics[i].operationName) + require.ElementsMatch(t, tc.expectedData[i], metrics[i].data) + } + }) + } +} + +func TestGetNumVFs(t *testing.T) { + testCases := []struct { + name string + input []string + expected int + err error + }{ + {"incorrect format of the line", []string{"Device Status::VFs"}, -1, errors.New("incorrect format of the line")}, + {"when the line is correct, no error should be returned", []string{"Device Status:: 0 VFs"}, 0, nil}, + {"when the line is correct, no error should be returned", []string{"Device Status:: 10 VFs"}, 10, nil}, + {"when the line is correct, no error should be returned", []string{"Device Status:: 5000 VFs"}, 5000, nil}, + {"when the value is not int, error should be returned", []string{"Device Status:: Nah VFs"}, -1, errors.New("invalid syntax")}, + {"when end prefix isn't found, error should be returned", []string{"Device Status:: Nah END"}, -1, errors.New("couldn't find device end prefix")}, + {"when the line is empty, error should be returned", []string{""}, -1, errors.New("numVFs data wasn't found")}, + {"when the line is empty, error should be returned", nil, -1, errors.New("numVFs data wasn't found")}, + } + + logConnector := prepareLogConnMock() + require.NotNil(t, logConnector) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + logConnector.lines = tc.input + err := logConnector.readNumVFs() + if tc.err != nil { + require.ErrorContains(t, err, tc.err.Error()) + return + } + + require.NoError(t, err) + numVFs := logConnector.getNumVFs() + require.Equal(t, tc.expected, numVFs) + require.Equal(t, tc.expected, logConnector.numVFs) + }) + } +} + +func TestParseOperationName(t *testing.T) { + testCases := []struct { + input string + expected string + }{ + {"Thu May 18 08:45:15 2023:INFO:5GUL counters: Code Blocks", "5GUL"}, + {"May 18 08:45:15 2023:INFO:5GUL counters: Per Engine", "5GUL"}, + {"023:INFO:3G counters: Per ", "3G"}, + {"Device Status:: Nah VFs", ""}, + {"", ""}, + } + + logConnector := prepareLogConnMock() + require.NotNil(t, logConnector) + for _, tc := range testCases { + t.Run(fmt.Sprintf("expected %s", tc.expected), func(t *testing.T) { + operationName := logConnector.parseOperationName(tc.input) + require.Equal(t, tc.expected, operationName) + }) + } +} + +func prepareLogConnMock() *logConnector { + return &logConnector{ + path: "", + numVFs: -1, + lastModTime: time.Time{}, + } +} diff --git a/plugins/inputs/intel_baseband/mock/conn.go b/plugins/inputs/intel_baseband/mock/conn.go new file mode 100644 index 0000000000000..58961039dce86 --- /dev/null +++ b/plugins/inputs/intel_baseband/mock/conn.go @@ -0,0 +1,146 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + net "net" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// Conn is an autogenerated mock type for the Conn type +type Conn struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *Conn) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// LocalAddr provides a mock function with given fields: +func (_m *Conn) LocalAddr() net.Addr { + ret := _m.Called() + + var r0 net.Addr + if rf, ok := ret.Get(0).(func() net.Addr); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Addr) + } + } + + return r0 +} + +// Read provides a mock function with given fields: b +func (_m *Conn) Read(b []byte) (int, error) { + ret := _m.Called(b) + + var r0 int + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(b) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoteAddr provides a mock function with given fields: +func (_m *Conn) RemoteAddr() net.Addr { + ret := _m.Called() + + var r0 net.Addr + if rf, ok := ret.Get(0).(func() net.Addr); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(net.Addr) + } + } + + return r0 +} + +// SetDeadline provides a mock function with given fields: t +func (_m *Conn) SetDeadline(t time.Time) error { + ret := _m.Called(t) + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetReadDeadline provides a mock function with given fields: t +func (_m *Conn) SetReadDeadline(t time.Time) error { + ret := _m.Called(t) + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetWriteDeadline provides a mock function with given fields: t +func (_m *Conn) SetWriteDeadline(t time.Time) error { + ret := _m.Called(t) + + var r0 error + if rf, ok := ret.Get(0).(func(time.Time) error); ok { + r0 = rf(t) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Write provides a mock function with given fields: b +func (_m *Conn) Write(b []byte) (int, error) { + ret := _m.Called(b) + + var r0 int + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(b) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(b) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/plugins/inputs/intel_baseband/sample.conf b/plugins/inputs/intel_baseband/sample.conf new file mode 100644 index 0000000000000..7189a233dbf0c --- /dev/null +++ b/plugins/inputs/intel_baseband/sample.conf @@ -0,0 +1,31 @@ +# Intel Baseband Accelerator Input Plugin collects metrics from both dedicated and integrated +# Intel devices that provide Wireless Baseband hardware acceleration. +# This plugin ONLY supports Linux. +[[inputs.intel_baseband]] + ## Path to socket exposed by pf-bb-config for CLI interaction (mandatory). + ## In version v23.03 of pf-bb-config the path is created according to the schema: + ## "/tmp/pf_bb_config.0000\:\:..sock" where 0000\:\:. is the PCI device ID. + socket_path = "" + + ## Path to log file exposed by pf-bb-config with telemetry to read (mandatory). + ## In version v23.03 of pf-bb-config the path is created according to the schema: + ## "/var/log/pf_bb_cfg_0000\:\:..log" where 0000\:\:. is the PCI device ID. + log_file_path = "" + + ## Specifies plugin behavior regarding unreachable socket (which might not have been initialized yet). + ## Available choices: + ## - error: Telegraf will return an error on startup if socket is unreachable + ## - ignore: Telegraf will ignore error regarding unreachable socket on both startup and gather + # unreachable_socket_behavior = "error" + + ## Duration that defines how long the connected socket client will wait for + ## a response before terminating connection. + ## Since it's local socket access to a fast packet processing application, the timeout should + ## be sufficient for most users. + ## Setting the value to 0 disables the timeout (not recommended). + # socket_access_timeout = "1s" + + ## Duration that defines maximum time plugin will wait for pf-bb-config to write telemetry to the log file. + ## Timeout may differ depending on the environment. + ## Must be equal or larger than 50ms. + # wait_for_telemetry_timeout = "1s" diff --git a/plugins/inputs/intel_baseband/sock_connector.go b/plugins/inputs/intel_baseband/sock_connector.go new file mode 100644 index 0000000000000..dc98f3bbab583 --- /dev/null +++ b/plugins/inputs/intel_baseband/sock_connector.go @@ -0,0 +1,101 @@ +//go:build linux && amd64 + +package intel_baseband + +import ( + "fmt" + "net" + "time" +) + +const ( + // Command code + clearLogCmdID = 0x4 + deviceDataCmdID = 0x9 +) + +type socketConnector struct { + pathToSocket string + accessTimeout time.Duration + connection net.Conn +} + +func (sc *socketConnector) dumpTelemetryToLog() error { + // clean the log to have only the latest metrics in the file + err := sc.sendCommandToSocket(clearLogCmdID) + if err != nil { + return fmt.Errorf("failed to send clear log command: %w", err) + } + + // fill the file with the latest metrics + err = sc.sendCommandToSocket(deviceDataCmdID) + if err != nil { + return fmt.Errorf("failed to send device data command: %w", err) + } + return nil +} + +func (sc *socketConnector) sendCommandToSocket(c byte) error { + err := sc.connect() + if err != nil { + return err + } + defer sc.close() + err = sc.writeCommandToSocket(c) + if err != nil { + return err + } + return nil +} + +func (sc *socketConnector) writeCommandToSocket(c byte) error { + if sc.connection == nil { + return fmt.Errorf("connection had not been established before") + } + var err error + if sc.accessTimeout == 0 { + err = sc.connection.SetWriteDeadline(time.Time{}) + } else { + err = sc.connection.SetWriteDeadline(time.Now().Add(sc.accessTimeout)) + } + + if err != nil { + return fmt.Errorf("failed to set timeout for request: %w", err) + } + + _, err = sc.connection.Write([]byte{c, 0x00}) + if err != nil { + return fmt.Errorf("failed to send request to socket: %w", err) + } + return nil +} + +func (sc *socketConnector) connect() error { + connection, err := net.Dial("unix", sc.pathToSocket) + if err != nil { + return fmt.Errorf("failed to connect to the socket: %w", err) + } + + sc.connection = connection + return nil +} + +func (sc *socketConnector) close() error { + if sc.connection == nil { + return nil + } + + err := sc.connection.Close() + sc.connection = nil + if err != nil { + return err + } + return nil +} + +func newSocketConnector(pathToSocket string, accessTimeout time.Duration) *socketConnector { + return &socketConnector{ + pathToSocket: pathToSocket, + accessTimeout: accessTimeout, + } +} diff --git a/plugins/inputs/intel_baseband/sock_connector_test.go b/plugins/inputs/intel_baseband/sock_connector_test.go new file mode 100644 index 0000000000000..5965d47626b4c --- /dev/null +++ b/plugins/inputs/intel_baseband/sock_connector_test.go @@ -0,0 +1,77 @@ +//go:build linux && amd64 + +package intel_baseband + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf/plugins/inputs/intel_baseband/mock" +) + +func TestWriteCommandToSocket(t *testing.T) { + t.Run("correct execution of the function", func(t *testing.T) { + conn := &mocks.Conn{} + conn.On("Write", mock.Anything).Return(2, nil) + conn.On("SetWriteDeadline", mock.Anything).Return(nil) + connector := socketConnector{connection: conn} + + err := connector.writeCommandToSocket(0x00) + require.NoError(t, err) + + defer conn.AssertExpectations(t) + }) + + t.Run("without setting up a connection it should return an error", func(t *testing.T) { + connector := socketConnector{} + + err := connector.writeCommandToSocket(0x00) + require.Error(t, err) + require.ErrorContains(t, err, "connection had not been established before") + }) + + t.Run("handling timeout setting error", func(t *testing.T) { + conn := &mocks.Conn{} + conn.On("SetWriteDeadline", mock.Anything).Return(fmt.Errorf("deadline set error")) + connector := socketConnector{connection: conn} + + err := connector.writeCommandToSocket(0x00) + + require.Error(t, err) + require.ErrorContains(t, err, "failed to set timeout for request") + require.ErrorContains(t, err, "deadline set error") + defer conn.AssertExpectations(t) + }) + + t.Run("handling net.Write error", func(t *testing.T) { + var unsupportedCommand byte = 0x99 + conn := &mocks.Conn{} + conn.On("Write", []byte{unsupportedCommand, 0x00}).Return(0, fmt.Errorf("unsupported command")) + conn.On("SetWriteDeadline", mock.Anything).Return(nil) + connector := socketConnector{connection: conn} + + err := connector.writeCommandToSocket(unsupportedCommand) + + require.Error(t, err) + require.ErrorContains(t, err, "failed to send request to socket") + require.ErrorContains(t, err, "unsupported command") + defer conn.AssertExpectations(t) + }) +} + +func TestDumpTelemetryToLog(t *testing.T) { + t.Run("with correct temporary socket should return only an error related to the inability to refresh telemetry", func(t *testing.T) { + tempSocket := newTempSocket(t) + defer tempSocket.Close() + tempLogFile := newTempLogFile(t) + defer tempLogFile.Close() + connector := newSocketConnector(tempSocket.pathToSocket, 5*time.Second) + + err := connector.dumpTelemetryToLog() + require.NoError(t, err) + }) +} diff --git a/plugins/inputs/intel_baseband/testdata/logfiles/empty.log b/plugins/inputs/intel_baseband/testdata/logfiles/empty.log new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/plugins/inputs/intel_baseband/testdata/logfiles/example.log b/plugins/inputs/intel_baseband/testdata/logfiles/example.log new file mode 100644 index 0000000000000..7e34830c02eca --- /dev/null +++ b/plugins/inputs/intel_baseband/testdata/logfiles/example.log @@ -0,0 +1,16 @@ +Thu May 18 08:45:15 2023:INFO:device_data command received +Thu May 18 08:45:15 2023:INFO:Device Status:: 2 VFs +Thu May 18 08:45:15 2023:INFO:-  VF 0 RTE_BBDEV_DEV_CONFIGURED +Thu May 18 08:45:15 2023:INFO:-  VF 1 RTE_BBDEV_DEV_CONFIGURED +Thu May 18 08:45:15 2023:INFO:5GUL counters: Code Blocks +Thu May 18 08:45:15 2023:INFO:0 0 +Thu May 18 08:45:15 2023:INFO:5GUL counters: Data (Bytes) +Thu May 18 08:45:15 2023:INFO:0 0 +Thu May 18 08:45:15 2023:INFO:5GUL counters: Per Engine +Thu May 18 08:45:15 2023:INFO:0 0 0 0 0 0 0 0 +Thu May 18 08:45:15 2023:INFO:5GDL counters: Code Blocks +Thu May 18 08:45:15 2023:INFO:1 0 +Thu May 18 08:45:15 2023:INFO:5GDL counters: Data (Bytes) +Thu May 18 08:45:15 2023:INFO:2699 0 +Thu May 18 08:45:15 2023:INFO:5GDL counters: Per Engine +Thu May 18 08:45:15 2023:INFO:1 0 0 \ No newline at end of file diff --git a/plugins/inputs/intel_baseband/utils.go b/plugins/inputs/intel_baseband/utils.go new file mode 100644 index 0000000000000..3d54eb25f9f42 --- /dev/null +++ b/plugins/inputs/intel_baseband/utils.go @@ -0,0 +1,81 @@ +//go:build linux && amd64 + +package intel_baseband + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path" + "strconv" + "strings" +) + +type fileType int + +const ( + log fileType = iota + socket +) + +func validatePath(pathToRead string, ft fileType) (string, error) { + if pathToRead == "" { + return "", errors.New("required path not specified") + } + cleanPath := path.Clean(pathToRead) + if (ft == log && path.Ext(cleanPath) != logFileExtension) || (ft == socket && path.Ext(cleanPath) != socketExtension) { + return "", fmt.Errorf("wrong file extension: %q", cleanPath) + } + if !path.IsAbs(cleanPath) { + return "", fmt.Errorf("path is not absolute %q", cleanPath) + } + return cleanPath, nil +} + +func checkFile(pathToFile string, fileType fileType) error { + pathInfo, err := os.Lstat(pathToFile) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("provided path does not exist: %q", pathToFile) + } + if errors.Is(err, fs.ErrPermission) { + return fmt.Errorf("user doesn't have enough privileges to file %q", pathToFile) + } + + return fmt.Errorf("couldn't get system information of file %q: %w", pathToFile, err) + } + + mode := pathInfo.Mode() + switch fileType { + case socket: + if mode&os.ModeSocket != os.ModeSocket { + return fmt.Errorf("provided path does not point to a socket file: %q", pathToFile) + } + case log: + if !(mode.IsRegular()) { + return fmt.Errorf("provided path does not point to a log file: %q", pathToFile) + } + } + return nil +} + +// Replace metric name to snake case +// Example: Code Blocks -> code_blocks +func metricNameToTagName(metricName string) string { + cleanedStr := strings.Replace(strings.Replace(strings.Replace(metricName, "(", "", -1), ")", "", -1), " ", "_", -1) + return strings.ToLower(cleanedStr) +} + +func logMetricDataToValue(data string) (int, error) { + value, err := strconv.Atoi(data) + if err != nil { + return 0, err + } + + if value < 0 { + return 0, fmt.Errorf("metric can't be negative") + } + + return value, nil +} diff --git a/plugins/inputs/intel_baseband/utils_test.go b/plugins/inputs/intel_baseband/utils_test.go new file mode 100644 index 0000000000000..204f9a0277238 --- /dev/null +++ b/plugins/inputs/intel_baseband/utils_test.go @@ -0,0 +1,183 @@ +//go:build linux && amd64 + +package intel_baseband + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMetricNameToTagName(t *testing.T) { + testCases := []struct { + metricName string + expectedTagName string + }{ + {vfCodeBlocks, "code_blocks"}, + {vfDataBlock, "data_bytes"}, + {engineBlock, "per_engine"}, + {"", ""}, + } + + t.Run("check the correct transformation metric name", func(t *testing.T) { + for _, tc := range testCases { + tagName := metricNameToTagName(tc.metricName) + require.Equal(t, tc.expectedTagName, tagName) + } + }) +} + +func TestValidatePath(t *testing.T) { + t.Run("with correct file extensions checkFile shouldn't return any errors", func(t *testing.T) { + testCases := []struct { + path string + ft fileType + expectedPath string + }{ + {"/tmp/socket.sock", socket, "/tmp/socket.sock"}, + {"/foo/../tmp/socket.sock", socket, "/tmp/socket.sock"}, + {"/tmp/file.log", log, "/tmp/file.log"}, + {"/foo/../tmp/file.log", log, "/tmp/file.log"}, + } + + for _, tc := range testCases { + returnPath, err := validatePath(tc.path, tc.ft) + require.Equal(t, tc.expectedPath, returnPath) + require.NoError(t, err) + } + }) + t.Run("with empty path specified validate path should return an error", func(t *testing.T) { + testCases := []struct { + path string + ft fileType + expectedErrorContains string + }{ + {"", socket, "required path not specified"}, + {"", log, "required path not specified"}, + } + + for _, tc := range testCases { + returnPath, err := validatePath(tc.path, tc.ft) + require.Equal(t, "", returnPath) + require.ErrorContains(t, err, tc.expectedErrorContains) + } + }) + t.Run("with wrong extension file validatePath should return an error", func(t *testing.T) { + testCases := []struct { + path string + ft fileType + expectedErrorContains string + }{ + {"/tmp/socket.foo", socket, "wrong file extension"}, + {"/tmp/file.foo", log, "wrong file extension"}, + {"/tmp/socket.sock", log, "wrong file extension"}, + {"/tmp/file.log", socket, "wrong file extension"}, + } + + for _, tc := range testCases { + returnPath, err := validatePath(tc.path, tc.ft) + require.Equal(t, "", returnPath) + require.ErrorContains(t, err, tc.expectedErrorContains) + } + }) + t.Run("with not absolute path validatePath should return the error", func(t *testing.T) { + testCases := []struct { + path string + ft fileType + expectedErrorContains string + }{ + {"foo/tmp/socket.sock", socket, "path is not absolute"}, + {"foo/tmp/file.log", log, "path is not absolute"}, + } + + for _, tc := range testCases { + returnPath, err := validatePath(tc.path, tc.ft) + require.Equal(t, "", returnPath) + require.ErrorContains(t, err, tc.expectedErrorContains) + } + }) +} + +func TestCheckFile(t *testing.T) { + t.Run("with correct file extensions checkFile shouldn't return any errors", func(t *testing.T) { + tempSocket := newTempSocket(t) + defer tempSocket.Close() + + testCases := []struct { + path string + ft fileType + }{ + {"testdata/logfiles/example.log", log}, + {tempSocket.pathToSocket, socket}, + } + + for _, tc := range testCases { + err := checkFile(tc.path, tc.ft) + require.NoError(t, err) + } + }) + t.Run("path does not point to the correct file type", func(t *testing.T) { + tempSocket := newTempSocket(t) + defer tempSocket.Close() + + testCases := []struct { + path string + ft fileType + expectedErrorContains string + }{ + {"testdata/logfiles/example.log", socket, "provided path does not point to a socket file"}, + {tempSocket.pathToSocket, log, "provided path does not point to a log file:"}, + } + + for _, tc := range testCases { + err := checkFile(tc.path, tc.ft) + require.ErrorContains(t, err, tc.expectedErrorContains) + } + }) + + t.Run("with path to non existing file checkFile should return the error", func(t *testing.T) { + testCases := []struct { + path string + ft fileType + expectedErrorContains string + }{ + {"/foo/example.log", log, "provided path does not exist"}, + {"/foo/example.sock", socket, "provided path does not exist"}, + } + + for _, tc := range testCases { + err := checkFile(tc.path, tc.ft) + require.ErrorContains(t, err, tc.expectedErrorContains) + } + }) +} + +func TestLogMetricDataToValue(t *testing.T) { + testCases := []struct { + metricData string + expectedValue int + err error + }{ + {"010", 10, nil}, + {"00", 0, nil}, + {"5", 5, nil}, + {"-010", 0, errors.New("metric can't be negative")}, + {"", 0, fmt.Errorf("invalid syntax")}, + {"0Nax10", 0, fmt.Errorf("invalid syntax")}, + } + + t.Run("check correct returned values", func(t *testing.T) { + for _, tc := range testCases { + value, err := logMetricDataToValue(tc.metricData) + if tc.err != nil { + require.ErrorContains(t, err, tc.err.Error()) + continue + } + + require.NoError(t, err) + require.Equal(t, tc.expectedValue, value) + } + }) +}