1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,129 @@
# Intel Baseband Accelerator Input Plugin
This plugin collects metrics from both dedicated and integrated Intel devices
providing 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 commercial, off-the-shelf platforms
by integrating e.g.
- Forward Error Correction (FEC) processing,
- 4G Turbo FEC processing,
- 5G Low Density Parity Check (LDPC)
- Fast Fourier Transform (FFT) block providing DFT/iDFT processing offload for
the 5G Sounding Reference Signal (SRS)
⭐ Telegraf v1.27.0
🏷️ hardware, network, system
💻 linux
## Requirements
- supported Intel Baseband device installed and configured
- Linux kernel 5.7+
- [pf-bb-config](https://github.com/intel/pf-bb-config) (version >= v23.03)
installed and running
This plugin supports the following 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)
For more information regarding system configuration, please follow DPDK
installation guides:
- [Intel® vRAN Boost Poll Mode Driver (PMD)][VRB1]
- [Intel® ACC100 5G/4G FEC Poll Mode Drivers][ACC100]
[VRB1]: https://doc.dpdk.org/guides/bbdevs/vrb1.html#installation
[ACC100]: https://doc.dpdk.org/guides/bbdevs/acc100.html#installation
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
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\:<b>\:<d>.<f>.sock" where 0000\:<b>\:<d>.<f> 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\:<b>\:<d>.<f>.log" where 0000\:<b>\:<d>.<f> 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
```

View file

@ -0,0 +1,227 @@
//go:generate ../../../tools/readme_config_includer/generator
//go:build linux && amd64
package intel_baseband
import (
_ "embed"
"errors"
"fmt"
"strconv"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
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)
)
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 (*Baseband) SampleConfig() string {
return sampleConfig
}
func (b *Baseband) Init() error {
if b.SocketAccessTimeout < 0 {
return errors.New("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 errors.New("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": strconv.Itoa(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": strconv.Itoa(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()
})
}

View file

@ -0,0 +1,32 @@
//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 (*Baseband) SampleConfig() string { return sampleConfig }
func (b *Baseband) Init() error {
b.Log.Warn("Current platform is not supported")
return nil
}
func (*Baseband) Gather(_ telegraf.Accumulator) error { return nil }
func init() {
inputs.Add("intel_baseband", func() telegraf.Input {
return &Baseband{}
})
}

View file

@ -0,0 +1,179 @@
//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 {
// The Maximum length of the socket path is 104/108 characters, path created with t.TempDir() is too long for some cases
// (it combines test name with subtest name and some random numbers in the path). Therefore, in this case, it is safer to stick with `os.MkdirTemp()`.
//nolint:usetesting // Ignore "os.MkdirTemp() could be replaced by t.TempDir() in newTempSocket" finding.
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(t.TempDir(), "*.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
}

View file

@ -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.Equal(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 := parseNumVFs(line)
if err != nil {
lc.numVFs = -1
return err
}
lc.numVFs = numVFs
return nil
}
return errors.New("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, "", errors.New("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 := 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, errors.New("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, errors.New("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 parseNumVFs(s string) (int, error) {
i := strings.LastIndex(s, deviceStatusStartPrefix)
if i == -1 {
return 0, errors.New("couldn't find device status prefix in line")
}
j := strings.Index(s[i:], deviceStatusEndPrefix)
if j == -1 {
return 0, errors.New("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, errors.New("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 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,
}
}

View file

@ -0,0 +1,257 @@
//go:build linux && amd64
package intel_baseband
import (
"errors"
"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", ""},
{"", ""},
}
for _, tc := range testCases {
t.Run("expected "+tc.expected, func(t *testing.T) {
operationName := parseOperationName(tc.input)
require.Equal(t, tc.expected, operationName)
})
}
}
func prepareLogConnMock() *logConnector {
return &logConnector{
path: "",
numVFs: -1,
lastModTime: time.Time{},
}
}

View file

@ -0,0 +1,197 @@
// Code generated by mockery v2.46.3. DO NOT EDIT.
package mocks
import (
"net"
"time"
"github.com/stretchr/testify/mock"
)
// 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()
if len(ret) == 0 {
panic("no return value specified for Close")
}
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()
if len(ret) == 0 {
panic("no return value specified for LocalAddr")
}
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)
if len(ret) == 0 {
panic("no return value specified for Read")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
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()
if len(ret) == 0 {
panic("no return value specified for RemoteAddr")
}
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)
if len(ret) == 0 {
panic("no return value specified for SetDeadline")
}
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)
if len(ret) == 0 {
panic("no return value specified for SetReadDeadline")
}
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)
if len(ret) == 0 {
panic("no return value specified for SetWriteDeadline")
}
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)
if len(ret) == 0 {
panic("no return value specified for Write")
}
var r0 int
var r1 error
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
return rf(b)
}
if rf, ok := ret.Get(0).(func([]byte) int); ok {
r0 = rf(b)
} else {
r0 = ret.Get(0).(int)
}
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(b)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewConn creates a new instance of Conn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewConn(t interface {
mock.TestingT
Cleanup(func())
}) *Conn {
mock := &Conn{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -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\:<b>\:<d>.<f>.sock" where 0000\:<b>\:<d>.<f> 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\:<b>\:<d>.<f>.log" where 0000\:<b>\:<d>.<f> 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"

View file

@ -0,0 +1,102 @@
//go:build linux && amd64
package intel_baseband
import (
"errors"
"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 errors.New("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,
}
}

View file

@ -0,0 +1,77 @@
//go:build linux && amd64
package intel_baseband
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/influxdata/telegraf/plugins/inputs/intel_baseband/mocks"
)
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(errors.New("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, errors.New("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)
})
}

View file

@ -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

View file

@ -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, errors.New("metric can't be negative")
}
return value, nil
}

View file

@ -0,0 +1,182 @@
//go:build linux && amd64
package intel_baseband
import (
"errors"
"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.Empty(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.Empty(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.Empty(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, errors.New("invalid syntax")},
{"0Nax10", 0, errors.New("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)
}
})
}