From 4af59c5c7da5510b2fa85672996cf8ec33d38907 Mon Sep 17 00:00:00 2001 From: AbhishekJ Date: Thu, 5 Oct 2023 21:49:53 +0530 Subject: [PATCH] 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 --- .gitignore | 4 + README.md | 70 +++++++ debian_stable_docker_images.inc | 1 + metrics-exporter-api/debian/Dockerfile | 41 ++++ .../metrics-exporter-api.stable_docker_image | 4 + .../docker/metrics-exporter-api/.golangci.yml | 45 +++++ .../docker/metrics-exporter-api/Makefile | 45 +++++ .../docker/metrics-exporter-api/go.mod | 33 ++++ .../docker/metrics-exporter-api/go.sum | 50 +++++ .../docker/metrics-exporter-api/handler.go | 63 +++++++ .../metrics-exporter-api/handler_test.go | 43 +++++ .../metrics-exporter-api/json_handler.go | 120 ++++++++++++ .../metrics-exporter-api/json_handler_test.go | 81 ++++++++ .../docker/metrics-exporter-api/main.go | 61 ++++++ .../metrics-exporter-api/openmet_handler.go | 175 ++++++++++++++++++ .../openmet_handler_test.go | 79 ++++++++ .../docker/metrics-exporter-api/util.go | 92 +++++++++ .../docker/metrics-exporter-api/variable.go | 31 ++++ 18 files changed, 1038 insertions(+) create mode 100644 README.md create mode 100644 debian_stable_docker_images.inc create mode 100644 metrics-exporter-api/debian/Dockerfile create mode 100644 metrics-exporter-api/debian/metrics-exporter-api.stable_docker_image create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/.golangci.yml create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/Makefile create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/go.mod create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/go.sum create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/handler.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/handler_test.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/json_handler.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/json_handler_test.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/main.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/openmet_handler.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/openmet_handler_test.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/util.go create mode 100644 metrics-exporter-api/docker/metrics-exporter-api/variable.go diff --git a/.gitignore b/.gitignore index 78c457c..c4ce58a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,7 @@ cover AUTHORS ChangeLog *.sqlite +**bin +**go-compose +*.log +coverage.* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1ca28b --- /dev/null +++ b/README.md @@ -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]: +[OpenMetrics]: \ No newline at end of file diff --git a/debian_stable_docker_images.inc b/debian_stable_docker_images.inc new file mode 100644 index 0000000..0f045e5 --- /dev/null +++ b/debian_stable_docker_images.inc @@ -0,0 +1 @@ +metrics-exporter-api \ No newline at end of file diff --git a/metrics-exporter-api/debian/Dockerfile b/metrics-exporter-api/debian/Dockerfile new file mode 100644 index 0000000..c62fea8 --- /dev/null +++ b/metrics-exporter-api/debian/Dockerfile @@ -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"] \ No newline at end of file diff --git a/metrics-exporter-api/debian/metrics-exporter-api.stable_docker_image b/metrics-exporter-api/debian/metrics-exporter-api.stable_docker_image new file mode 100644 index 0000000..7447eb0 --- /dev/null +++ b/metrics-exporter-api/debian/metrics-exporter-api.stable_docker_image @@ -0,0 +1,4 @@ +BUILDER=docker +LABEL=metrics-exporter-api +DOCKER_CONTEXT=../docker +DOCKER_FILE=./Dockerfile \ No newline at end of file diff --git a/metrics-exporter-api/docker/metrics-exporter-api/.golangci.yml b/metrics-exporter-api/docker/metrics-exporter-api/.golangci.yml new file mode 100644 index 0000000..fbb5d6b --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/.golangci.yml @@ -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 \ No newline at end of file diff --git a/metrics-exporter-api/docker/metrics-exporter-api/Makefile b/metrics-exporter-api/docker/metrics-exporter-api/Makefile new file mode 100644 index 0000000..fd0e262 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/Makefile @@ -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 ../ diff --git a/metrics-exporter-api/docker/metrics-exporter-api/go.mod b/metrics-exporter-api/docker/metrics-exporter-api/go.mod new file mode 100644 index 0000000..a7735de --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/go.mod @@ -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 +) diff --git a/metrics-exporter-api/docker/metrics-exporter-api/go.sum b/metrics-exporter-api/docker/metrics-exporter-api/go.sum new file mode 100644 index 0000000..5ddaae5 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/go.sum @@ -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= diff --git a/metrics-exporter-api/docker/metrics-exporter-api/handler.go b/metrics-exporter-api/docker/metrics-exporter-api/handler.go new file mode 100644 index 0000000..b401e79 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/handler.go @@ -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) + } +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/handler_test.go b/metrics-exporter-api/docker/metrics-exporter-api/handler_test.go new file mode 100644 index 0000000..a5fe4c1 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/handler_test.go @@ -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() + +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/json_handler.go b/metrics-exporter-api/docker/metrics-exporter-api/json_handler.go new file mode 100644 index 0000000..96bb2b6 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/json_handler.go @@ -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://:/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://:/json/device/ +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://:/json/pci-addr/ +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) + } + +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/json_handler_test.go b/metrics-exporter-api/docker/metrics-exporter-api/json_handler_test.go new file mode 100644 index 0000000..66ea965 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/json_handler_test.go @@ -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) +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/main.go b/metrics-exporter-api/docker/metrics-exporter-api/main.go new file mode 100644 index 0000000..1879cf5 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/main.go @@ -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") +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/openmet_handler.go b/metrics-exporter-api/docker/metrics-exporter-api/openmet_handler.go new file mode 100644 index 0000000..024db67 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/openmet_handler.go @@ -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://:/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://:/device/ +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://:/pci-addr/ +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 +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/openmet_handler_test.go b/metrics-exporter-api/docker/metrics-exporter-api/openmet_handler_test.go new file mode 100644 index 0000000..d13938c --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/openmet_handler_test.go @@ -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) +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/util.go b/metrics-exporter-api/docker/metrics-exporter-api/util.go new file mode 100644 index 0000000..271a626 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/util.go @@ -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 +} diff --git a/metrics-exporter-api/docker/metrics-exporter-api/variable.go b/metrics-exporter-api/docker/metrics-exporter-api/variable.go new file mode 100644 index 0000000..3a07318 --- /dev/null +++ b/metrics-exporter-api/docker/metrics-exporter-api/variable.go @@ -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" +)