Node interface metrics application server code

This commit adds code in GO language to expose Physical Function
interface device info and statistics (metrics) of a node using
REST API service.
NIC Statistics provided by Netlink.

Following APIs are implemented:
/metrics -- all statistics in OpenMetrics format

/metrics/device/{DeviceName} -- particular device statistics in
OpenMetrics format

/metrics/pci-addr/{PciAddr} -- particular pci-address statistics in
OpenMetrics format

/json/metrics -- all metrics in json format

/json/metrics/device/{DeviceName} -- particular device statistics in
json format

/json/metrics/pci-addr/{PciAddr} -- particular pci-address statistics
in json format

Test Plan:
PASS: GO linting
PASS: Unit test
PASS: Api test.
PASS: Docker image build process defined
      here [1]
PASS: Created container image of this app, pushed to local registry
      and deployed on AIO-SX lab using sample deployment file.
      Then tested the APIs and validated the results.

Story: 2010918
Task: 48794

[1]https://docs.starlingx.io/developer_resources/build_docker_image.html

Change-Id: I5229b338b9e9afff3b02fe2389cfcd0c4e0590f6
Signed-off-by: AbhishekJ <abhishek.jaiswal@windriver.com>
This commit is contained in:
AbhishekJ 2023-10-05 21:49:53 +05:30
parent b718b0e230
commit 4af59c5c7d
18 changed files with 1038 additions and 0 deletions

4
.gitignore vendored
View File

@ -33,3 +33,7 @@ cover
AUTHORS
ChangeLog
*.sqlite
**bin
**go-compose
*.log
coverage.*

70
README.md Normal file
View File

@ -0,0 +1,70 @@
# app-node-interface-metrics-exporter
app-node-interface-metrics-exporter flux cd app
#### Top Level Directory Structure
```bash
├── app-node-interface-metrics-exporter # Root Folder
│   ├── debian_build_layer.cfg
│   ├── debian_iso_image.inc
│   ├── debian_pkg_dirs
│   ├── metrics-exporter-api # Go code for api server which will expose Metrics for NIC .
│   ├── python3-k8sapp-node-interface-metrics-exporter # lifecycle managemnt code to support flux apps
│   ├── README.md
│   ├── requirements.txt
│   ├── stx-node-interface-metrics-exporter-helm # helm Package manager for the app
│   ├── test-requirements.txt
│   └── tox.ini
```
> all command related to go code should run from `app-node-interface-metrics-exporter/metrics-exporter-api/docker/metrics-exporter-api`
### About metrics-exporter-api
It is Simple Http Server which reads network interface (PCI devices) and exposes following API's
1) http://{hostname}:{port}/metrics -- all node metrics in [OpenMetrics] format
2) http://{hostname}:{port}/metrics/device/{DeviceName} -- specified interface metrics identified by device name in [OpenMetrics] format
3) http://{hostname}:{port}/metrics/pci-addr/{PciAddr} -- specified interface metrics identified by PCI address in [OpenMetrics] format
4) http://{hostname}:{port}/json/metrics -- all node metrics in [JSON] format
5) http://{hostname}:{port}/json/metrics/device/{DeviceName} -- specified interface metrics identified by device name in [JSON] format
6) http://{hostname}:{port}/json/metrics/pci-addr/{PciAddr} -- specified interface metrics identified by PCI address in [JSON] format
#### Makefile Support
```bash
$ make
help: Show this help.
install_dep: install go dependency
run: run app on host machine
test: run go unit test
testcov: run go coverage test
vet: run go vet
lint: run go lint
build_linux: Build application
build_image: Build docker image
```
> `$ make run ` will start go dev http server
#### Run time Options / params for the metrics-exporter-app usage
| Options | Help |
| ------ | ------ |
| `-log.file` | Log file name (default "node_metrics_api.log") |
| `-log.level` | log level. (default "info"). Valid options trace, debug, info, warning, error, fatal and panic |
| `-log.file` | Log file name (default "node_metrics_api.log") |
| `-web.listen-address` | Port to listen on for web interface. (default ":9110") |
| `-path.sys` | mounted path for host /sys inside container (default "/sys") |
#### Local / Devlopment Set UP for metrics-exporter-api
`pre requisite go 1.21.0`
#### Container image reference for helm
[Dockerfile](/metrics-exporter-api/debian/Dockerfile)
[Build Reference](/metrics-exporter-api/debian/metrics-exporter-api.stable_docker_image)
#### References
[StarlingX](https://www.starlingx.io/)
[How to add a FluxCD App to Starlingx](https://wiki.openstack.org/wiki/StarlingX/Containers/HowToAddNewFluxCDAppInSTX)
[OpenMetrics](https://openmetrics.io/)
[JSON]: <https://www.json.org/>
[OpenMetrics]: <https://openmetrics.io/>

View File

@ -0,0 +1 @@
metrics-exporter-api

View File

@ -0,0 +1,41 @@
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# All Rights Reserved.
#
# ARG BASE
# FROM ${BASE}
# Build Stage
FROM golang:alpine3.17 as buildstage
# Set destination for COPY
WORKDIR /app
COPY metrics-exporter-api /app/
RUN go mod download
# Build
RUN CGO_ENABLED=0 GOOS=linux go build -o /metrics-api-server
# Deploy binary which will make image size smaller
FROM alpine:latest
# Set workdir context of current path wrt image
WORKDIR /
COPY --from=buildstage /metrics-api-server /metrics-api-server
# Optional:
# To bind to a TCP port, runtime parameters must be supplied to the
# docker command.
# But we can document in the Dockerfile what ports
# the application is going to listen on by default.
# https://docs.docker.com/engine/reference/builder/#expose
EXPOSE 9110
ENTRYPOINT ["/metrics-api-server"]

View File

@ -0,0 +1,4 @@
BUILDER=docker
LABEL=metrics-exporter-api
DOCKER_CONTEXT=../docker
DOCKER_FILE=./Dockerfile

View File

@ -0,0 +1,45 @@
# Copyright (c) 2023 Wind River Systems, Inc.
# SPDX-License-Identifier: Apache-2.0
# All Rights Reserved.
run:
deadline: 5m
tests: true
skip-dirs-use-default: true
skip-files-use-default: true
skip-dirs:
- vendor
skip-files:
- bindata.go
linters:
enable-all: false
enable:
- unused
- revive
- deadcode
- gosec
- govet
- goimports
- gomodguard
- dupword
- godox
- misspell
- decorder
- gofmt
- sloglint
- tagalign
# Enable presets.
# https://golangci-lint.run/usage/linters
presets:
- bugs
- complexity
- error
- metalinter
- performance
- sql
- test
- unused
# Run only fast linters from enabled linters set (first run won't be fast)
# Default: false
fast: true

View File

@ -0,0 +1,45 @@
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# All Rights Reserved.
#
# variables
GO ?= go
GOCOVER ?= $(GO) tool cover
LABEL ?= metrics-exporter-api
# Targets
help: ## Show this help.
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
apphelp: ## Show this help.
$(GO) run . -h
install_dep: ## install go dependency
$(GO) mod tidy
run: ## run app on host machine
$(GO) run .
test: ## run go unit test
$(GO) test ./...
testcov: ## run go coverage test
$(GO) test -coverprofile=coverage.out ./...
$(GO) tool cover -func=coverage.out
$(GO) tool cover -html=coverage.out -o coverage.html
vet: ## run go vet
$(GO) vet
lint: ## run go lint
golangci-lint run
build_linux: ## Build application
CGO_ENABLED=0 GOOS=linux go build -o metrics-api-server
build_image: ## Build docker image
docker build -f ../../debian/Dockerfile -t starlingx/metrics-exporter-api ../

View File

@ -0,0 +1,33 @@
// Copyright (c) 2023 Wind River Systems, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// All Rights Reserved.
module opendev.org/starlingx/app-node-interface-metrics-exporter/metrics-exporter-api/docker/metrics-exporter-api
go 1.21.0
require (
github.com/bsm/openmetrics v0.3.1
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/iancoleman/strcase v0.3.0
github.com/rs/cors v1.10.1
github.com/safchain/ethtool v0.3.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.7.0
github.com/vishvananda/netlink v1.1.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

View File

@ -0,0 +1,50 @@
github.com/bsm/openmetrics v0.3.1 h1:nhR6QgaKaDmnbnvVP9R0JyPExt8Qa+n1cJk/ouGC4FY=
github.com/bsm/openmetrics v0.3.1/go.mod h1:tabLMhjVjhdhFuwm9YenEVx0s54uvu56faEwYgD6L2g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,63 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/rs/cors"
log "github.com/sirupsen/logrus"
)
// Function to create route/handler for web request
func allHandlers() http.Handler {
router := mux.NewRouter()
// allowed CORS
// more info https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
handler := cors.Default().Handler(router)
router.HandleFunc("/", rootGet).Methods("GET")
router.HandleFunc("/healthz", healthzGet).Methods("GET")
// Openmetrics endpoints
router.HandleFunc("/metrics", metricsGet).Methods("GET")
router.HandleFunc("/metrics/device/{DeviceName}", deviceGet).Methods("GET")
router.HandleFunc("/metrics/pci-addr/{PciAddr}", pciAddrGet).Methods("GET")
// json metrics endpoints
router.HandleFunc("/json/metrics", metricsGetJSON).Methods("GET")
router.HandleFunc("/json/metrics/device/{DeviceName}", deviceGetJSON).Methods("GET")
router.HandleFunc("/json/metrics/pci-addr/{PciAddr}", pciAddrGetJSON).Methods("GET")
return handler
}
// this endpoint shows the uptime of the application
// it may be helpful to create probes in K8s
func healthzGet(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
response := fmt.Sprintf("uptime: %s\n", time.Since(time.Unix(0, StartupTime)))
_, err := w.Write([]byte(response))
if err != nil {
log.Error(err)
}
}
// Since nothing is to show on root handler
func rootGet(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
response := fmt.Sprintf("/ root uptime: %s\n", time.Since(time.Unix(0, StartupTime)))
_, err := w.Write([]byte(response))
if err != nil {
log.Error(err)
}
}

View File

@ -0,0 +1,43 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRootGet(t *testing.T) {
t.Parallel()
r, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
rootGet(w, r)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestHealthzGet(t *testing.T) {
t.Parallel()
r, _ := http.NewRequest("GET", "/healthz", nil)
w := httptest.NewRecorder()
healthzGet(w, r)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestHandlerFunc(t *testing.T) {
t.Parallel()
}

View File

@ -0,0 +1,120 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
// endpoint to get network metric of a node on which it is resides
// http://<hostname>:<port>/json/metrics
func metricsGetJSON(w http.ResponseWriter, _ *http.Request) {
DeviceStat := ListAllNetDev()
// convert the map to a JSON encoded byte slice
jsonContent, mErr := json.Marshal(DeviceStat)
if mErr != nil {
fmt.Println(mErr)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write(jsonContent)
if err != nil {
log.Error(err)
}
}
// endpoint to fetch metrics related to given network
// device by name
// http://<hostname>:<port>/json/device/<DeviceName>
func deviceGetJSON(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
DeviceName := params["DeviceName"]
allDeviceStat := ListAllNetDev()
devStats, ok := allDeviceStat[DeviceName]
// If the key exists
if ok {
// convert the map to a JSON encoded byte slice
jsonContent, mErr := json.Marshal(devStats)
if mErr != nil {
log.Error(mErr)
return
}
// convert the byte slice to a string
// jsonString := string(jsonContent)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write(jsonContent)
if err != nil {
log.Error(err)
}
} else {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, " %s Device Not Found ", DeviceName)
}
}
// endpoint to fetch metrics related to given network
// device by pci addr
// http://<hostname>:<port>/json/pci-addr/<PciAddr>
func pciAddrGetJSON(w http.ResponseWriter, r *http.Request) {
found := false
params := mux.Vars(r)
PciAddr := params["PciAddr"]
allDeviceStat := ListAllNetDev()
for _, dev := range allDeviceStat {
if dev.Pciaddr == PciAddr {
found = true
// convert the map to a JSON encoded byte slice
jsonContent, mErr := json.Marshal(dev)
if mErr != nil {
log.Error(mErr)
return
}
// convert the byte slice to a string
// jsonString := string(jsonContent)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write(jsonContent)
if err != nil {
log.Error(err)
}
}
}
if !found {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, " %s Pci Addr Not found ", PciAddr)
}
}

View File

@ -0,0 +1,81 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestMetricsGetJson(t *testing.T) {
t.Parallel()
// /device/{DeviceName}
r, _ := http.NewRequest("GET", "/json/metrics/", nil)
w := httptest.NewRecorder()
metricsGetJSON(w, r)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, w.Header().Get("Content-Type"), "application/json")
}
func TestDeviceGetJson(t *testing.T) {
t.Parallel()
// /device/{DeviceName}
r, _ := http.NewRequest("GET", "/json/device/", nil)
w := httptest.NewRecorder()
// Hack to try to fake gorilla/mux vars
vars := map[string]string{
"DeviceName": "eth0",
}
r = mux.SetURLVars(r, vars)
deviceGetJSON(w, r)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, w.Header().Get("Content-Type"), "application/json")
}
func TestDeviceGetJsonNeg(t *testing.T) {
t.Parallel()
// /device/{DeviceName}
r, _ := http.NewRequest("GET", "/json/device/", nil)
w := httptest.NewRecorder()
// Hack to try to fake gorilla/mux vars
vars := map[string]string{
"DeviceName": "404",
}
r = mux.SetURLVars(r, vars)
deviceGetJSON(w, r)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestPciAddrGetJsonNeg(t *testing.T) {
t.Parallel()
r, _ := http.NewRequest("GET", "/json/pci-addr/", nil)
w := httptest.NewRecorder()
// Hack to try to fake gorilla/mux vars
vars := map[string]string{
"PciAddr": "11:00:00",
}
r = mux.SetURLVars(r, vars)
pciAddrGetJSON(w, r)
assert.Equal(t, http.StatusNotFound, w.Code)
}

View File

@ -0,0 +1,61 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"flag"
"io"
"net/http"
"os"
"sync/atomic"
"time"
"github.com/gorilla/handlers"
log "github.com/sirupsen/logrus"
)
func main() {
handler := allHandlers()
// StartUp Time used to calculate Uptime for the App
atomic.StoreInt64(&StartupTime, time.Now().UnixNano())
// Parse the flags or commandline arguments
flag.Parse()
// log setup
logFile := OpenFile(*logFileName)
// close file on exit
defer logFile.Close()
logOutput := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(logOutput)
logLevel, _ := log.ParseLevel(*logLevel)
// Only log the debug severity or above
log.SetLevel(logLevel)
// Print all flags what valuse is used
flag.VisitAll(func(f *flag.Flag) {
log.Infof("%s: %s", f.Name, f.Value)
})
loggedRouter := handlers.LoggingHandler(os.Stdout, handler)
server := &http.Server{
Addr: *addr,
ReadHeaderTimeout: 3 * time.Second,
Handler: loggedRouter,
}
// Starting Http server
err := server.ListenAndServe()
if err != nil {
panic(err)
}
// on exit log
log.Info("Server stopped\n")
}

View File

@ -0,0 +1,175 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"bytes"
"fmt"
"net/http"
"reflect"
"github.com/bsm/openmetrics"
"github.com/gorilla/mux"
"github.com/iancoleman/strcase"
log "github.com/sirupsen/logrus"
)
// endpoint to get all network metric of a node on which it is resides
// http://<hostname>:<port>/metrics
func metricsGet(w http.ResponseWriter, _ *http.Request) {
openMetContent := allDevInfoOpenMet()
w.Header().Set("Content-Type", OpenMetContentType)
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(openMetContent))
if err != nil {
log.Error(err)
}
}
// endpoint to fetch metrics related to given network
// device by name
// http://<hostname>:<port>/device/<DeviceName>
func deviceGet(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
devName := params["DeviceName"]
allDeviceStat := ListAllNetDev()
devStats, ok := allDeviceStat[devName]
// If the key exists
if ok {
devStatOpnFmt := devStatOpenMet(devStats)
w.Header().Set("Content-Type", OpenMetContentType)
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(devStatOpnFmt))
if err != nil {
log.Error(err)
}
} else {
w.Header().Set("Content-Type", OpenMetContentType)
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, " %s Device Not Found ", devName)
}
}
// endpoint to fetch metrics related to given network
// device by pci addr
// http://<hostname>:<port>/pci-addr/<PciAddr>
func pciAddrGet(w http.ResponseWriter, r *http.Request) {
found := false
params := mux.Vars(r)
PciAddr := params["PciAddr"]
allDeviceStat := ListAllNetDev()
for _, devStats := range allDeviceStat {
if devStats.Pciaddr == PciAddr {
found = true
DevStatOpnFmt := devStatOpenMet(devStats)
w.Header().Set("Content-Type", OpenMetContentType)
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(DevStatOpnFmt))
if err != nil {
log.Error(err)
}
}
}
if !found {
w.Header().Set("Content-Type", OpenMetContentType)
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, " %s Pci Addr Not found ", PciAddr)
}
}
// function to register and update network device info
// and statistics to Openmetrics format
func allDevInfoOpenMet() string {
// get all device info from lspci command
allDeviceInfo := ListAllNetDev()
// store stats data in openmetrics
var openMetContent string
for _, device := range allDeviceInfo {
openMetContent += devStatOpenMet(device)
}
return openMetContent
}
func devStatOpenMet(devInfo DevInfo) string {
var reg = openmetrics.NewRegistry()
var openMetVar = regDevInfo(reg)
openMetVar.With(
devInfo.Name,
devInfo.HardwareAddr,
devInfo.Broadcast,
// devInfo.duplex,
devInfo.Alias,
devInfo.OperState,
devInfo.Pciaddr,
)
// convert all netlink.LinkStatistics Struct to map
fields := reflect.TypeOf(*devInfo.Statistics)
// valPtr := reflect.ValueOf(devInfo.Statistics)
values := reflect.Indirect(reflect.ValueOf(devInfo.Statistics))
// get the number of field for looping n times
num := fields.NumField()
for i := 0; i < num; i++ {
field := fields.Field(i)
value := values.Field(i)
log.Debug("Type:", field.Type, ",", field.Name, "=", value, "\n")
// register counter
name := "network_interface_" + strcase.ToSnake(field.Name)
var newInfo = reg.Counter(openmetrics.Desc{
Name: name,
Help: name,
Labels: []string{"device"},
})
newInfo.With(
devInfo.Name,
).Add(float64(value.Uint()))
}
// create buffer to return
var buf bytes.Buffer
if _, err := reg.WriteTo(&buf); err != nil {
panic(err)
}
// buffer.String()
return buf.String()
}
// function to create Informational Openmetrics paramater / variable
// for Network Interface Device Info
func regDevInfo(reg *openmetrics.Registry) openmetrics.InfoFamily {
var deviceInfo = reg.Info(openmetrics.Desc{
Name: "network_interface_device",
Help: "network_interface_device ",
Labels: []string{"name", "address", "broadcast", "ifalias", "operstate", "pciaddr"},
})
return deviceInfo
}

View File

@ -0,0 +1,79 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
func TestMetricsGet(t *testing.T) {
t.Parallel()
// /device/{DeviceName}
r, _ := http.NewRequest("GET", "/json/metrics/", nil)
w := httptest.NewRecorder()
metricsGet(w, r)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestDeviceGet(t *testing.T) {
t.Parallel()
// /device/{DeviceName}
r, _ := http.NewRequest("GET", "/json/device/", nil)
w := httptest.NewRecorder()
// Hack to try to fake gorilla/mux vars
vars := map[string]string{
"DeviceName": "eth0",
}
r = mux.SetURLVars(r, vars)
deviceGet(w, r)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestDeviceGetNeg(t *testing.T) {
t.Parallel()
// /device/{DeviceName}
r, _ := http.NewRequest("GET", "/json/device/", nil)
w := httptest.NewRecorder()
// Hack to try to fake gorilla/mux vars
vars := map[string]string{
"DeviceName": "404",
}
r = mux.SetURLVars(r, vars)
deviceGet(w, r)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestPciAddrGetNeg(t *testing.T) {
t.Parallel()
r, _ := http.NewRequest("GET", "/json/pci-addr/", nil)
w := httptest.NewRecorder()
// Hack to try to fake gorilla/mux vars
vars := map[string]string{
"PciAddr": "11:00:00",
}
r = mux.SetURLVars(r, vars)
pciAddrGet(w, r)
assert.Equal(t, http.StatusNotFound, w.Code)
}

View File

@ -0,0 +1,92 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import (
"os"
"github.com/safchain/ethtool"
log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
)
// DevInfo Data structures to store Device info.
type DevInfo struct {
Name string
Type string
HardwareAddr string
OperState string
EncapType string
Alias string
Pciaddr string
Broadcast string
Statistics *netlink.LinkStatistics
}
// OpenFile function
func OpenFile(fileName string) *os.File {
logFile := fileName
// Open logfile
f, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Info("Failed to create logfile" + logFile)
log.Error(err)
panic(err)
}
return f
}
// ListAllNetDev func to findout all the Pcie Devices and its statistics
// which have network or ethernet in Classname
// func ListAllPciNetDev(sysPath string) map[string]map[string]string {
func ListAllNetDev() map[string]DevInfo {
// make the list of dict containing DevInfo
allDeviceInfo := make(map[string]DevInfo)
allNetDevices, _ := netlink.LinkList()
for _, dev := range allNetDevices {
deviceName := dev.Attrs().Name
pciaddr, err := ethtool.BusInfo(dev.Attrs().Name)
if err != nil {
log.Infof(
"Unable to fetch Info from ethtool for %s, err: %s",
deviceName, err,
)
}
var broadcast string
// try to fetch Broadcast Address
addr, err := netlink.AddrList(dev, netlink.NewRule().Family)
// we know that all device will not have Broadcast addr
// so just logged the name of devices which are not
if err != nil {
log.Info("Unable to fetch AddrList ", err)
}
if len(addr) > 0 {
broadcast = addr[0].Broadcast.String()
}
allDeviceInfo[deviceName] = DevInfo{
Name: deviceName,
Type: dev.Type(),
HardwareAddr: dev.Attrs().HardwareAddr.String(),
OperState: dev.Attrs().OperState.String(),
EncapType: dev.Attrs().EncapType,
Alias: dev.Attrs().Alias,
Pciaddr: pciaddr,
Broadcast: broadcast,
Statistics: dev.Attrs().Statistics,
}
}
return allDeviceInfo
}

View File

@ -0,0 +1,31 @@
/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
All Rights Reserved.
*/
package main
import "flag"
var (
addr = flag.String(
"web.listen-address", ":9110", "Port to listen on for web interface.",
)
logLevel = flag.String(
"log.level", "info", "log level. Valid options trace,"+
" debug, info, warning, error, fatal and panic",
)
logFileName = flag.String(
"log.file", "node_metrics_api.log", "Log file name",
)
// StartupTime string
StartupTime int64
// OpenMetContentType content Type
// OpenMetContentType = "application/openmetrics-text; version=1.0.0; charset=utf-8"
OpenMetContentType = "text/plain"
)