mirror of
https://github.com/ClusterCockpit/cc-metric-collector.git
synced 2025-07-19 11:21:41 +02:00
Compare commits
5 Commits
snmp_recei
...
http_stats
Author | SHA1 | Date | |
---|---|---|---|
|
9dd6ff1a76 | ||
|
257b4a64b5 | ||
|
5eeb097136 | ||
|
4a4992877c | ||
|
9447685a69 |
4
.github/ci-config.json
vendored
4
.github/ci-config.json
vendored
@@ -3,6 +3,6 @@
|
||||
"collectors" : ".github/ci-collectors.json",
|
||||
"receivers" : ".github/ci-receivers.json",
|
||||
"router" : ".github/ci-router.json",
|
||||
"interval": "5s",
|
||||
"duration": "1s"
|
||||
"interval": 5,
|
||||
"duration": 1
|
||||
}
|
||||
|
4
.github/ci-sinks.json
vendored
4
.github/ci-sinks.json
vendored
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"testoutput" : {
|
||||
"type" : "stdout",
|
||||
"meta_as_tags" : [
|
||||
"unit"
|
||||
]
|
||||
"meta_as_tags" : true
|
||||
}
|
||||
}
|
||||
|
149
.github/workflows/Release.yml
vendored
149
.github/workflows/Release.yml
vendored
@@ -8,17 +8,16 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
#
|
||||
# Build on AlmaLinux 8 using go-toolset
|
||||
# Build on AlmaLinux 8.5 using go-toolset
|
||||
#
|
||||
AlmaLinux-RPM-build:
|
||||
runs-on: ubuntu-latest
|
||||
# See: https://hub.docker.com/_/almalinux
|
||||
container: almalinux:8
|
||||
container: almalinux:8.5
|
||||
# The job outputs link to the outputs of the 'rpmrename' step
|
||||
# Only job outputs can be used in child jobs
|
||||
outputs:
|
||||
@@ -28,46 +27,37 @@ jobs:
|
||||
|
||||
# Use dnf to install development packages
|
||||
- name: Install development packages
|
||||
run: |
|
||||
dnf --assumeyes group install "Development Tools" "RPM Development Tools"
|
||||
dnf --assumeyes install wget openssl-devel diffutils delve which
|
||||
run: dnf --assumeyes group install "Development Tools" "RPM Development Tools"
|
||||
|
||||
# Checkout git repository and submodules
|
||||
# fetch-depth must be 0 to use git describe
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Use dnf to install build dependencies
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf --assumeyes install \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-bin-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-src-1.20.6-2.module_el8+658+f14b2092.noarch.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/go-toolset-1.20.6-1.module_el8+602+8bb8a8d6.x86_64.rpm
|
||||
run: dnf --assumeyes builddep scripts/cc-metric-collector.spec
|
||||
|
||||
- name: RPM build MetricCollector
|
||||
id: rpmbuild
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/cc-metric-collector/cc-metric-collector
|
||||
make RPM
|
||||
|
||||
# AlmaLinux 8 is a derivate of RedHat Enterprise Linux 8 (UBI8),
|
||||
run: make RPM
|
||||
|
||||
# AlmaLinux 8.5 is a derivate of RedHat Enterprise Linux 8 (UBI8),
|
||||
# so the created RPM both contain the substring 'el8' in the RPM file names
|
||||
# This step replaces the substring 'el8' to 'alma8'. It uses the move operation
|
||||
# because it is unclear whether the default AlmaLinux 8 container contains the
|
||||
# This step replaces the substring 'el8' to 'alma85'. It uses the move operation
|
||||
# because it is unclear whether the default AlmaLinux 8.5 container contains the
|
||||
# 'rename' command. This way we also get the new names for output.
|
||||
- name: Rename RPMs (s/el8/alma8/)
|
||||
- name: Rename RPMs (s/el8/alma85/)
|
||||
id: rpmrename
|
||||
run: |
|
||||
OLD_RPM="${{steps.rpmbuild.outputs.RPM}}"
|
||||
OLD_SRPM="${{steps.rpmbuild.outputs.SRPM}}"
|
||||
NEW_RPM="${OLD_RPM/el8/alma8}"
|
||||
NEW_SRPM=${OLD_SRPM/el8/alma8}
|
||||
NEW_RPM="${OLD_RPM/el8/alma85}"
|
||||
NEW_SRPM=${OLD_SRPM/el8/alma85}
|
||||
mv "${OLD_RPM}" "${NEW_RPM}"
|
||||
mv "${OLD_SRPM}" "${NEW_SRPM}"
|
||||
echo "::set-output name=SRPM::${NEW_SRPM}"
|
||||
@@ -77,12 +67,12 @@ jobs:
|
||||
- name: Save RPM as artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector RPM for AlmaLinux 8
|
||||
name: cc-metric-collector RPM for AlmaLinux 8.5
|
||||
path: ${{ steps.rpmrename.outputs.RPM }}
|
||||
- name: Save SRPM as artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector SRPM for AlmaLinux 8
|
||||
name: cc-metric-collector SRPM for AlmaLinux 8.5
|
||||
path: ${{ steps.rpmrename.outputs.SRPM }}
|
||||
|
||||
#
|
||||
@@ -90,8 +80,8 @@ jobs:
|
||||
#
|
||||
UBI-8-RPM-build:
|
||||
runs-on: ubuntu-latest
|
||||
# See: https://catalog.redhat.com/software/containers/ubi8/ubi/5c35984d70cc534b3a3784e?container-tabs=gti
|
||||
container: registry.access.redhat.com/ubi8/ubi:8.8-1032.1692772289
|
||||
# See: https://catalog.redhat.com/software/containers/ubi8/ubi/5c359854d70cc534b3a3784e?container-tabs=gti
|
||||
container: registry.access.redhat.com/ubi8/ubi:8.5-226.1645809065
|
||||
# The job outputs link to the outputs of the 'rpmbuild' step
|
||||
outputs:
|
||||
rpm : ${{steps.rpmbuild.outputs.RPM}}
|
||||
@@ -100,31 +90,24 @@ jobs:
|
||||
|
||||
# Use dnf to install development packages
|
||||
- name: Install development packages
|
||||
run: dnf --assumeyes --disableplugin=subscription-manager install rpm-build go-srpm-macros rpm-build-libs rpm-libs gcc make python38 git wget openssl-devel diffutils delve which
|
||||
run: dnf --assumeyes --disableplugin=subscription-manager install rpm-build go-srpm-macros rpm-build-libs rpm-libs gcc make python38 git
|
||||
|
||||
# Checkout git repository and submodules
|
||||
# fetch-depth must be 0 to use git describe
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Use dnf to install build dependencies
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf --assumeyes --disableplugin=subscription-manager install \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-bin-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-src-1.20.6-2.module_el8+658+f14b2092.noarch.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/go-toolset-1.20.6-1.module_el8+602+8bb8a8d6.x86_64.rpm
|
||||
run: dnf --assumeyes --disableplugin=subscription-manager builddep scripts/cc-metric-collector.spec
|
||||
|
||||
- name: RPM build MetricCollector
|
||||
id: rpmbuild
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/cc-metric-collector/cc-metric-collector
|
||||
make RPM
|
||||
run: make RPM
|
||||
|
||||
# See: https://github.com/actions/upload-artifact
|
||||
- name: Save RPM as artifact
|
||||
@@ -138,76 +121,25 @@ jobs:
|
||||
name: cc-metric-collector SRPM for UBI 8
|
||||
path: ${{ steps.rpmbuild.outputs.SRPM }}
|
||||
|
||||
#
|
||||
# Build on Ubuntu 22.04 using official go package
|
||||
#
|
||||
Ubuntu-jammy-build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:22.04
|
||||
# The job outputs link to the outputs of the 'debrename' step
|
||||
# Only job outputs can be used in child jobs
|
||||
outputs:
|
||||
deb : ${{steps.debrename.outputs.DEB}}
|
||||
steps:
|
||||
# Use apt to install development packages
|
||||
- name: Install development packages
|
||||
run: |
|
||||
apt update && apt --assume-yes upgrade
|
||||
apt --assume-yes install build-essential sed git wget bash
|
||||
# Checkout git repository and submodules
|
||||
# fetch-depth must be 0 to use git describe
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
# Use official golang package
|
||||
- name: Install Golang
|
||||
run: |
|
||||
wget -q https://go.dev/dl/go1.21.1.linux-amd64.tar.gz --output-document=- | \
|
||||
tar --directory=/usr/local --extract --gzip
|
||||
export PATH=/usr/local/go/bin:/usr/local/go/pkg/tool/linux_amd64:$PATH
|
||||
go version
|
||||
- name: DEB build MetricCollector
|
||||
id: dpkg-build
|
||||
run: |
|
||||
export PATH=/usr/local/go/bin:/usr/local/go/pkg/tool/linux_amd64:$PATH
|
||||
git config --global --add safe.directory /__w/cc-metric-collector/cc-metric-collector
|
||||
make DEB
|
||||
- name: Rename DEB (add '_ubuntu22.04')
|
||||
id: debrename
|
||||
run: |
|
||||
OLD_DEB_NAME=$(echo "${{steps.dpkg-build.outputs.DEB}}" | rev | cut -d '.' -f 2- | rev)
|
||||
NEW_DEB_FILE="${OLD_DEB_NAME}_ubuntu22.04.deb"
|
||||
mv "${{steps.dpkg-build.outputs.DEB}}" "${NEW_DEB_FILE}"
|
||||
echo "::set-output name=DEB::${NEW_DEB_FILE}"
|
||||
# See: https://github.com/actions/upload-artifact
|
||||
- name: Save DEB as artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector DEB for Ubuntu 22.04
|
||||
path: ${{ steps.debrename.outputs.DEB }}
|
||||
|
||||
#
|
||||
# Create release with fresh RPMs
|
||||
#
|
||||
Release:
|
||||
runs-on: ubuntu-latest
|
||||
# We need the RPMs, so add dependency
|
||||
needs: [AlmaLinux-RPM-build, UBI-8-RPM-build, Ubuntu-jammy-build]
|
||||
needs: [AlmaLinux-RPM-build, UBI-8-RPM-build]
|
||||
|
||||
steps:
|
||||
# See: https://github.com/actions/download-artifact
|
||||
- name: Download AlmaLinux 8 RPM
|
||||
- name: Download AlmaLinux 8.5 RPM
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector RPM for AlmaLinux 8
|
||||
- name: Download AlmaLinux 8 SRPM
|
||||
name: cc-metric-collector RPM for AlmaLinux 8.5
|
||||
- name: Download AlmaLinux 8.5 SRPM
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector SRPM for AlmaLinux 8
|
||||
|
||||
name: cc-metric-collector SRPM for AlmaLinux 8.5
|
||||
|
||||
- name: Download UBI 8 RPM
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
@@ -217,11 +149,6 @@ jobs:
|
||||
with:
|
||||
name: cc-metric-collector SRPM for UBI 8
|
||||
|
||||
- name: Download Ubuntu 22.04 DEB
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector DEB for Ubuntu 22.04
|
||||
|
||||
# The download actions do not publish the name of the downloaded file,
|
||||
# so we re-use the job outputs of the parent jobs. The files are all
|
||||
# downloaded to the current folder.
|
||||
@@ -231,21 +158,18 @@ jobs:
|
||||
- name: Set RPM variables
|
||||
id: files
|
||||
run: |
|
||||
ALMA_8_RPM=$(basename "${{ needs.AlmaLinux-RPM-build.outputs.rpm}}")
|
||||
ALMA_8_SRPM=$(basename "${{ needs.AlmaLinux-RPM-build.outputs.srpm}}")
|
||||
ALMA_85_RPM=$(basename "${{ needs.AlmaLinux-RPM-build.outputs.rpm}}")
|
||||
ALMA_85_SRPM=$(basename "${{ needs.AlmaLinux-RPM-build.outputs.srpm}}")
|
||||
UBI_8_RPM=$(basename "${{ needs.UBI-8-RPM-build.outputs.rpm}}")
|
||||
UBI_8_SRPM=$(basename "${{ needs.UBI-8-RPM-build.outputs.srpm}}")
|
||||
U_2004_DEB=$(basename "${{ needs.Ubuntu-focal-build.outputs.deb}}")
|
||||
echo "ALMA_8_RPM::${ALMA_8_RPM}"
|
||||
echo "ALMA_8_SRPM::${ALMA_8_SRPM}"
|
||||
echo "ALMA_85_RPM::${ALMA_85_RPM}"
|
||||
echo "ALMA_85_SRPM::${ALMA_85_SRPM}"
|
||||
echo "UBI_8_RPM::${UBI_8_RPM}"
|
||||
echo "UBI_8_SRPM::${UBI_8_SRPM}"
|
||||
echo "U_2004_DEB::${U_2004_DEB}"
|
||||
echo "::set-output name=ALMA_8_RPM::${ALMA_8_RPM}"
|
||||
echo "::set-output name=ALMA_8_SRPM::${ALMA_8_SRPM}"
|
||||
echo "::set-output name=ALMA_85_RPM::${ALMA_85_RPM}"
|
||||
echo "::set-output name=ALMA_85_SRPM::${ALMA_85_SRPM}"
|
||||
echo "::set-output name=UBI_8_RPM::${UBI_8_RPM}"
|
||||
echo "::set-output name=UBI_8_SRPM::${UBI_8_SRPM}"
|
||||
echo "::set-output name=U_2004_DEB::${U_2004_DEB}"
|
||||
|
||||
# See: https://github.com/softprops/action-gh-release
|
||||
- name: Release
|
||||
@@ -254,8 +178,7 @@ jobs:
|
||||
with:
|
||||
name: cc-metric-collector-${{github.ref_name}}
|
||||
files: |
|
||||
${{ steps.files.outputs.ALMA_8_RPM }}
|
||||
${{ steps.files.outputs.ALMA_8_SRPM }}
|
||||
${{ steps.files.outputs.ALMA_85_RPM }}
|
||||
${{ steps.files.outputs.ALMA_85_SRPM }}
|
||||
${{ steps.files.outputs.UBI_8_RPM }}
|
||||
${{ steps.files.outputs.UBI_8_SRPM }}
|
||||
${{ steps.files.outputs.U_2004_DEB }}
|
||||
${{ steps.files.outputs.UBI_8_SRPM }}
|
151
.github/workflows/runonce.yml
vendored
151
.github/workflows/runonce.yml
vendored
@@ -4,31 +4,32 @@
|
||||
name: Run Test
|
||||
|
||||
# Run on event push
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
|
||||
#
|
||||
# Job build-1-20
|
||||
# Build on latest Ubuntu using golang version 1.20
|
||||
# Job build-1-17
|
||||
# Build on latest Ubuntu using golang version 1.17
|
||||
#
|
||||
build-1-20:
|
||||
build-1-17:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
# Checkout git repository and submodules
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# See: https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: '^1.17.7'
|
||||
|
||||
# Install libganglia
|
||||
- name: Setup Ganglia
|
||||
run: sudo apt install ganglia-monitor libganglia1
|
||||
|
||||
- name: Build MetricCollector
|
||||
run: make
|
||||
@@ -37,141 +38,31 @@ jobs:
|
||||
run: ./cc-metric-collector --once --config .github/ci-config.json
|
||||
|
||||
#
|
||||
# Job build-1-21
|
||||
# Build on latest Ubuntu using golang version 1.21
|
||||
# Job build-1-16
|
||||
# Build on latest Ubuntu using golang version 1.16
|
||||
#
|
||||
build-1-21:
|
||||
build-1-16:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
# Checkout git repository and submodules
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# See: https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '^1.16.7' # The version AlmaLinux 8.5 uses
|
||||
|
||||
# Install libganglia
|
||||
- name: Setup Ganglia
|
||||
run: sudo apt install ganglia-monitor libganglia1
|
||||
|
||||
- name: Build MetricCollector
|
||||
run: make
|
||||
|
||||
- name: Run MetricCollector once
|
||||
run: ./cc-metric-collector --once --config .github/ci-config.json
|
||||
|
||||
#
|
||||
# Build on AlmaLinux 8 using go-toolset
|
||||
#
|
||||
AlmaLinux-RPM-build:
|
||||
runs-on: ubuntu-latest
|
||||
# See: https://hub.docker.com/_/almalinux
|
||||
container: almalinux:8
|
||||
# The job outputs link to the outputs of the 'rpmrename' step
|
||||
# Only job outputs can be used in child jobs
|
||||
steps:
|
||||
|
||||
# Use dnf to install development packages
|
||||
- name: Install development packages
|
||||
run: |
|
||||
dnf --assumeyes group install "Development Tools" "RPM Development Tools"
|
||||
dnf --assumeyes install wget openssl-devel diffutils delve which
|
||||
|
||||
# Checkout git repository and submodules
|
||||
# fetch-depth must be 0 to use git describe
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Use dnf to install build dependencies
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf --assumeyes install \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-bin-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-src-1.20.6-2.module_el8+658+f14b2092.noarch.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/go-toolset-1.20.6-1.module_el8+602+8bb8a8d6.x86_64.rpm
|
||||
|
||||
- name: RPM build MetricCollector
|
||||
id: rpmbuild
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/cc-metric-collector/cc-metric-collector
|
||||
make RPM
|
||||
|
||||
#
|
||||
# Build on UBI 8 using go-toolset
|
||||
#
|
||||
UBI-8-RPM-build:
|
||||
runs-on: ubuntu-latest
|
||||
# See: https://catalog.redhat.com/software/containers/ubi8/ubi/5c359854d70cc534b3a3784e?container-tabs=gti
|
||||
container: registry.access.redhat.com/ubi8/ubi:8.8-1032.1692772289
|
||||
# The job outputs link to the outputs of the 'rpmbuild' step
|
||||
steps:
|
||||
|
||||
# Use dnf to install development packages
|
||||
- name: Install development packages
|
||||
run: dnf --assumeyes --disableplugin=subscription-manager install rpm-build go-srpm-macros rpm-build-libs rpm-libs gcc make python38 git wget openssl-devel diffutils delve which
|
||||
|
||||
# Checkout git repository and submodules
|
||||
# fetch-depth must be 0 to use git describe
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Use dnf to install build dependencies
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf --assumeyes --disableplugin=subscription-manager install \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-bin-1.20.6-2.module_el8+658+f14b2092.x86_64.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/golang-src-1.20.6-2.module_el8+658+f14b2092.noarch.rpm \
|
||||
http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/Packages/go-toolset-1.20.6-1.module_el8+602+8bb8a8d6.x86_64.rpm
|
||||
|
||||
- name: RPM build MetricCollector
|
||||
id: rpmbuild
|
||||
run: |
|
||||
git config --global --add safe.directory /__w/cc-metric-collector/cc-metric-collector
|
||||
make RPM
|
||||
|
||||
#
|
||||
# Build on Ubuntu 22.04 using official go package
|
||||
#
|
||||
Ubuntu-jammy-build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ubuntu:22.04
|
||||
|
||||
steps:
|
||||
# Use apt to install development packages
|
||||
- name: Install development packages
|
||||
run: |
|
||||
apt update && apt --assume-yes upgrade
|
||||
apt --assume-yes install build-essential sed git wget bash
|
||||
# Checkout git repository and submodules
|
||||
# fetch-depth must be 0 to use git describe
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
# Use official golang package
|
||||
- name: Install Golang
|
||||
run: |
|
||||
wget -q https://go.dev/dl/go1.21.1.linux-amd64.tar.gz --output-document=- | \
|
||||
tar --directory=/usr/local --extract --gzip
|
||||
export PATH=/usr/local/go/bin:/usr/local/go/pkg/tool/linux_amd64:$PATH
|
||||
go version
|
||||
- name: DEB build MetricCollector
|
||||
id: dpkg-build
|
||||
run: |
|
||||
export PATH=/usr/local/go/bin:/usr/local/go/pkg/tool/linux_amd64:$PATH
|
||||
git config --global --add safe.directory /__w/cc-metric-collector/cc-metric-collector
|
||||
make DEB
|
29
.zenodo.json
29
.zenodo.json
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"title": "cc-metric-collector",
|
||||
"description": "Monitoring agent for ClusterCockpit.",
|
||||
"creators": [
|
||||
{
|
||||
"affiliation": "Regionales Rechenzentrum Erlangen, Friedrich-Alexander-Universität Erlangen-Nürnberg",
|
||||
"name": "Thomas Gruber",
|
||||
"orcid": "0000-0001-5560-6964"
|
||||
},
|
||||
{
|
||||
"affiliation": "Steinbuch Centre for Computing, Karlsruher Institut für Technologie",
|
||||
"name": "Holger Obermaier",
|
||||
"orcid": "0000-0002-6830-6626"
|
||||
}
|
||||
],
|
||||
"upload_type": "software",
|
||||
"license": "MIT",
|
||||
"access_right": "open",
|
||||
"keywords": [
|
||||
"performance-monitoring",
|
||||
"cluster-monitoring",
|
||||
"open-source"
|
||||
],
|
||||
"communities": [
|
||||
{
|
||||
"identifier": "clustercockpit"
|
||||
}
|
||||
]
|
||||
}
|
50
Makefile
50
Makefile
@@ -16,16 +16,15 @@ COMPONENT_DIRS := collectors \
|
||||
internal/multiChanTicker
|
||||
|
||||
BINDIR = bin
|
||||
GOBIN = $(shell which go)
|
||||
|
||||
|
||||
.PHONY: all
|
||||
all: $(APP)
|
||||
|
||||
$(APP): $(GOSRC) go.mod
|
||||
$(APP): $(GOSRC)
|
||||
make -C collectors
|
||||
$(GOBIN) get
|
||||
$(GOBIN) build -o $(APP) $(GOSRC_APP)
|
||||
go get
|
||||
go build -o $(APP) $(GOSRC_APP)
|
||||
|
||||
install: $(APP)
|
||||
@WORKSPACE=$(PREFIX)
|
||||
@@ -52,25 +51,25 @@ clean:
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
$(GOBIN) fmt $(GOSRC_COLLECTORS)
|
||||
$(GOBIN) fmt $(GOSRC_SINKS)
|
||||
$(GOBIN) fmt $(GOSRC_RECEIVERS)
|
||||
$(GOBIN) fmt $(GOSRC_APP)
|
||||
@for F in $(GOSRC_INTERNAL); do $(GOBIN) fmt $$F; done
|
||||
go fmt $(GOSRC_COLLECTORS)
|
||||
go fmt $(GOSRC_SINKS)
|
||||
go fmt $(GOSRC_RECEIVERS)
|
||||
go fmt $(GOSRC_APP)
|
||||
@for F in $(GOSRC_INTERNAL); do go fmt $$F; done
|
||||
|
||||
|
||||
# Examine Go source code and reports suspicious constructs
|
||||
.PHONY: vet
|
||||
vet:
|
||||
$(GOBIN) vet ./...
|
||||
go vet ./...
|
||||
|
||||
|
||||
# Run linter for the Go programming language.
|
||||
# Using static analysis, it finds bugs and performance issues, offers simplifications, and enforces style rules
|
||||
.PHONY: staticcheck
|
||||
staticcheck:
|
||||
$(GOBIN) install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
$$($(GOBIN) env GOPATH)/bin/staticcheck ./...
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
$$(go env GOPATH)/bin/staticcheck ./...
|
||||
|
||||
.ONESHELL:
|
||||
.PHONY: RPM
|
||||
@@ -84,7 +83,7 @@ RPM: scripts/cc-metric-collector.spec
|
||||
@COMMITISH="HEAD"
|
||||
@VERS=$$(git describe --tags $${COMMITISH})
|
||||
@VERS=$${VERS#v}
|
||||
@VERS=$$(echo $${VERS} | sed -e s+'-'+'_'+g)
|
||||
@VERS=$$(echo $$VERS | sed -e s+'-'+'_'+g)
|
||||
@eval $$(rpmspec --query --queryformat "NAME='%{name}' VERSION='%{version}' RELEASE='%{release}' NVR='%{NVR}' NVRA='%{NVRA}'" --define="VERS $${VERS}" "$${SPECFILE}")
|
||||
@PREFIX="$${NAME}-$${VERSION}"
|
||||
@FORMAT="tar.gz"
|
||||
@@ -96,8 +95,10 @@ RPM: scripts/cc-metric-collector.spec
|
||||
@if [[ "$${GITHUB_ACTIONS}" == true ]]; then
|
||||
@ RPMFILE="$${RPMDIR}/$${ARCH}/$${NVRA}.rpm"
|
||||
@ SRPMFILE="$${SRPMDIR}/$${NVR}.src.rpm"
|
||||
@ echo "SRPM=$${SRPMFILE}" >> $${GITHUB_OUTPUT}
|
||||
@ echo "RPM=$${RPMFILE}" >> $${GITHUB_OUTPUT}
|
||||
@ echo "RPM: $${RPMFILE}"
|
||||
@ echo "SRPM: $${SRPMFILE}"
|
||||
@ echo "::set-output name=SRPM::$${SRPMFILE}"
|
||||
@ echo "::set-output name=RPM::$${RPMFILE}"
|
||||
@fi
|
||||
|
||||
.PHONY: DEB
|
||||
@@ -106,24 +107,21 @@ DEB: scripts/cc-metric-collector.deb.control $(APP)
|
||||
@WORKSPACE=$${PWD}/.dpkgbuild
|
||||
@DEBIANDIR=$${WORKSPACE}/debian
|
||||
@DEBIANBINDIR=$${WORKSPACE}/DEBIAN
|
||||
@mkdir --parents --verbose $${WORKSPACE} $${DEBIANBINDIR}
|
||||
@mkdir --parents --verbose $$WORKSPACE $$DEBIANBINDIR
|
||||
#@mkdir --parents --verbose $$DEBIANDIR
|
||||
@CONTROLFILE="$${BASEDIR}/scripts/cc-metric-collector.deb.control"
|
||||
@COMMITISH="HEAD"
|
||||
@VERS=$$(git describe --tags --abbrev=0 $${COMMITISH})
|
||||
@if [ -z "$${VERS}" ]; then VERS=${GITHUB_REF_NAME}; fi
|
||||
@VERS=$${VERS#v}
|
||||
@VERS=$$(echo $$VERS | sed -e s+'-'+'_'+g)
|
||||
@ARCH=$$(uname -m)
|
||||
@ARCH=$$(echo $${ARCH} | sed -e s+'_'+'-'+g)
|
||||
@if [ "$${ARCH}" = "x86-64" ]; then ARCH=amd64; fi
|
||||
@ARCH=$$(echo $$ARCH | sed -e s+'_'+'-'+g)
|
||||
@PREFIX="$${NAME}-$${VERSION}_$${ARCH}"
|
||||
@SIZE_BYTES=$$(du -bcs --exclude=.dpkgbuild "$${WORKSPACE}"/ | awk '{print $$1}' | head -1 | sed -e 's/^0\+//')
|
||||
@SIZE="$$(awk -v size="$${SIZE_BYTES}" 'BEGIN {print (size/1024)+1}' | awk '{print int($$0)}')"
|
||||
@sed -e s+"{VERSION}"+"$${VERS}"+g -e s+"{INSTALLED_SIZE}"+"$${SIZE}"+g -e s+"{ARCH}"+"$${ARCH}"+g $${CONTROLFILE} > $${DEBIANBINDIR}/control
|
||||
@SIZE_BYTES=$$(du -bcs --exclude=.dpkgbuild "$$WORKSPACE"/ | awk '{print $$1}' | head -1 | sed -e 's/^0\+//')
|
||||
@SIZE="$$(awk -v size="$$SIZE_BYTES" 'BEGIN {print (size/1024)+1}' | awk '{print int($$0)}')"
|
||||
#@sed -e s+"{VERSION}"+"$$VERS"+g -e s+"{INSTALLED_SIZE}"+"$$SIZE"+g -e s+"{ARCH}"+"$$ARCH"+g $$CONTROLFILE > $${DEBIANDIR}/control
|
||||
@sed -e s+"{VERSION}"+"$$VERS"+g -e s+"{INSTALLED_SIZE}"+"$$SIZE"+g -e s+"{ARCH}"+"$$ARCH"+g $$CONTROLFILE > $${DEBIANBINDIR}/control
|
||||
@make PREFIX=$${WORKSPACE} install
|
||||
@DEB_FILE="cc-metric-collector_$${VERS}_$${ARCH}.deb"
|
||||
@dpkg-deb -b $${WORKSPACE} "$${DEB_FILE}"
|
||||
@if [ "$${GITHUB_ACTIONS}" = "true" ]; then
|
||||
@ echo "DEB=$${DEB_FILE}" >> $${GITHUB_OUTPUT}
|
||||
@fi
|
||||
@dpkg-deb -b $${WORKSPACE} "$$DEB_FILE"
|
||||
@rm -r "$${WORKSPACE}"
|
||||
|
24
README.md
24
README.md
@@ -1,6 +1,5 @@
|
||||
# cc-metric-collector
|
||||
|
||||
A node agent for measuring, processing and forwarding node level metrics. It is part of the [ClusterCockpit ecosystem](./docs/introduction.md).
|
||||
A node agent for measuring, processing and forwarding node level metrics. It is part of the ClusterCockpit ecosystem.
|
||||
|
||||
The metric collector sends (and receives) metric in the [InfluxDB line protocol](https://docs.influxdata.com/influxdb/cloud/reference/syntax/line-protocol/) as it provides flexibility while providing a separation between tags (like index columns in relational databases) and fields (like data columns).
|
||||
|
||||
@@ -8,14 +7,10 @@ There is a single timer loop that triggers all collectors serially, collects the
|
||||
|
||||
The receiver runs as a go routine side-by-side with the timer loop and asynchronously forwards received metrics to the sink.
|
||||
|
||||
|
||||
[](https://doi.org/10.5281/zenodo.7438287)
|
||||
|
||||
|
||||
# Configuration
|
||||
|
||||
Configuration is implemented using a single json document that is distributed over network and may be persisted as file.
|
||||
Supported metrics are documented [here](https://github.com/ClusterCockpit/cc-specifications/blob/master/interfaces/lineprotocol/README.md).
|
||||
Supported metrics are documented [here](https://github.com/ClusterCockpit/cc-specifications/blob/master/metrics/lineprotocol_alternative.md).
|
||||
|
||||
There is a main configuration file with basic settings that point to the other configuration files for the different components.
|
||||
|
||||
@@ -25,19 +20,21 @@ There is a main configuration file with basic settings that point to the other c
|
||||
"collectors" : "collectors.json",
|
||||
"receivers" : "receivers.json",
|
||||
"router" : "router.json",
|
||||
"interval": "10s",
|
||||
"duration": "1s"
|
||||
"stats_api" : "api.json",
|
||||
"interval": 10,
|
||||
"duration": 1
|
||||
}
|
||||
```
|
||||
|
||||
The `interval` defines how often the metrics should be read and send to the sink. The `duration` tells collectors how long one measurement has to take. This is important for some collectors, like the `likwid` collector. For more information, see [here](./docs/configuration.md).
|
||||
The `interval` defines how often the metrics should be read and send to the sink. The `duration` tells collectors how long one measurement has to take. This is important for some collectors, like the `likwid` collector.
|
||||
|
||||
See the component READMEs for their configuration:
|
||||
|
||||
* [`collectors`](./collectors/README.md)
|
||||
* [`sinks`](./sinks/README.md)
|
||||
* [`receivers`](./receivers/README.md)
|
||||
* [`router`](./internal/metricRouter/README.md)
|
||||
* [`stats_api`](./internal/metricRouter/StatsApi.md)
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -48,7 +45,6 @@ $ go get (requires at least golang 1.16)
|
||||
$ make
|
||||
```
|
||||
|
||||
For more information, see [here](./docs/building.md).
|
||||
|
||||
# Running
|
||||
|
||||
@@ -62,7 +58,6 @@ Usage of metric-collector:
|
||||
-once
|
||||
Run all collectors only once
|
||||
```
|
||||
|
||||
# Scenarios
|
||||
|
||||
The metric collector was designed with flexibility in mind, so it can be used in many scenarios. Here are a few:
|
||||
@@ -100,12 +95,11 @@ flowchart TD
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
The ClusterCockpit ecosystem is designed to be used by different HPC computing centers. Since configurations and setups differ between the centers, the centers likely have to put some work into the cc-metric-collector to gather all desired metrics.
|
||||
|
||||
You are free to open an issue to request a collector but we would also be happy about PRs.
|
||||
|
||||
# Contact
|
||||
# Contact
|
||||
|
||||
* [Matrix.org ClusterCockpit General chat](https://matrix.to/#/#clustercockpit-dev:matrix.org)
|
||||
* [Matrix.org ClusterCockpit Development chat](https://matrix.to/#/#clustercockpit:matrix.org)
|
||||
|
@@ -15,19 +15,20 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
mr "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/pkg/multiChanTicker"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/internal/multiChanTicker"
|
||||
)
|
||||
|
||||
type CentralConfigFile struct {
|
||||
Interval string `json:"interval"`
|
||||
Duration string `json:"duration"`
|
||||
Interval int `json:"interval"`
|
||||
Duration int `json:"duration"`
|
||||
CollectorConfigFile string `json:"collectors"`
|
||||
RouterConfigFile string `json:"router"`
|
||||
SinkConfigFile string `json:"sinks"`
|
||||
ReceiverConfigFile string `json:"receivers,omitempty"`
|
||||
StatsApiConfigFile string `json:"stats_api,omitempty"`
|
||||
}
|
||||
|
||||
func LoadCentralConfiguration(file string, config *CentralConfigFile) error {
|
||||
@@ -52,6 +53,7 @@ type RuntimeConfig struct {
|
||||
CollectManager collectors.CollectorManager
|
||||
SinkManager sinks.SinkManager
|
||||
ReceiveManager receivers.ReceiveManager
|
||||
StatsApi mr.StatsApi
|
||||
MultiChanTicker mct.MultiChanTicker
|
||||
|
||||
Channels []chan lp.CCMetric
|
||||
@@ -152,11 +154,16 @@ func shutdownHandler(config *RuntimeConfig, shutdownSignal chan os.Signal) {
|
||||
cclog.Debug("Shutdown SinkManager...")
|
||||
config.SinkManager.Close()
|
||||
}
|
||||
if config.StatsApi != nil {
|
||||
cclog.Debug("Shutdown StatsApi...")
|
||||
config.StatsApi.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func mainFunc() int {
|
||||
var err error
|
||||
use_recv := false
|
||||
use_api := false
|
||||
|
||||
// Initialize runtime configuration
|
||||
rcfg := RuntimeConfig{
|
||||
@@ -164,6 +171,7 @@ func mainFunc() int {
|
||||
CollectManager: nil,
|
||||
SinkManager: nil,
|
||||
ReceiveManager: nil,
|
||||
StatsApi: nil,
|
||||
CliArgs: ReadCli(),
|
||||
}
|
||||
|
||||
@@ -173,36 +181,16 @@ func mainFunc() int {
|
||||
cclog.Error("Error reading configuration file ", rcfg.CliArgs["configfile"], ": ", err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Properly use duration parser with inputs like '60s', '5m' or similar
|
||||
if len(rcfg.ConfigFile.Interval) > 0 {
|
||||
t, err := time.ParseDuration(rcfg.ConfigFile.Interval)
|
||||
if err != nil {
|
||||
cclog.Error("Configuration value 'interval' no valid duration")
|
||||
}
|
||||
rcfg.Interval = t
|
||||
if rcfg.Interval == 0 {
|
||||
cclog.Error("Configuration value 'interval' must be greater than zero")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Properly use duration parser with inputs like '60s', '5m' or similar
|
||||
if len(rcfg.ConfigFile.Duration) > 0 {
|
||||
t, err := time.ParseDuration(rcfg.ConfigFile.Duration)
|
||||
if err != nil {
|
||||
cclog.Error("Configuration value 'duration' no valid duration")
|
||||
}
|
||||
rcfg.Duration = t
|
||||
if rcfg.Duration == 0 {
|
||||
cclog.Error("Configuration value 'duration' must be greater than zero")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if rcfg.Duration > rcfg.Interval {
|
||||
cclog.Error("The interval should be greater than duration")
|
||||
if rcfg.ConfigFile.Interval <= 0 || time.Duration(rcfg.ConfigFile.Interval)*time.Second <= 0 {
|
||||
cclog.Error("Configuration value 'interval' must be greater than zero")
|
||||
return 1
|
||||
}
|
||||
rcfg.Interval = time.Duration(rcfg.ConfigFile.Interval) * time.Second
|
||||
if rcfg.ConfigFile.Duration <= 0 || time.Duration(rcfg.ConfigFile.Duration)*time.Second <= 0 {
|
||||
cclog.Error("Configuration value 'duration' must be greater than zero")
|
||||
return 1
|
||||
}
|
||||
rcfg.Duration = time.Duration(rcfg.ConfigFile.Duration) * time.Second
|
||||
|
||||
if len(rcfg.ConfigFile.RouterConfigFile) == 0 {
|
||||
cclog.Error("Metric router configuration file must be set")
|
||||
@@ -273,6 +261,16 @@ func mainFunc() int {
|
||||
use_recv = true
|
||||
}
|
||||
|
||||
// Create new statistics API manager
|
||||
if len(rcfg.ConfigFile.StatsApiConfigFile) > 0 {
|
||||
rcfg.StatsApi, err = mr.NewStatsApi(rcfg.MultiChanTicker, &rcfg.Sync, rcfg.ConfigFile.StatsApiConfigFile)
|
||||
if err != nil {
|
||||
cclog.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
use_api = true
|
||||
}
|
||||
|
||||
// Create shutdown handler
|
||||
shutdownSignal := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdownSignal, os.Interrupt)
|
||||
@@ -280,6 +278,11 @@ func mainFunc() int {
|
||||
rcfg.Sync.Add(1)
|
||||
go shutdownHandler(&rcfg, shutdownSignal)
|
||||
|
||||
// Start the stats api early to be prepared for init settings
|
||||
if use_api {
|
||||
rcfg.StatsApi.Start()
|
||||
}
|
||||
|
||||
// Start the managers
|
||||
rcfg.MetricRouter.Start()
|
||||
rcfg.SinkManager.Start()
|
||||
@@ -291,7 +294,7 @@ func mainFunc() int {
|
||||
|
||||
// Wait until one tick has passed. This is a workaround
|
||||
if rcfg.CliArgs["once"] == "true" {
|
||||
x := 1.2 * float64(rcfg.Interval.Seconds())
|
||||
x := 1.2 * float64(rcfg.ConfigFile.Interval)
|
||||
time.Sleep(time.Duration(int(x)) * time.Second)
|
||||
shutdownSignal <- os.Interrupt
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@
|
||||
"proc_total"
|
||||
]
|
||||
},
|
||||
"memstat": {},
|
||||
"netstat": {
|
||||
"include_devices": [
|
||||
"enp5s0"
|
||||
@@ -34,8 +33,5 @@
|
||||
"type-id": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"topprocs": {
|
||||
"num_procs": 5
|
||||
}
|
||||
}
|
@@ -1,33 +1,25 @@
|
||||
# LIKWID version
|
||||
LIKWID_VERSION := 5.2.2
|
||||
LIKWID_INSTALLED_FOLDER := $(shell dirname $$(which likwid-topology 2>/dev/null) 2>/dev/null)
|
||||
|
||||
LIKWID_FOLDER := $(CURDIR)/likwid
|
||||
|
||||
all: likwid
|
||||
|
||||
|
||||
# LIKWID version
|
||||
LIKWID_VERSION = 5.2.1
|
||||
|
||||
.ONESHELL:
|
||||
.PHONY: likwid
|
||||
likwid:
|
||||
if [ -n "$(LIKWID_INSTALLED_FOLDER)" ]; then
|
||||
# Using likwid include files from system installation
|
||||
INCLUDE_DIR="$(LIKWID_INSTALLED_FOLDER)/../include"
|
||||
mkdir --parents --verbose "$(LIKWID_FOLDER)"
|
||||
cp "$${INCLUDE_DIR}"/*.h "$(LIKWID_FOLDER)"
|
||||
else
|
||||
# Using likwid include files from downloaded tar archive
|
||||
if [ -d "$(LIKWID_FOLDER)" ]; then
|
||||
rm --recursive "$(LIKWID_FOLDER)"
|
||||
fi
|
||||
BUILD_FOLDER="$${PWD}/likwidbuild"
|
||||
mkdir --parents --verbose "$${BUILD_FOLDER}"
|
||||
wget --output-document=- http://ftp.rrze.uni-erlangen.de/mirrors/likwid/likwid-$(LIKWID_VERSION).tar.gz |
|
||||
tar --directory="$${BUILD_FOLDER}" --extract --gz
|
||||
install -D --verbose --preserve-timestamps --mode=0644 --target-directory="$(LIKWID_FOLDER)" "$${BUILD_FOLDER}/likwid-$(LIKWID_VERSION)/src/includes"/likwid*.h "$${BUILD_FOLDER}/likwid-$(LIKWID_VERSION)/src/includes"/bstrlib.h
|
||||
rm --recursive "$${BUILD_FOLDER}"
|
||||
fi
|
||||
INSTALL_FOLDER="$${PWD}/likwid"
|
||||
BUILD_FOLDER="$${PWD}/likwidbuild"
|
||||
if [ -d $${INSTALL_FOLDER} ]; then rm -r $${INSTALL_FOLDER}; fi
|
||||
mkdir --parents --verbose $${INSTALL_FOLDER} $${BUILD_FOLDER}
|
||||
wget -P "$${BUILD_FOLDER}" ftp://ftp.rrze.uni-erlangen.de/mirrors/likwid/likwid-$(LIKWID_VERSION).tar.gz
|
||||
tar -C $${BUILD_FOLDER} -xf $${BUILD_FOLDER}/likwid-$(LIKWID_VERSION).tar.gz
|
||||
install -Dpm 0644 $${BUILD_FOLDER}/likwid-$(LIKWID_VERSION)/src/includes/likwid*.h $${INSTALL_FOLDER}/
|
||||
install -Dpm 0644 $${BUILD_FOLDER}/likwid-$(LIKWID_VERSION)/src/includes/bstrlib.h $${INSTALL_FOLDER}/
|
||||
rm -r $${BUILD_FOLDER}
|
||||
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf likwid
|
||||
|
||||
.PHONY: clean
|
||||
|
@@ -35,11 +35,10 @@ In contrast to the configuration files for sinks and receivers, the collectors c
|
||||
* [`nfs4stat`](./nfs4Metric.md)
|
||||
* [`cpufreq`](./cpufreqMetric.md)
|
||||
* [`cpufreq_cpuinfo`](./cpufreqCpuinfoMetric.md)
|
||||
* [`numastats`](./numastatsMetric.md)
|
||||
* [`numastat`](./numastatMetric.md)
|
||||
* [`gpfs`](./gpfsMetric.md)
|
||||
* [`beegfs_meta`](./beegfsmetaMetric.md)
|
||||
* [`beegfs_storage`](./beegfsstorageMetric.md)
|
||||
* [`rocm_smi`](./rocmsmiMetric.md)
|
||||
|
||||
## Todos
|
||||
|
||||
@@ -51,7 +50,7 @@ A collector reads data from any source, parses it to metrics and submits these m
|
||||
* `Name() string`: Return the name of the collector
|
||||
* `Init(config json.RawMessage) error`: Initializes the collector using the given collector-specific config in JSON. Check if needed files/commands exists, ...
|
||||
* `Initialized() bool`: Check if a collector is successfully initialized
|
||||
* `Read(duration time.Duration, output chan ccMetric.CCMetric)`: Read, parse and submit data to the `output` channel as [`CCMetric`](../internal/ccMetric/README.md). If the collector has to measure anything for some duration, use the provided function argument `duration`.
|
||||
* `Read(duration time.Duration, output chan ccMetric.CCMetric)`: Read, parse and submit data to the `output` channel as [`CCMetric`](../internal/ccMetric/README.md). If the collector has to measure anything for some duration, use the provided function argument `duration`.
|
||||
* `Close()`: Closes down the collector.
|
||||
|
||||
It is recommanded to call `setup()` in the `Init()` function.
|
||||
|
@@ -5,7 +5,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const DEFAULT_BEEGFS_CMD = "beegfs-ctl"
|
||||
@@ -29,10 +30,11 @@ type BeegfsMetaCollectorConfig struct {
|
||||
|
||||
type BeegfsMetaCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config BeegfsMetaCollectorConfig
|
||||
skipFS map[string]struct{}
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config BeegfsMetaCollectorConfig
|
||||
skipFS map[string]struct{}
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *BeegfsMetaCollector) Init(config json.RawMessage) error {
|
||||
@@ -55,7 +57,6 @@ func (m *BeegfsMetaCollector) Init(config json.RawMessage) error {
|
||||
|
||||
m.name = "BeegfsMetaCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
// Set default beegfs-ctl binary
|
||||
|
||||
m.config.Beegfs = DEFAULT_BEEGFS_CMD
|
||||
@@ -106,6 +107,7 @@ func (m *BeegfsMetaCollector) Init(config json.RawMessage) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("BeegfsMetaCollector.Init(): Failed to find beegfs-ctl binary '%s': %v", m.config.Beegfs, err)
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -115,7 +117,7 @@ func (m *BeegfsMetaCollector) Read(interval time.Duration, output chan lp.CCMetr
|
||||
return
|
||||
}
|
||||
//get mounpoint
|
||||
buffer, _ := os.ReadFile(string("/proc/mounts"))
|
||||
buffer, _ := ioutil.ReadFile(string("/proc/mounts"))
|
||||
mounts := strings.Split(string(buffer), "\n")
|
||||
var mountpoints []string
|
||||
for _, line := range mounts {
|
||||
@@ -157,9 +159,9 @@ func (m *BeegfsMetaCollector) Read(interval time.Duration, output chan lp.CCMetr
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "BeegfsMetaCollector.Read(): Failed to execute command \"%s\": %s\n", cmd.String(), err.Error())
|
||||
fmt.Fprintf(os.Stderr, "BeegfsMetaCollector.Read(): command exit code: \"%d\"\n", cmd.ProcessState.ExitCode())
|
||||
data, _ := io.ReadAll(cmdStderr)
|
||||
data, _ := ioutil.ReadAll(cmdStderr)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsMetaCollector.Read(): command stderr: \"%s\"\n", string(data))
|
||||
data, _ = io.ReadAll(cmdStdout)
|
||||
data, _ = ioutil.ReadAll(cmdStdout)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsMetaCollector.Read(): command stdout: \"%s\"\n", string(data))
|
||||
return
|
||||
}
|
||||
@@ -219,10 +221,12 @@ func (m *BeegfsMetaCollector) Read(interval time.Duration, output chan lp.CCMetr
|
||||
y, err := lp.New(key, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *BeegfsMetaCollector) Close() {
|
||||
|
@@ -5,7 +5,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
// Struct for the collector-specific JSON config
|
||||
@@ -27,10 +28,11 @@ type BeegfsStorageCollectorConfig struct {
|
||||
|
||||
type BeegfsStorageCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config BeegfsStorageCollectorConfig
|
||||
skipFS map[string]struct{}
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config BeegfsStorageCollectorConfig
|
||||
skipFS map[string]struct{}
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *BeegfsStorageCollector) Init(config json.RawMessage) error {
|
||||
@@ -48,7 +50,6 @@ func (m *BeegfsStorageCollector) Init(config json.RawMessage) error {
|
||||
|
||||
m.name = "BeegfsStorageCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
// Set default beegfs-ctl binary
|
||||
|
||||
m.config.Beegfs = DEFAULT_BEEGFS_CMD
|
||||
@@ -99,6 +100,7 @@ func (m *BeegfsStorageCollector) Init(config json.RawMessage) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("BeegfsStorageCollector.Init(): Failed to find beegfs-ctl binary '%s': %v", m.config.Beegfs, err)
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -108,7 +110,7 @@ func (m *BeegfsStorageCollector) Read(interval time.Duration, output chan lp.CCM
|
||||
return
|
||||
}
|
||||
//get mounpoint
|
||||
buffer, _ := os.ReadFile(string("/proc/mounts"))
|
||||
buffer, _ := ioutil.ReadFile(string("/proc/mounts"))
|
||||
mounts := strings.Split(string(buffer), "\n")
|
||||
var mountpoints []string
|
||||
for _, line := range mounts {
|
||||
@@ -149,9 +151,9 @@ func (m *BeegfsStorageCollector) Read(interval time.Duration, output chan lp.CCM
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "BeegfsStorageCollector.Read(): Failed to execute command \"%s\": %s\n", cmd.String(), err.Error())
|
||||
fmt.Fprintf(os.Stderr, "BeegfsStorageCollector.Read(): command exit code: \"%d\"\n", cmd.ProcessState.ExitCode())
|
||||
data, _ := io.ReadAll(cmdStderr)
|
||||
data, _ := ioutil.ReadAll(cmdStderr)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsStorageCollector.Read(): command stderr: \"%s\"\n", string(data))
|
||||
data, _ = io.ReadAll(cmdStdout)
|
||||
data, _ = ioutil.ReadAll(cmdStdout)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsStorageCollector.Read(): command stdout: \"%s\"\n", string(data))
|
||||
return
|
||||
}
|
||||
@@ -211,10 +213,12 @@ func (m *BeegfsStorageCollector) Read(interval time.Duration, output chan lp.CCM
|
||||
y, err := lp.New(key, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *BeegfsStorageCollector) Close() {
|
||||
|
@@ -6,55 +6,47 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/pkg/multiChanTicker"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/internal/multiChanTicker"
|
||||
)
|
||||
|
||||
// Map of all available metric collectors
|
||||
var AvailableCollectors = map[string]MetricCollector{
|
||||
|
||||
"likwid": new(LikwidCollector),
|
||||
"loadavg": new(LoadavgCollector),
|
||||
"memstat": new(MemstatCollector),
|
||||
"netstat": new(NetstatCollector),
|
||||
"ibstat": new(InfinibandCollector),
|
||||
"lustrestat": new(LustreCollector),
|
||||
"cpustat": new(CpustatCollector),
|
||||
"topprocs": new(TopProcsCollector),
|
||||
"nvidia": new(NvidiaCollector),
|
||||
"customcmd": new(CustomCmdCollector),
|
||||
"iostat": new(IOstatCollector),
|
||||
"diskstat": new(DiskstatCollector),
|
||||
"tempstat": new(TempCollector),
|
||||
"ipmistat": new(IpmiCollector),
|
||||
"gpfs": new(GpfsCollector),
|
||||
"cpufreq": new(CPUFreqCollector),
|
||||
"cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector),
|
||||
"nfs3stat": new(Nfs3Collector),
|
||||
"nfs4stat": new(Nfs4Collector),
|
||||
"numastats": new(NUMAStatsCollector),
|
||||
"beegfs_meta": new(BeegfsMetaCollector),
|
||||
"beegfs_storage": new(BeegfsStorageCollector),
|
||||
"rapl": new(RAPLCollector),
|
||||
"rocm_smi": new(RocmSmiCollector),
|
||||
"self": new(SelfCollector),
|
||||
"schedstat": new(SchedstatCollector),
|
||||
"nfsiostat": new(NfsIOStatCollector),
|
||||
"likwid": new(LikwidCollector),
|
||||
"loadavg": new(LoadavgCollector),
|
||||
"memstat": new(MemstatCollector),
|
||||
"netstat": new(NetstatCollector),
|
||||
"ibstat": new(InfinibandCollector),
|
||||
"lustrestat": new(LustreCollector),
|
||||
"cpustat": new(CpustatCollector),
|
||||
"topprocs": new(TopProcsCollector),
|
||||
"nvidia": new(NvidiaCollector),
|
||||
"customcmd": new(CustomCmdCollector),
|
||||
"iostat": new(IOstatCollector),
|
||||
"diskstat": new(DiskstatCollector),
|
||||
"tempstat": new(TempCollector),
|
||||
"ipmistat": new(IpmiCollector),
|
||||
"gpfs": new(GpfsCollector),
|
||||
"cpufreq": new(CPUFreqCollector),
|
||||
"cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector),
|
||||
"nfs3stat": new(Nfs3Collector),
|
||||
"nfs4stat": new(Nfs4Collector),
|
||||
"numastats": new(NUMAStatsCollector),
|
||||
"beegfs_meta": new(BeegfsMetaCollector),
|
||||
"beegfs_storage": new(BeegfsStorageCollector),
|
||||
}
|
||||
|
||||
// Metric collector manager data structure
|
||||
type collectorManager struct {
|
||||
collectors []MetricCollector // List of metric collectors to read in parallel
|
||||
serial []MetricCollector // List of metric collectors to read serially
|
||||
output chan lp.CCMetric // Output channels
|
||||
done chan bool // channel to finish / stop metric collector manager
|
||||
ticker mct.MultiChanTicker // periodically ticking once each interval
|
||||
duration time.Duration // duration (for metrics that measure over a given duration)
|
||||
wg *sync.WaitGroup // wait group for all goroutines in cc-metric-collector
|
||||
config map[string]json.RawMessage // json encoded config for collector manager
|
||||
collector_wg sync.WaitGroup // internally used wait group for the parallel reading of collector
|
||||
parallel_run bool // Flag whether the collectors are currently read in parallel
|
||||
collectors []MetricCollector // List of metric collectors to use
|
||||
output chan lp.CCMetric // Output channels
|
||||
done chan bool // channel to finish / stop metric collector manager
|
||||
ticker mct.MultiChanTicker // periodically ticking once each interval
|
||||
duration time.Duration // duration (for metrics that measure over a given duration)
|
||||
wg *sync.WaitGroup // wait group for all goroutines in cc-metric-collector
|
||||
config map[string]json.RawMessage // json encoded config for collector manager
|
||||
}
|
||||
|
||||
// Metric collector manager access functions
|
||||
@@ -74,7 +66,6 @@ type CollectorManager interface {
|
||||
// Initialization is done for all configured collectors
|
||||
func (cm *collectorManager) Init(ticker mct.MultiChanTicker, duration time.Duration, wg *sync.WaitGroup, collectConfigFile string) error {
|
||||
cm.collectors = make([]MetricCollector, 0)
|
||||
cm.serial = make([]MetricCollector, 0)
|
||||
cm.output = nil
|
||||
cm.done = make(chan bool)
|
||||
cm.wg = wg
|
||||
@@ -109,11 +100,7 @@ func (cm *collectorManager) Init(ticker mct.MultiChanTicker, duration time.Durat
|
||||
continue
|
||||
}
|
||||
cclog.ComponentDebug("CollectorManager", "ADD COLLECTOR", collector.Name())
|
||||
if collector.Parallel() {
|
||||
cm.collectors = append(cm.collectors, collector)
|
||||
} else {
|
||||
cm.serial = append(cm.serial, collector)
|
||||
}
|
||||
cm.collectors = append(cm.collectors, collector)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -129,10 +116,6 @@ func (cm *collectorManager) Start() {
|
||||
// Collector manager is done
|
||||
done := func() {
|
||||
// close all metric collectors
|
||||
if cm.parallel_run {
|
||||
cm.collector_wg.Wait()
|
||||
cm.parallel_run = false
|
||||
}
|
||||
for _, c := range cm.collectors {
|
||||
c.Close()
|
||||
}
|
||||
@@ -147,26 +130,7 @@ func (cm *collectorManager) Start() {
|
||||
done()
|
||||
return
|
||||
case t := <-tick:
|
||||
cm.parallel_run = true
|
||||
for _, c := range cm.collectors {
|
||||
// Wait for done signal or execute the collector
|
||||
select {
|
||||
case <-cm.done:
|
||||
done()
|
||||
return
|
||||
default:
|
||||
// Read metrics from collector c via goroutine
|
||||
cclog.ComponentDebug("CollectorManager", c.Name(), t)
|
||||
cm.collector_wg.Add(1)
|
||||
go func(myc MetricCollector) {
|
||||
myc.Read(cm.duration, cm.output)
|
||||
cm.collector_wg.Done()
|
||||
}(c)
|
||||
}
|
||||
}
|
||||
cm.collector_wg.Wait()
|
||||
cm.parallel_run = false
|
||||
for _, c := range cm.serial {
|
||||
// Wait for done signal or execute the collector
|
||||
select {
|
||||
case <-cm.done:
|
||||
|
@@ -10,22 +10,35 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
//
|
||||
// CPUFreqCollector
|
||||
// a metric collector to measure the current frequency of the CPUs
|
||||
// as obtained from /proc/cpuinfo
|
||||
// Only measure on the first hyperthread
|
||||
//
|
||||
type CPUFreqCpuInfoCollectorTopology struct {
|
||||
isHT bool
|
||||
tagSet map[string]string
|
||||
processor string // logical processor number (continuous, starting at 0)
|
||||
coreID string // socket local core ID
|
||||
coreID_int int64
|
||||
physicalPackageID string // socket / package ID
|
||||
physicalPackageID_int int64
|
||||
numPhysicalPackages string // number of sockets / packages
|
||||
numPhysicalPackages_int int64
|
||||
isHT bool
|
||||
numNonHT string // number of non hyperthreading processors
|
||||
numNonHT_int int64
|
||||
tagSet map[string]string
|
||||
}
|
||||
|
||||
type CPUFreqCpuInfoCollector struct {
|
||||
metricCollector
|
||||
topology []CPUFreqCpuInfoCollectorTopology
|
||||
topology []*CPUFreqCpuInfoCollectorTopology
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
@@ -37,7 +50,6 @@ func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
m.setup()
|
||||
|
||||
m.name = "CPUFreqCpuInfoCollector"
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
"group": "CPU",
|
||||
@@ -54,9 +66,11 @@ func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
// Collect topology information from file cpuinfo
|
||||
foundFreq := false
|
||||
processor := ""
|
||||
var numNonHT_int int64 = 0
|
||||
coreID := ""
|
||||
physicalPackageID := ""
|
||||
m.topology = make([]CPUFreqCpuInfoCollectorTopology, 0)
|
||||
var maxPhysicalPackageID int64 = 0
|
||||
m.topology = make([]*CPUFreqCpuInfoCollectorTopology, 0)
|
||||
coreSeenBefore := make(map[string]bool)
|
||||
|
||||
// Read cpuinfo file, line by line
|
||||
@@ -85,22 +99,41 @@ func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
len(coreID) > 0 &&
|
||||
len(physicalPackageID) > 0 {
|
||||
|
||||
topology := new(CPUFreqCpuInfoCollectorTopology)
|
||||
|
||||
// Processor
|
||||
topology.processor = processor
|
||||
|
||||
// Core ID
|
||||
topology.coreID = coreID
|
||||
topology.coreID_int, err = strconv.ParseInt(coreID, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert coreID '%s' to int64: %v", coreID, err)
|
||||
}
|
||||
|
||||
// Physical package ID
|
||||
topology.physicalPackageID = physicalPackageID
|
||||
topology.physicalPackageID_int, err = strconv.ParseInt(physicalPackageID, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert physicalPackageID '%s' to int64: %v", physicalPackageID, err)
|
||||
}
|
||||
|
||||
// increase maximun socket / package ID, when required
|
||||
if topology.physicalPackageID_int > maxPhysicalPackageID {
|
||||
maxPhysicalPackageID = topology.physicalPackageID_int
|
||||
}
|
||||
|
||||
// is hyperthread?
|
||||
globalID := physicalPackageID + ":" + coreID
|
||||
topology.isHT = coreSeenBefore[globalID]
|
||||
coreSeenBefore[globalID] = true
|
||||
if !topology.isHT {
|
||||
// increase number on non hyper thread cores
|
||||
numNonHT_int++
|
||||
}
|
||||
|
||||
// store collected topology information
|
||||
m.topology = append(m.topology,
|
||||
CPUFreqCpuInfoCollectorTopology{
|
||||
isHT: coreSeenBefore[globalID],
|
||||
tagSet: map[string]string{
|
||||
"type": "hwthread",
|
||||
"type-id": processor,
|
||||
"package_id": physicalPackageID,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// mark core as seen before
|
||||
coreSeenBefore[globalID] = true
|
||||
m.topology = append(m.topology, topology)
|
||||
|
||||
// reset topology information
|
||||
foundFreq = false
|
||||
@@ -110,11 +143,21 @@ func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if at least one CPU with frequency information was detected
|
||||
if len(m.topology) == 0 {
|
||||
return fmt.Errorf("No CPU frequency info found in %s", cpuInfoFile)
|
||||
numPhysicalPackageID_int := maxPhysicalPackageID + 1
|
||||
numPhysicalPackageID := fmt.Sprint(numPhysicalPackageID_int)
|
||||
numNonHT := fmt.Sprint(numNonHT_int)
|
||||
for _, t := range m.topology {
|
||||
t.numPhysicalPackages = numPhysicalPackageID
|
||||
t.numPhysicalPackages_int = numPhysicalPackageID_int
|
||||
t.numNonHT = numNonHT
|
||||
t.numNonHT_int = numNonHT_int
|
||||
t.tagSet = map[string]string{
|
||||
"type": "cpu",
|
||||
"type-id": t.processor,
|
||||
"package_id": t.physicalPackageID,
|
||||
}
|
||||
}
|
||||
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -155,6 +198,7 @@ func (m *CPUFreqCpuInfoCollector) Read(interval time.Duration, output chan lp.CC
|
||||
return
|
||||
}
|
||||
if y, err := lp.New("cpufreq", t.tagSet, m.meta, map[string]interface{}{"value": value}, now); err == nil {
|
||||
m.statsProcessedMetrics++
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
@@ -162,6 +206,7 @@ func (m *CPUFreqCpuInfoCollector) Read(interval time.Duration, output chan lp.CC
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *CPUFreqCpuInfoCollector) Close() {
|
||||
|
@@ -1,11 +1,10 @@
|
||||
## `cpufreq_cpuinfo` collector
|
||||
|
||||
## `cpufreq_cpuinfo` collector
|
||||
```json
|
||||
"cpufreq_cpuinfo": {}
|
||||
```
|
||||
|
||||
The `cpufreq_cpuinfo` collector reads the clock frequency from `/proc/cpuinfo` and outputs a handful **hwthread** metrics.
|
||||
The `cpufreq_cpuinfo` collector reads the clock frequency from `/proc/cpuinfo` and outputs a handful **cpu** metrics.
|
||||
|
||||
Metrics:
|
||||
|
||||
* `cpufreq`
|
||||
|
@@ -3,33 +3,46 @@ package collectors
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
"github.com/ClusterCockpit/cc-metric-collector/pkg/ccTopology"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type CPUFreqCollectorTopology struct {
|
||||
scalingCurFreqFile string
|
||||
tagSet map[string]string
|
||||
processor string // logical processor number (continuous, starting at 0)
|
||||
coreID string // socket local core ID
|
||||
coreID_int int64
|
||||
physicalPackageID string // socket / package ID
|
||||
physicalPackageID_int int64
|
||||
numPhysicalPackages string // number of sockets / packages
|
||||
numPhysicalPackages_int int64
|
||||
isHT bool
|
||||
numNonHT string // number of non hyperthreading processors
|
||||
numNonHT_int int64
|
||||
scalingCurFreqFile string
|
||||
tagSet map[string]string
|
||||
}
|
||||
|
||||
//
|
||||
// CPUFreqCollector
|
||||
// a metric collector to measure the current frequency of the CPUs
|
||||
// as obtained from the hardware (in KHz)
|
||||
// Only measure on the first hyper-thread
|
||||
// Only measure on the first hyper thread
|
||||
//
|
||||
// See: https://www.kernel.org/doc/html/latest/admin-guide/pm/cpufreq.html
|
||||
//
|
||||
type CPUFreqCollector struct {
|
||||
metricCollector
|
||||
topology []CPUFreqCollectorTopology
|
||||
config struct {
|
||||
topology []CPUFreqCollectorTopology
|
||||
statsProcessedMetrics int64
|
||||
config struct {
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
}
|
||||
}
|
||||
@@ -42,7 +55,6 @@ func (m *CPUFreqCollector) Init(config json.RawMessage) error {
|
||||
|
||||
m.name = "CPUFreqCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
@@ -52,41 +64,111 @@ func (m *CPUFreqCollector) Init(config json.RawMessage) error {
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
"group": "CPU",
|
||||
"unit": "Hz",
|
||||
"unit": "MHz",
|
||||
}
|
||||
|
||||
m.topology = make([]CPUFreqCollectorTopology, 0)
|
||||
for _, c := range ccTopology.CpuData() {
|
||||
// Loop for all CPU directories
|
||||
baseDir := "/sys/devices/system/cpu"
|
||||
globPattern := filepath.Join(baseDir, "cpu[0-9]*")
|
||||
cpuDirs, err := filepath.Glob(globPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to glob files with pattern '%s': %v", globPattern, err)
|
||||
}
|
||||
if cpuDirs == nil {
|
||||
return fmt.Errorf("unable to find any files with pattern '%s'", globPattern)
|
||||
}
|
||||
|
||||
// Skip hyper threading CPUs
|
||||
if c.CpuID != c.CoreCPUsList[0] {
|
||||
continue
|
||||
// Initialize CPU topology
|
||||
m.topology = make([]CPUFreqCollectorTopology, len(cpuDirs))
|
||||
for _, cpuDir := range cpuDirs {
|
||||
processor := strings.TrimPrefix(cpuDir, "/sys/devices/system/cpu/cpu")
|
||||
processor_int, err := strconv.ParseInt(processor, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert cpuID '%s' to int64: %v", processor, err)
|
||||
}
|
||||
|
||||
// Read package ID
|
||||
physicalPackageIDFile := filepath.Join(cpuDir, "topology", "physical_package_id")
|
||||
line, err := ioutil.ReadFile(physicalPackageIDFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read physical package ID from file '%s': %v", physicalPackageIDFile, err)
|
||||
}
|
||||
physicalPackageID := strings.TrimSpace(string(line))
|
||||
physicalPackageID_int, err := strconv.ParseInt(physicalPackageID, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert packageID '%s' to int64: %v", physicalPackageID, err)
|
||||
}
|
||||
|
||||
// Read core ID
|
||||
coreIDFile := filepath.Join(cpuDir, "topology", "core_id")
|
||||
line, err = ioutil.ReadFile(coreIDFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read core ID from file '%s': %v", coreIDFile, err)
|
||||
}
|
||||
coreID := strings.TrimSpace(string(line))
|
||||
coreID_int, err := strconv.ParseInt(coreID, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to convert coreID '%s' to int64: %v", coreID, err)
|
||||
}
|
||||
|
||||
// Check access to current frequency file
|
||||
scalingCurFreqFile := filepath.Join("/sys/devices/system/cpu", fmt.Sprintf("cpu%d", c.CpuID), "cpufreq/scaling_cur_freq")
|
||||
err := unix.Access(scalingCurFreqFile, unix.R_OK)
|
||||
scalingCurFreqFile := filepath.Join(cpuDir, "cpufreq", "scaling_cur_freq")
|
||||
err = unix.Access(scalingCurFreqFile, unix.R_OK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to access file '%s': %v", scalingCurFreqFile, err)
|
||||
}
|
||||
|
||||
m.topology = append(m.topology,
|
||||
CPUFreqCollectorTopology{
|
||||
tagSet: map[string]string{
|
||||
"type": "hwthread",
|
||||
"type-id": fmt.Sprint(c.CpuID),
|
||||
"package_id": fmt.Sprint(c.Socket),
|
||||
},
|
||||
scalingCurFreqFile: scalingCurFreqFile,
|
||||
},
|
||||
)
|
||||
t := &m.topology[processor_int]
|
||||
t.processor = processor
|
||||
t.physicalPackageID = physicalPackageID
|
||||
t.physicalPackageID_int = physicalPackageID_int
|
||||
t.coreID = coreID
|
||||
t.coreID_int = coreID_int
|
||||
t.scalingCurFreqFile = scalingCurFreqFile
|
||||
}
|
||||
|
||||
// Initialized
|
||||
cclog.ComponentDebug(
|
||||
m.name,
|
||||
"initialized",
|
||||
len(m.topology), "non-hyper-threading CPUs")
|
||||
// is processor a hyperthread?
|
||||
coreSeenBefore := make(map[string]bool)
|
||||
for i := range m.topology {
|
||||
t := &m.topology[i]
|
||||
|
||||
globalID := t.physicalPackageID + ":" + t.coreID
|
||||
t.isHT = coreSeenBefore[globalID]
|
||||
coreSeenBefore[globalID] = true
|
||||
}
|
||||
|
||||
// number of non hyper thread cores and packages / sockets
|
||||
var numNonHT_int int64 = 0
|
||||
var maxPhysicalPackageID int64 = 0
|
||||
for i := range m.topology {
|
||||
t := &m.topology[i]
|
||||
|
||||
// Update maxPackageID
|
||||
if t.physicalPackageID_int > maxPhysicalPackageID {
|
||||
maxPhysicalPackageID = t.physicalPackageID_int
|
||||
}
|
||||
|
||||
if !t.isHT {
|
||||
numNonHT_int++
|
||||
}
|
||||
}
|
||||
|
||||
numPhysicalPackageID_int := maxPhysicalPackageID + 1
|
||||
numPhysicalPackageID := fmt.Sprint(numPhysicalPackageID_int)
|
||||
numNonHT := fmt.Sprint(numNonHT_int)
|
||||
for i := range m.topology {
|
||||
t := &m.topology[i]
|
||||
t.numPhysicalPackages = numPhysicalPackageID
|
||||
t.numPhysicalPackages_int = numPhysicalPackageID_int
|
||||
t.numNonHT = numNonHT
|
||||
t.numNonHT_int = numNonHT_int
|
||||
t.tagSet = map[string]string{
|
||||
"type": "cpu",
|
||||
"type-id": t.processor,
|
||||
"package_id": t.physicalPackageID,
|
||||
}
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -101,8 +183,13 @@ func (m *CPUFreqCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
for i := range m.topology {
|
||||
t := &m.topology[i]
|
||||
|
||||
// skip hyperthreads
|
||||
if t.isHT {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read current frequency
|
||||
line, err := os.ReadFile(t.scalingCurFreqFile)
|
||||
line, err := ioutil.ReadFile(t.scalingCurFreqFile)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
@@ -118,9 +205,11 @@ func (m *CPUFreqCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
}
|
||||
|
||||
if y, err := lp.New("cpufreq", t.tagSet, m.meta, map[string]interface{}{"value": cpuFreq}, now); err == nil {
|
||||
m.statsProcessedMetrics++
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *CPUFreqCollector) Close() {
|
||||
|
@@ -1,13 +1,11 @@
|
||||
## `cpufreq_cpuinfo` collector
|
||||
|
||||
```json
|
||||
"cpufreq": {
|
||||
"exclude_metrics": []
|
||||
}
|
||||
```
|
||||
|
||||
The `cpufreq` collector reads the clock frequency from `/sys/devices/system/cpu/cpu*/cpufreq` and outputs a handful **hwthread** metrics.
|
||||
The `cpufreq` collector reads the clock frequency from `/sys/devices/system/cpu/cpu*/cpufreq` and outputs a handful **cpu** metrics.
|
||||
|
||||
Metrics:
|
||||
|
||||
* `cpufreq`
|
||||
* `cpufreq`
|
@@ -9,9 +9,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
sysconf "github.com/tklauser/go-sysconf"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const CPUSTATFILE = `/proc/stat`
|
||||
@@ -22,18 +22,16 @@ type CpustatCollectorConfig struct {
|
||||
|
||||
type CpustatCollector struct {
|
||||
metricCollector
|
||||
config CpustatCollectorConfig
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive values
|
||||
matches map[string]int
|
||||
cputags map[string]map[string]string
|
||||
nodetags map[string]string
|
||||
olddata map[string]map[string]int64
|
||||
config CpustatCollectorConfig
|
||||
matches map[string]int
|
||||
cputags map[string]map[string]string
|
||||
nodetags map[string]string
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *CpustatCollector) Init(config json.RawMessage) error {
|
||||
m.name = "CpustatCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{"source": m.name, "group": "CPU", "unit": "Percent"}
|
||||
m.nodetags = map[string]string{"type": "node"}
|
||||
if len(config) > 0 {
|
||||
@@ -79,58 +77,39 @@ func (m *CpustatCollector) Init(config json.RawMessage) error {
|
||||
// Pre-generate tags for all CPUs
|
||||
num_cpus := 0
|
||||
m.cputags = make(map[string]map[string]string)
|
||||
m.olddata = make(map[string]map[string]int64)
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
linefields := strings.Fields(line)
|
||||
if strings.Compare(linefields[0], "cpu") == 0 {
|
||||
m.olddata["cpu"] = make(map[string]int64)
|
||||
for k, v := range m.matches {
|
||||
m.olddata["cpu"][k], _ = strconv.ParseInt(linefields[v], 0, 64)
|
||||
}
|
||||
} else if strings.HasPrefix(linefields[0], "cpu") && strings.Compare(linefields[0], "cpu") != 0 {
|
||||
if strings.HasPrefix(linefields[0], "cpu") && strings.Compare(linefields[0], "cpu") != 0 {
|
||||
cpustr := strings.TrimLeft(linefields[0], "cpu")
|
||||
cpu, _ := strconv.Atoi(cpustr)
|
||||
m.cputags[linefields[0]] = map[string]string{"type": "hwthread", "type-id": fmt.Sprintf("%d", cpu)}
|
||||
m.olddata[linefields[0]] = make(map[string]int64)
|
||||
for k, v := range m.matches {
|
||||
m.olddata[linefields[0]][k], _ = strconv.ParseInt(linefields[v], 0, 64)
|
||||
}
|
||||
m.cputags[linefields[0]] = map[string]string{"type": "cpu", "type-id": fmt.Sprintf("%d", cpu)}
|
||||
num_cpus++
|
||||
}
|
||||
}
|
||||
m.lastTimestamp = time.Now()
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CpustatCollector) parseStatLine(linefields []string, tags map[string]string, output chan lp.CCMetric, now time.Time, tsdelta time.Duration) {
|
||||
func (m *CpustatCollector) parseStatLine(linefields []string, tags map[string]string, output chan lp.CCMetric) {
|
||||
values := make(map[string]float64)
|
||||
clktck, _ := sysconf.Sysconf(sysconf.SC_CLK_TCK)
|
||||
total := 0.0
|
||||
for match, index := range m.matches {
|
||||
if len(match) > 0 {
|
||||
x, err := strconv.ParseInt(linefields[index], 0, 64)
|
||||
if err == nil {
|
||||
vdiff := x - m.olddata[linefields[0]][match]
|
||||
m.olddata[linefields[0]][match] = x // Store new value for next run
|
||||
values[match] = float64(vdiff) / float64(tsdelta.Seconds()) / float64(clktck)
|
||||
values[match] = float64(x)
|
||||
total += values[match]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sum := float64(0)
|
||||
t := time.Now()
|
||||
for name, value := range values {
|
||||
sum += value
|
||||
y, err := lp.New(name, tags, m.meta, map[string]interface{}{"value": value * 100}, now)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if v, ok := values["cpu_idle"]; ok {
|
||||
sum -= v
|
||||
y, err := lp.New("cpu_used", tags, m.meta, map[string]interface{}{"value": sum * 100}, now)
|
||||
y, err := lp.New(name, tags, m.meta, map[string]interface{}{"value": (value * 100.0) / total}, t)
|
||||
if err == nil {
|
||||
m.statsProcessedMetrics++
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
@@ -141,9 +120,6 @@ func (m *CpustatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
return
|
||||
}
|
||||
num_cpus := 0
|
||||
now := time.Now()
|
||||
tsdelta := now.Sub(m.lastTimestamp)
|
||||
|
||||
file, err := os.Open(string(CPUSTATFILE))
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
@@ -155,9 +131,9 @@ func (m *CpustatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
line := scanner.Text()
|
||||
linefields := strings.Fields(line)
|
||||
if strings.Compare(linefields[0], "cpu") == 0 {
|
||||
m.parseStatLine(linefields, m.nodetags, output, now, tsdelta)
|
||||
m.parseStatLine(linefields, m.nodetags, output)
|
||||
} else if strings.HasPrefix(linefields[0], "cpu") {
|
||||
m.parseStatLine(linefields, m.cputags[linefields[0]], output, now, tsdelta)
|
||||
m.parseStatLine(linefields, m.cputags[linefields[0]], output)
|
||||
num_cpus++
|
||||
}
|
||||
}
|
||||
@@ -166,13 +142,13 @@ func (m *CpustatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
m.nodetags,
|
||||
m.meta,
|
||||
map[string]interface{}{"value": int(num_cpus)},
|
||||
now,
|
||||
time.Now(),
|
||||
)
|
||||
if err == nil {
|
||||
m.statsProcessedMetrics++
|
||||
output <- num_cpus_metric
|
||||
}
|
||||
|
||||
m.lastTimestamp = now
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *CpustatCollector) Close() {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
|
||||
## `cpustat` collector
|
||||
|
||||
```json
|
||||
"cpustat": {
|
||||
"exclude_metrics": [
|
||||
@@ -9,10 +8,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
The `cpustat` collector reads data from `/proc/stat` and outputs a handful **node** and **hwthread** metrics. If a metric is not required, it can be excluded from forwarding it to the sink.
|
||||
The `cpustat` collector reads data from `/proc/stats` and outputs a handful **node** and **hwthread** metrics. If a metric is not required, it can be excluded from forwarding it to the sink.
|
||||
|
||||
Metrics:
|
||||
|
||||
* `cpu_user`
|
||||
* `cpu_nice`
|
||||
* `cpu_system`
|
||||
@@ -23,4 +21,3 @@ Metrics:
|
||||
* `cpu_steal`
|
||||
* `cpu_guest`
|
||||
* `cpu_guest_nice`
|
||||
* `cpu_used` = `cpu_* - cpu_idle`
|
@@ -3,13 +3,14 @@ package collectors
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
influx "github.com/influxdata/line-protocol"
|
||||
)
|
||||
|
||||
@@ -23,17 +24,19 @@ type CustomCmdCollectorConfig struct {
|
||||
|
||||
type CustomCmdCollector struct {
|
||||
metricCollector
|
||||
handler *influx.MetricHandler
|
||||
parser *influx.Parser
|
||||
config CustomCmdCollectorConfig
|
||||
commands []string
|
||||
files []string
|
||||
handler *influx.MetricHandler
|
||||
parser *influx.Parser
|
||||
config CustomCmdCollectorConfig
|
||||
commands []string
|
||||
files []string
|
||||
statsProcessedMetrics int64
|
||||
statsProcessedCommands int64
|
||||
statsProcessedFiles int64
|
||||
}
|
||||
|
||||
func (m *CustomCmdCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "CustomCmdCollector"
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{"source": m.name, "group": "Custom"}
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
@@ -48,12 +51,12 @@ func (m *CustomCmdCollector) Init(config json.RawMessage) error {
|
||||
command := exec.Command(cmdfields[0], strings.Join(cmdfields[1:], " "))
|
||||
command.Wait()
|
||||
_, err = command.Output()
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
m.commands = append(m.commands, c)
|
||||
}
|
||||
}
|
||||
for _, f := range m.config.Files {
|
||||
_, err = os.ReadFile(f)
|
||||
_, err = ioutil.ReadFile(f)
|
||||
if err == nil {
|
||||
m.files = append(m.files, f)
|
||||
} else {
|
||||
@@ -67,6 +70,9 @@ func (m *CustomCmdCollector) Init(config json.RawMessage) error {
|
||||
m.handler = influx.NewMetricHandler()
|
||||
m.parser = influx.NewParser(m.handler)
|
||||
m.parser.SetTimeFunc(DefaultTime)
|
||||
m.statsProcessedMetrics = 0
|
||||
m.statsProcessedFiles = 0
|
||||
m.statsProcessedCommands = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -101,12 +107,16 @@ func (m *CustomCmdCollector) Read(interval time.Duration, output chan lp.CCMetri
|
||||
|
||||
y := lp.FromInfluxMetric(c)
|
||||
if err == nil {
|
||||
m.statsProcessedMetrics++
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
m.statsProcessedCommands++
|
||||
stats.ComponentStatInt(m.name, "processed_commands", m.statsProcessedCommands)
|
||||
}
|
||||
for _, file := range m.files {
|
||||
buffer, err := os.ReadFile(file)
|
||||
buffer, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
@@ -123,9 +133,13 @@ func (m *CustomCmdCollector) Read(interval time.Duration, output chan lp.CCMetri
|
||||
}
|
||||
y := lp.FromInfluxMetric(f)
|
||||
if err == nil {
|
||||
m.statsProcessedMetrics++
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
m.statsProcessedFiles++
|
||||
stats.ComponentStatInt(m.name, "processed_files", m.statsProcessedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,13 +3,15 @@ package collectors
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
// "log"
|
||||
@@ -22,14 +24,12 @@ type DiskstatCollectorConfig struct {
|
||||
|
||||
type DiskstatCollector struct {
|
||||
metricCollector
|
||||
//matches map[string]int
|
||||
config IOstatCollectorConfig
|
||||
//devices map[string]IOstatCollectorEntry
|
||||
config DiskstatCollectorConfig
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *DiskstatCollector) Init(config json.RawMessage) error {
|
||||
m.name = "DiskstatCollector"
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{"source": m.name, "group": "Disk"}
|
||||
m.setup()
|
||||
if len(config) > 0 {
|
||||
@@ -44,6 +44,7 @@ func (m *DiskstatCollector) Init(config json.RawMessage) error {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -78,41 +79,39 @@ func (m *DiskstatCollector) Read(interval time.Duration, output chan lp.CCMetric
|
||||
continue
|
||||
}
|
||||
path := strings.Replace(linefields[1], `\040`, " ", -1)
|
||||
stat := syscall.Statfs_t{
|
||||
Blocks: 0,
|
||||
Bsize: 0,
|
||||
Bfree: 0,
|
||||
}
|
||||
stat := syscall.Statfs_t{}
|
||||
err := syscall.Statfs(path, &stat)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if stat.Blocks == 0 || stat.Bsize == 0 {
|
||||
continue
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
tags := map[string]string{"type": "node", "device": linefields[0]}
|
||||
total := (stat.Blocks * uint64(stat.Bsize)) / uint64(1000000000)
|
||||
y, err := lp.New("disk_total", tags, m.meta, map[string]interface{}{"value": total}, time.Now())
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "GBytes")
|
||||
m.statsProcessedMetrics++
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
output <- y
|
||||
}
|
||||
free := (stat.Bfree * uint64(stat.Bsize)) / uint64(1000000000)
|
||||
y, err = lp.New("disk_free", tags, m.meta, map[string]interface{}{"value": free}, time.Now())
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "GBytes")
|
||||
m.statsProcessedMetrics++
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
output <- y
|
||||
}
|
||||
if total > 0 {
|
||||
perc := (100 * (total - free)) / total
|
||||
if perc > part_max_used {
|
||||
part_max_used = perc
|
||||
}
|
||||
perc := (100 * (total - free)) / total
|
||||
if perc > part_max_used {
|
||||
part_max_used = perc
|
||||
}
|
||||
}
|
||||
y, err := lp.New("part_max_used", map[string]string{"type": "node"}, m.meta, map[string]interface{}{"value": int(part_max_used)}, time.Now())
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "percent")
|
||||
m.statsProcessedMetrics++
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
@@ -13,8 +13,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const DEFAULT_GPFS_CMD = "mmpmon"
|
||||
@@ -31,11 +32,11 @@ type GpfsCollector struct {
|
||||
Mmpmon string `json:"mmpmon_path,omitempty"`
|
||||
ExcludeFilesystem []string `json:"exclude_filesystem,omitempty"`
|
||||
SendBandwidths bool `json:"send_bandwidths"`
|
||||
SendTotalValues bool `json:"send_total_values"`
|
||||
}
|
||||
skipFS map[string]struct{}
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
lastState map[string]GpfsCollectorLastState
|
||||
skipFS map[string]struct{}
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
lastState map[string]GpfsCollectorLastState
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *GpfsCollector) Init(config json.RawMessage) error {
|
||||
@@ -47,7 +48,6 @@ func (m *GpfsCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "GpfsCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
|
||||
// Set default mmpmon binary
|
||||
m.config.Mmpmon = DEFAULT_GPFS_CMD
|
||||
@@ -72,7 +72,6 @@ func (m *GpfsCollector) Init(config json.RawMessage) error {
|
||||
for _, fs := range m.config.ExcludeFilesystem {
|
||||
m.skipFS[fs] = struct{}{}
|
||||
}
|
||||
m.lastState = make(map[string]GpfsCollectorLastState)
|
||||
|
||||
// GPFS / IBM Spectrum Scale file system statistics can only be queried by user root
|
||||
user, err := user.Current()
|
||||
@@ -89,7 +88,7 @@ func (m *GpfsCollector) Init(config json.RawMessage) error {
|
||||
return fmt.Errorf("failed to find mmpmon binary '%s': %v", m.config.Mmpmon, err)
|
||||
}
|
||||
m.config.Mmpmon = p
|
||||
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -119,8 +118,8 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
cmd.Stderr = cmdStderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
dataStdErr, _ := io.ReadAll(cmdStderr)
|
||||
dataStdOut, _ := io.ReadAll(cmdStdout)
|
||||
dataStdErr, _ := ioutil.ReadAll(cmdStderr)
|
||||
dataStdOut, _ := ioutil.ReadAll(cmdStdout)
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("Read(): Failed to execute command \"%s\": %v\n", cmd.String(), err),
|
||||
@@ -165,16 +164,11 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add filesystem tag
|
||||
m.tags["filesystem"] = filesystem
|
||||
|
||||
// Create initial last state
|
||||
if m.config.SendBandwidths {
|
||||
if _, ok := m.lastState[filesystem]; !ok {
|
||||
m.lastState[filesystem] = GpfsCollectorLastState{
|
||||
bytesRead: -1,
|
||||
bytesWritten: -1,
|
||||
}
|
||||
if _, ok := m.lastState[filesystem]; !ok {
|
||||
m.lastState[filesystem] = GpfsCollectorLastState{
|
||||
bytesRead: -1,
|
||||
bytesWritten: -1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,34 +211,16 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
fmt.Sprintf("Read(): Failed to convert bytes read '%s' to int64: %v", key_value["_br_"], err))
|
||||
continue
|
||||
}
|
||||
if y, err :=
|
||||
lp.New(
|
||||
"gpfs_bytes_read",
|
||||
m.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": bytesRead,
|
||||
},
|
||||
timestamp,
|
||||
); err == nil {
|
||||
y.AddMeta("unit", "bytes")
|
||||
if y, err := lp.New("gpfs_bytes_read", m.tags, m.meta, map[string]interface{}{"value": bytesRead}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
if m.config.SendBandwidths {
|
||||
if lastBytesRead := m.lastState[filesystem].bytesRead; lastBytesRead >= 0 {
|
||||
bwRead := float64(bytesRead-lastBytesRead) / timeDiff
|
||||
if y, err :=
|
||||
lp.New(
|
||||
"gpfs_bw_read",
|
||||
m.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": bwRead,
|
||||
},
|
||||
timestamp,
|
||||
); err == nil {
|
||||
y.AddMeta("unit", "bytes/sec")
|
||||
if y, err := lp.New("gpfs_bw_read", m.tags, m.meta, map[string]interface{}{"value": bwRead}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,34 +233,16 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
fmt.Sprintf("Read(): Failed to convert bytes written '%s' to int64: %v", key_value["_bw_"], err))
|
||||
continue
|
||||
}
|
||||
if y, err :=
|
||||
lp.New(
|
||||
"gpfs_bytes_written",
|
||||
m.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": bytesWritten,
|
||||
},
|
||||
timestamp,
|
||||
); err == nil {
|
||||
y.AddMeta("unit", "bytes")
|
||||
if y, err := lp.New("gpfs_bytes_written", m.tags, m.meta, map[string]interface{}{"value": bytesWritten}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
if m.config.SendBandwidths {
|
||||
if lastBytesWritten := m.lastState[filesystem].bytesRead; lastBytesWritten >= 0 {
|
||||
bwWrite := float64(bytesWritten-lastBytesWritten) / timeDiff
|
||||
if y, err :=
|
||||
lp.New(
|
||||
"gpfs_bw_write",
|
||||
m.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": bwWrite,
|
||||
},
|
||||
timestamp,
|
||||
); err == nil {
|
||||
y.AddMeta("unit", "bytes/sec")
|
||||
if y, err := lp.New("gpfs_bw_write", m.tags, m.meta, map[string]interface{}{"value": bwWrite}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,6 +264,7 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
}
|
||||
if y, err := lp.New("gpfs_num_opens", m.tags, m.meta, map[string]interface{}{"value": numOpens}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
|
||||
// number of closes
|
||||
@@ -318,6 +277,7 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
}
|
||||
if y, err := lp.New("gpfs_num_closes", m.tags, m.meta, map[string]interface{}{"value": numCloses}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
|
||||
// number of reads
|
||||
@@ -330,6 +290,7 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
}
|
||||
if y, err := lp.New("gpfs_num_reads", m.tags, m.meta, map[string]interface{}{"value": numReads}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
|
||||
// number of writes
|
||||
@@ -342,6 +303,7 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
}
|
||||
if y, err := lp.New("gpfs_num_writes", m.tags, m.meta, map[string]interface{}{"value": numWrites}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
|
||||
// number of read directories
|
||||
@@ -354,6 +316,7 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
}
|
||||
if y, err := lp.New("gpfs_num_readdirs", m.tags, m.meta, map[string]interface{}{"value": numReaddirs}, timestamp); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
|
||||
// Number of inode updates
|
||||
@@ -365,50 +328,11 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
continue
|
||||
}
|
||||
if y, err := lp.New("gpfs_num_inode_updates", m.tags, m.meta, map[string]interface{}{"value": numInodeUpdates}, timestamp); err == nil {
|
||||
m.statsProcessedMetrics++
|
||||
output <- y
|
||||
}
|
||||
|
||||
// Total values
|
||||
if m.config.SendTotalValues {
|
||||
bytesTotal := bytesRead + bytesWritten
|
||||
if y, err :=
|
||||
lp.New("gpfs_bytes_total",
|
||||
m.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": bytesTotal,
|
||||
},
|
||||
timestamp,
|
||||
); err == nil {
|
||||
y.AddMeta("unit", "bytes")
|
||||
output <- y
|
||||
}
|
||||
iops := numReads + numWrites
|
||||
if y, err :=
|
||||
lp.New("gpfs_iops",
|
||||
m.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": iops,
|
||||
},
|
||||
timestamp,
|
||||
); err == nil {
|
||||
output <- y
|
||||
}
|
||||
metaops := numInodeUpdates + numCloses + numOpens + numReaddirs
|
||||
if y, err :=
|
||||
lp.New("gpfs_metaops",
|
||||
m.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": metaops,
|
||||
},
|
||||
timestamp,
|
||||
); err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *GpfsCollector) Close() {
|
||||
|
@@ -6,8 +6,7 @@
|
||||
"exclude_filesystem": [
|
||||
"fs1"
|
||||
],
|
||||
"send_bandwidths": true,
|
||||
"send_total_values": true
|
||||
"send_bandwidths" : true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -27,12 +26,8 @@ Metrics:
|
||||
* `gpfs_num_opens`
|
||||
* `gpfs_num_closes`
|
||||
* `gpfs_num_reads`
|
||||
* `gpfs_num_writes`
|
||||
* `gpfs_num_readdirs`
|
||||
* `gpfs_num_inode_updates`
|
||||
* `gpfs_bytes_total = gpfs_bytes_read + gpfs_bytes_written` (if `send_total_values == true`)
|
||||
* `gpfs_iops = gpfs_num_reads + gpfs_num_writes` (if `send_total_values == true`)
|
||||
* `gpfs_metaops = gpfs_num_inode_updates + gpfs_num_closes + gpfs_num_opens + gpfs_num_readdirs` (if `send_total_values == true`)
|
||||
* `gpfs_bw_read` (if `send_bandwidths == true`)
|
||||
* `gpfs_bw_write` (if `send_bandwidths == true`)
|
||||
|
||||
|
@@ -2,10 +2,12 @@ package collectors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"encoding/json"
|
||||
@@ -18,22 +20,17 @@ import (
|
||||
const IB_BASEPATH = "/sys/class/infiniband/"
|
||||
|
||||
type InfinibandCollectorMetric struct {
|
||||
name string
|
||||
path string
|
||||
unit string
|
||||
scale int64
|
||||
addToIBTotal bool
|
||||
addToIBTotalPkgs bool
|
||||
currentState int64
|
||||
lastState int64
|
||||
path string
|
||||
unit string
|
||||
}
|
||||
|
||||
type InfinibandCollectorInfo struct {
|
||||
LID string // IB local Identifier (LID)
|
||||
device string // IB device
|
||||
port string // IB device port
|
||||
portCounterFiles []InfinibandCollectorMetric // mapping counter name -> InfinibandCollectorMetric
|
||||
tagSet map[string]string // corresponding tag list
|
||||
LID string // IB local Identifier (LID)
|
||||
device string // IB device
|
||||
port string // IB device port
|
||||
portCounterFiles map[string]InfinibandCollectorMetric // mapping counter name -> InfinibandCollectorMetric
|
||||
tagSet map[string]string // corresponding tag list
|
||||
lastState map[string]int64 // State from last measurement
|
||||
}
|
||||
|
||||
type InfinibandCollector struct {
|
||||
@@ -41,11 +38,11 @@ type InfinibandCollector struct {
|
||||
config struct {
|
||||
ExcludeDevices []string `json:"exclude_devices,omitempty"` // IB device to exclude e.g. mlx5_0
|
||||
SendAbsoluteValues bool `json:"send_abs_values"` // Send absolut values as read from sys filesystem
|
||||
SendTotalValues bool `json:"send_total_values"` // Send computed total values
|
||||
SendDerivedValues bool `json:"send_derived_values"` // Send derived values e.g. rates
|
||||
}
|
||||
info []InfinibandCollectorInfo
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
info []*InfinibandCollectorInfo
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
// Init initializes the Infiniband collector by walking through files below IB_BASEPATH
|
||||
@@ -59,7 +56,6 @@ func (m *InfinibandCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "InfinibandCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
"group": "Network",
|
||||
@@ -89,7 +85,7 @@ func (m *InfinibandCollector) Init(config json.RawMessage) error {
|
||||
for _, path := range ibDirs {
|
||||
|
||||
// Skip, when no LID is assigned
|
||||
line, err := os.ReadFile(filepath.Join(path, "lid"))
|
||||
line, err := ioutil.ReadFile(filepath.Join(path, "lid"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -117,39 +113,11 @@ func (m *InfinibandCollector) Init(config json.RawMessage) error {
|
||||
|
||||
// Check access to counter files
|
||||
countersDir := filepath.Join(path, "counters")
|
||||
portCounterFiles := []InfinibandCollectorMetric{
|
||||
{
|
||||
name: "ib_recv",
|
||||
path: filepath.Join(countersDir, "port_rcv_data"),
|
||||
unit: "bytes",
|
||||
scale: 4,
|
||||
addToIBTotal: true,
|
||||
lastState: -1,
|
||||
},
|
||||
{
|
||||
name: "ib_xmit",
|
||||
path: filepath.Join(countersDir, "port_xmit_data"),
|
||||
unit: "bytes",
|
||||
scale: 4,
|
||||
addToIBTotal: true,
|
||||
lastState: -1,
|
||||
},
|
||||
{
|
||||
name: "ib_recv_pkts",
|
||||
path: filepath.Join(countersDir, "port_rcv_packets"),
|
||||
unit: "packets",
|
||||
scale: 1,
|
||||
addToIBTotalPkgs: true,
|
||||
lastState: -1,
|
||||
},
|
||||
{
|
||||
name: "ib_xmit_pkts",
|
||||
path: filepath.Join(countersDir, "port_xmit_packets"),
|
||||
unit: "packets",
|
||||
scale: 1,
|
||||
addToIBTotalPkgs: true,
|
||||
lastState: -1,
|
||||
},
|
||||
portCounterFiles := map[string]InfinibandCollectorMetric{
|
||||
"ib_recv": {path: filepath.Join(countersDir, "port_rcv_data"), unit: "bytes"},
|
||||
"ib_xmit": {path: filepath.Join(countersDir, "port_xmit_data"), unit: "bytes"},
|
||||
"ib_recv_pkts": {path: filepath.Join(countersDir, "port_rcv_packets"), unit: "packets"},
|
||||
"ib_xmit_pkts": {path: filepath.Join(countersDir, "port_xmit_packets"), unit: "packets"},
|
||||
}
|
||||
for _, counter := range portCounterFiles {
|
||||
err := unix.Access(counter.path, unix.R_OK)
|
||||
@@ -158,8 +126,14 @@ func (m *InfinibandCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize last state
|
||||
lastState := make(map[string]int64)
|
||||
for counter := range portCounterFiles {
|
||||
lastState[counter] = -1
|
||||
}
|
||||
|
||||
m.info = append(m.info,
|
||||
InfinibandCollectorInfo{
|
||||
&InfinibandCollectorInfo{
|
||||
LID: LID,
|
||||
device: device,
|
||||
port: port,
|
||||
@@ -170,13 +144,14 @@ func (m *InfinibandCollector) Init(config json.RawMessage) error {
|
||||
"port": port,
|
||||
"lid": LID,
|
||||
},
|
||||
lastState: lastState,
|
||||
})
|
||||
}
|
||||
|
||||
if len(m.info) == 0 {
|
||||
return fmt.Errorf("found no IB devices")
|
||||
}
|
||||
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -196,15 +171,11 @@ func (m *InfinibandCollector) Read(interval time.Duration, output chan lp.CCMetr
|
||||
// Save current timestamp
|
||||
m.lastTimestamp = now
|
||||
|
||||
for i := range m.info {
|
||||
info := &m.info[i]
|
||||
|
||||
var ib_total, ib_total_pkts int64
|
||||
for i := range info.portCounterFiles {
|
||||
counterDef := &info.portCounterFiles[i]
|
||||
for _, info := range m.info {
|
||||
for counterName, counterDef := range info.portCounterFiles {
|
||||
|
||||
// Read counter file
|
||||
line, err := os.ReadFile(counterDef.path)
|
||||
line, err := ioutil.ReadFile(counterDef.path)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
@@ -218,92 +189,36 @@ func (m *InfinibandCollector) Read(interval time.Duration, output chan lp.CCMetr
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("Read(): Failed to convert Infininiband metrice %s='%s' to int64: %v", counterDef.name, data, err))
|
||||
fmt.Sprintf("Read(): Failed to convert Infininiband metrice %s='%s' to int64: %v", counterName, data, err))
|
||||
continue
|
||||
}
|
||||
// Scale raw value
|
||||
v *= counterDef.scale
|
||||
|
||||
// Save current state
|
||||
counterDef.currentState = v
|
||||
|
||||
// Send absolut values
|
||||
if m.config.SendAbsoluteValues {
|
||||
if y, err :=
|
||||
lp.New(
|
||||
counterDef.name,
|
||||
info.tagSet,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": counterDef.currentState,
|
||||
},
|
||||
now); err == nil {
|
||||
if y, err := lp.New(counterName, info.tagSet, m.meta, map[string]interface{}{"value": v}, now); err == nil {
|
||||
y.AddMeta("unit", counterDef.unit)
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
|
||||
// Send derived values
|
||||
if m.config.SendDerivedValues {
|
||||
if counterDef.lastState >= 0 {
|
||||
rate := float64((counterDef.currentState - counterDef.lastState)) / timeDiff
|
||||
if y, err :=
|
||||
lp.New(
|
||||
counterDef.name+"_bw",
|
||||
info.tagSet,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": rate,
|
||||
},
|
||||
now); err == nil {
|
||||
if info.lastState[counterName] >= 0 {
|
||||
rate := float64((v - info.lastState[counterName])) / timeDiff
|
||||
if y, err := lp.New(counterName+"_bw", info.tagSet, m.meta, map[string]interface{}{"value": rate}, now); err == nil {
|
||||
y.AddMeta("unit", counterDef.unit+"/sec")
|
||||
output <- y
|
||||
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
counterDef.lastState = counterDef.currentState
|
||||
}
|
||||
|
||||
// Sum up total values
|
||||
if m.config.SendTotalValues {
|
||||
switch {
|
||||
case counterDef.addToIBTotal:
|
||||
ib_total += counterDef.currentState
|
||||
case counterDef.addToIBTotalPkgs:
|
||||
ib_total_pkts += counterDef.currentState
|
||||
}
|
||||
// Save current state
|
||||
info.lastState[counterName] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Send total values
|
||||
if m.config.SendTotalValues {
|
||||
if y, err :=
|
||||
lp.New(
|
||||
"ib_total",
|
||||
info.tagSet,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": ib_total,
|
||||
},
|
||||
now); err == nil {
|
||||
y.AddMeta("unit", "bytes")
|
||||
output <- y
|
||||
}
|
||||
|
||||
if y, err :=
|
||||
lp.New(
|
||||
"ib_total_pkts",
|
||||
info.tagSet,
|
||||
m.meta,
|
||||
map[string]interface{}{
|
||||
"value": ib_total_pkts,
|
||||
},
|
||||
now); err == nil {
|
||||
y.AddMeta("unit", "packets")
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *InfinibandCollector) Close() {
|
||||
|
@@ -17,16 +17,13 @@ LID file (`/sys/class/infiniband/<dev>/ports/<port>/lid`)
|
||||
|
||||
The devices can be filtered with the `exclude_devices` option in the configuration.
|
||||
|
||||
For each found LID the collector reads data through the sysfs files below `/sys/class/infiniband/<device>`. (See: <https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-infiniband>)
|
||||
For each found LID the collector reads data through the sysfs files below `/sys/class/infiniband/<device>`.
|
||||
|
||||
Metrics:
|
||||
|
||||
* `ib_recv`
|
||||
* `ib_xmit`
|
||||
* `ib_recv_pkts`
|
||||
* `ib_xmit_pkts`
|
||||
* `ib_total = ib_recv + ib_xmit` (if `send_total_values == true`)
|
||||
* `ib_total_pkts = ib_recv_pkts + ib_xmit_pkts` (if `send_total_values == true`)
|
||||
* `ib_recv_bw` (if `send_derived_values == true`)
|
||||
* `ib_xmit_bw` (if `send_derived_values == true`)
|
||||
* `ib_recv_pkts_bw` (if `send_derived_values == true`)
|
||||
|
@@ -4,8 +4,9 @@ import (
|
||||
"bufio"
|
||||
"os"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
|
||||
// "log"
|
||||
"encoding/json"
|
||||
@@ -29,15 +30,15 @@ type IOstatCollectorEntry struct {
|
||||
|
||||
type IOstatCollector struct {
|
||||
metricCollector
|
||||
matches map[string]int
|
||||
config IOstatCollectorConfig
|
||||
devices map[string]IOstatCollectorEntry
|
||||
matches map[string]int
|
||||
config IOstatCollectorConfig
|
||||
devices map[string]IOstatCollectorEntry
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *IOstatCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "IOstatCollector"
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{"source": m.name, "group": "Disk"}
|
||||
m.setup()
|
||||
if len(config) > 0 {
|
||||
@@ -103,6 +104,7 @@ func (m *IOstatCollector) Init(config json.RawMessage) error {
|
||||
lastValues: values,
|
||||
}
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
@@ -142,6 +144,7 @@ func (m *IOstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
y, err := lp.New(name, entry.tags, m.meta, map[string]interface{}{"value": int(diff)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
entry.lastValues[name] = x
|
||||
@@ -149,6 +152,7 @@ func (m *IOstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
}
|
||||
m.devices[device] = entry
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *IOstatCollector) Close() {
|
||||
|
@@ -1,57 +1,52 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const IPMITOOL_PATH = `ipmitool`
|
||||
const IPMISENSORS_PATH = `ipmi-sensors`
|
||||
|
||||
type IpmiCollectorConfig struct {
|
||||
ExcludeDevices []string `json:"exclude_devices"`
|
||||
IpmitoolPath string `json:"ipmitool_path"`
|
||||
IpmisensorsPath string `json:"ipmisensors_path"`
|
||||
}
|
||||
|
||||
type IpmiCollector struct {
|
||||
metricCollector
|
||||
config struct {
|
||||
ExcludeDevices []string `json:"exclude_devices"`
|
||||
IpmitoolPath string `json:"ipmitool_path"`
|
||||
IpmisensorsPath string `json:"ipmisensors_path"`
|
||||
}
|
||||
ipmitool string
|
||||
ipmisensors string
|
||||
//tags map[string]string
|
||||
//matches map[string]string
|
||||
config IpmiCollectorConfig
|
||||
ipmitool string
|
||||
ipmisensors string
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *IpmiCollector) Init(config json.RawMessage) error {
|
||||
// Check if already initialized
|
||||
if m.init {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.name = "IpmiCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
"group": "IPMI",
|
||||
}
|
||||
// default path to IPMI tools
|
||||
m.config.IpmitoolPath = "ipmitool"
|
||||
m.config.IpmisensorsPath = "ipmi-sensors"
|
||||
m.meta = map[string]string{"source": m.name, "group": "IPMI"}
|
||||
m.config.IpmitoolPath = string(IPMITOOL_PATH)
|
||||
m.config.IpmisensorsPath = string(IPMISENSORS_PATH)
|
||||
m.ipmitool = ""
|
||||
m.ipmisensors = ""
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Check if executables ipmitool or ipmisensors are found
|
||||
p, err := exec.LookPath(m.config.IpmitoolPath)
|
||||
if err == nil {
|
||||
m.ipmitool = p
|
||||
@@ -63,38 +58,31 @@ func (m *IpmiCollector) Init(config json.RawMessage) error {
|
||||
if len(m.ipmitool) == 0 && len(m.ipmisensors) == 0 {
|
||||
return errors.New("no IPMI reader found")
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IpmiCollector) readIpmiTool(cmd string, output chan lp.CCMetric) {
|
||||
|
||||
// Setup ipmitool command
|
||||
command := exec.Command(cmd, "sensor")
|
||||
stdout, _ := command.StdoutPipe()
|
||||
errBuf := new(bytes.Buffer)
|
||||
command.Stderr = errBuf
|
||||
|
||||
// start command
|
||||
if err := command.Start(); err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("readIpmiTool(): Failed to start command \"%s\": %v", command.String(), err),
|
||||
)
|
||||
command.Wait()
|
||||
stdout, err := command.Output()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Read command output
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
lv := strings.Split(scanner.Text(), "|")
|
||||
ll := strings.Split(string(stdout), "\n")
|
||||
|
||||
for _, line := range ll {
|
||||
lv := strings.Split(line, "|")
|
||||
if len(lv) < 3 {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.ParseFloat(strings.TrimSpace(lv[1]), 64)
|
||||
v, err := strconv.ParseFloat(strings.Trim(lv[1], " "), 64)
|
||||
if err == nil {
|
||||
name := strings.ToLower(strings.Replace(strings.TrimSpace(lv[0]), " ", "_", -1))
|
||||
unit := strings.TrimSpace(lv[2])
|
||||
name := strings.ToLower(strings.Replace(strings.Trim(lv[0], " "), " ", "_", -1))
|
||||
unit := strings.Trim(lv[2], " ")
|
||||
if unit == "Volts" {
|
||||
unit = "Volts"
|
||||
} else if unit == "degrees C" {
|
||||
@@ -109,20 +97,10 @@ func (m *IpmiCollector) readIpmiTool(cmd string, output chan lp.CCMetric) {
|
||||
if err == nil {
|
||||
y.AddMeta("unit", unit)
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for command end
|
||||
if err := command.Wait(); err != nil {
|
||||
errMsg, _ := io.ReadAll(errBuf)
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("readIpmiTool(): Failed to wait for the end of command \"%s\": %v\n", command.String(), err),
|
||||
fmt.Sprintf("readIpmiTool(): command stderr: \"%s\"\n", string(errMsg)),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IpmiCollector) readIpmiSensors(cmd string, output chan lp.CCMetric) {
|
||||
@@ -149,6 +127,7 @@ func (m *IpmiCollector) readIpmiSensors(cmd string, output chan lp.CCMetric) {
|
||||
y.AddMeta("unit", lv[4])
|
||||
}
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,17 +135,18 @@ func (m *IpmiCollector) readIpmiSensors(cmd string, output chan lp.CCMetric) {
|
||||
}
|
||||
|
||||
func (m *IpmiCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
|
||||
// Check if already initialized
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
|
||||
if len(m.config.IpmitoolPath) > 0 {
|
||||
m.readIpmiTool(m.config.IpmitoolPath, output)
|
||||
_, err := os.Stat(m.config.IpmitoolPath)
|
||||
if err == nil {
|
||||
m.readIpmiTool(m.config.IpmitoolPath, output)
|
||||
}
|
||||
} else if len(m.config.IpmisensorsPath) > 0 {
|
||||
m.readIpmiSensors(m.config.IpmisensorsPath, output)
|
||||
_, err := os.Stat(m.config.IpmisensorsPath)
|
||||
if err == nil {
|
||||
m.readIpmiSensors(m.config.IpmisensorsPath, output)
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *IpmiCollector) Close() {
|
||||
|
@@ -8,6 +8,9 @@
|
||||
}
|
||||
```
|
||||
|
||||
The `ipmistat` collector reads data from `ipmitool` (`ipmitool sensor`) or `ipmi-sensors` (`ipmi-sensors --sdr-cache-recreate --comma-separated-output`).
|
||||
The `ipmistat` collector reads data from `ipmitool` (`ipmitool sensor`) or `ipmi-sensors` (`ipmi-sensors --sdr-cache-recreate --comma-separated-output`).
|
||||
|
||||
The metrics depend on the output of the underlying tools but contain temperature, power and energy metrics.
|
||||
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -3,77 +3,37 @@
|
||||
|
||||
The `likwid` collector is probably the most complicated collector. The LIKWID library is included as static library with *direct* access mode. The *direct* access mode is suitable if the daemon is executed by a root user. The static library does not contain the performance groups, so all information needs to be provided in the configuration.
|
||||
|
||||
```json
|
||||
"likwid": {
|
||||
"force_overwrite" : false,
|
||||
"invalid_to_zero" : false,
|
||||
"liblikwid_path" : "/path/to/liblikwid.so",
|
||||
"accessdaemon_path" : "/folder/that/contains/likwid-accessD",
|
||||
"access_mode" : "direct or accessdaemon or perf_event",
|
||||
"lockfile_path" : "/var/run/likwid.lock",
|
||||
"eventsets": [
|
||||
{
|
||||
"events" : {
|
||||
"COUNTER0": "EVENT0",
|
||||
"COUNTER1": "EVENT1",
|
||||
},
|
||||
"metrics" : [
|
||||
{
|
||||
"name": "sum_01",
|
||||
"calc": "COUNTER0 + COUNTER1",
|
||||
"publish": false,
|
||||
"unit": "myunit",
|
||||
"type": "hwthread"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"globalmetrics" : [
|
||||
{
|
||||
"name": "global_sum",
|
||||
"calc": "sum_01",
|
||||
"publish": true,
|
||||
"unit": "myunit",
|
||||
"type": "hwthread"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `likwid` configuration consists of two parts, the `eventsets` and `globalmetrics`:
|
||||
|
||||
- An event set list itself has two parts, the `events` and a set of derivable `metrics`. Each of the `events` is a `counter:event` pair in LIKWID's syntax. The `metrics` are a list of formulas to derive the metric value from the measurements of the `events`' values. Each metric has a name, the formula, a type and a publish flag. There is an optional `unit` field. Counter names can be used like variables in the formulas, so `PMC0+PMC1` sums the measurements for the both events configured in the counters `PMC0` and `PMC1`. You can optionally use `time` for the measurement time and `inverseClock` for `1.0/baseCpuFrequency`. The type tells the LikwidCollector whether it is a metric for each hardware thread (`cpu`) or each CPU socket (`socket`). You may specify a unit for the metric with `unit`. The last one is the publishing flag. It tells the LikwidCollector whether a metric should be sent to the router or is only used internally to compute a global metric.
|
||||
- The `globalmetrics` are metrics which require data from multiple event set measurements to be derived. The inputs are the metrics in the event sets. Similar to the metrics in the event sets, the global metrics are defined by a name, a formula, a type and a publish flag. See event set metrics for details. The only difference is that there is no access to the raw event measurements anymore but only to the metrics. Also `time` and `inverseClock` cannot be used anymore. So, the idea is to derive a metric in the `eventsets` section and reuse it in the `globalmetrics` part. If you need a metric only for deriving the global metrics, disable forwarding of the event set metrics (`"publish": false`). **Be aware** that the combination might be misleading because the "behavior" of a metric changes over time and the multiple measurements might count different computing phases. Similar to the metrics in the eventset, you can specify a metric unit with the `unit` field.
|
||||
The `likwid` configuration consists of two parts, the "eventsets" and "globalmetrics":
|
||||
- An event set list itself has two parts, the "events" and a set of derivable "metrics". Each of the "events" is a counter:event pair in LIKWID's syntax. The "metrics" are a list of formulas to derive the metric value from the measurements of the "events". Each metric has a name, the formula, a scope and a publish flag. Counter names can be used like variables in the formulas, so `PMC0+PMC1` sums the measurements for the both events configured in the counters `PMC0` and `PMC1`. The scope tells the Collector whether it is a metric for each hardware thread (`cpu`) or each CPU socket (`socket`). You may specify a unit for the metric with `unit`. The last one is the publishing flag. It tells the collector whether a metric should be sent to the router.
|
||||
- The global metrics are metrics which require data from all event set measurements to be derived. The inputs are the metrics in the event sets. Similar to the metrics in the event sets, the global metrics are defined by a name, a formula, a scope and a publish flag. See event set metrics for details. The only difference is that there is no access to the raw event measurements anymore but only to the metrics. So, the idea is to derive a metric in the "eventsets" section and reuse it in the "globalmetrics" part. If you need a metric only for deriving the global metrics, disable forwarding of the event set metrics (`publish=false`). **Be aware** that the combination might be misleading because the "behavior" of a metric changes over time and the multiple measurements might count different computing phases. Similar to the metrics in the eventset, you can specify a metric unit with the `unit` field.
|
||||
|
||||
Additional options:
|
||||
|
||||
- `access_mode` : Method to use for hardware performance monitoring (`direct` access as root user, `accessdaemon` for the daemon mode)
|
||||
- `accessdaemon_path`: Folder with the access daemon `likwid-accessD`, commonly `$LIKWID_INSTALL_LOC/sbin`
|
||||
- `force_overwrite`: Same as setting `LIKWID_FORCE=1`. In case counters are already in-use, LIKWID overwrites their configuration to do its measurements
|
||||
- `invalid_to_zero`: In some cases, the calculations result in `NaN` or `Inf`. With this option, all `NaN` and `Inf` values are replaces with `0.0`. See below in [seperate section](./likwidMetric.md#invalid_to_zero-option)
|
||||
- `access_mode`: Specify LIKWID access mode: `direct` for direct register access as root user or `accessdaemon`. The access mode `perf_event` is current untested.
|
||||
- `accessdaemon_path`: Folder of the accessDaemon `likwid-accessD` (like `/usr/local/sbin`)
|
||||
- `liblikwid_path`: Location of `liblikwid.so` including file name like `/usr/local/lib/liblikwid.so`
|
||||
- `lockfile_path`: Location of LIKWID's lock file if multiple tools should access the hardware counters. Default `/var/run/likwid.lock`
|
||||
- `invalid_to_zero`: In some cases, the calculations result in `NaN` or `Inf`. With this option, all `NaN` and `Inf` values are replaces with `0.0`.
|
||||
- `access_mode`: Specify LIKWID access mode: `direct` for direct register access as root user or `accessdaemon`
|
||||
- `accessdaemon_path`: Folder of the accessDaemon `likwid-accessD`
|
||||
- `liblikwid_path`: Location of `liblikwid.so`
|
||||
|
||||
### Available metric types
|
||||
### Available metric scopes
|
||||
|
||||
Hardware performance counters are scattered all over the system nowadays. A counter coveres a specific part of the system. While there are hardware thread specific counter for CPU cycles, instructions and so on, some others are specific for a whole CPU socket/package. To address that, the LikwidCollector provides the specification of a `type` for each metric.
|
||||
Hardware performance counters are scattered all over the system nowadays. A counter coveres a specific part of the system. While there are hardware thread specific counter for CPU cycles, instructions and so on, some others are specific for a whole CPU socket/package. To address that, the collector provides the specification of a 'scope' for each metric.
|
||||
|
||||
- `hwthread` : One metric per CPU hardware thread with the tags `"type" : "hwthread"` and `"type-id" : "$hwthread_id"`
|
||||
- `cpu` : One metric per CPU hardware thread with the tags `"type" : "cpu"` and `"type-id" : "$cpu_id"`
|
||||
- `socket` : One metric per CPU socket/package with the tags `"type" : "socket"` and `"type-id" : "$socket_id"`
|
||||
|
||||
**Note:** You cannot specify `socket` type for a metric that is measured at `hwthread` type, so some kind of expert knowledge or lookup work in the [Likwid Wiki](https://github.com/RRZE-HPC/likwid/wiki) is required. Get the type of each counter from the *Architecture* pages and as soon as one counter in a metric is socket-specific, the whole metric is socket-specific.
|
||||
**Note:** You cannot specify `socket` scope for a metric that is measured at `cpu` scope, so some kind of expert knowledge or lookup work in the [Likwid Wiki](https://github.com/RRZE-HPC/likwid/wiki) is required. Get the scope of each counter from the *Architecture* pages and as soon as one counter in a metric is socket-specific, the whole metric is socket-specific.
|
||||
|
||||
As a guideline:
|
||||
|
||||
- All counters `FIXCx`, `PMCy` and `TMAz` have the type `hwthread`
|
||||
- All counters names containing `BOX` have the type `socket`
|
||||
- All `PWRx` counters have type `socket`, except `"PWR1" : "RAPL_CORE_ENERGY"` has `hwthread` type
|
||||
- All `DFCx` counters have type `socket`
|
||||
- All counters `FIXCx`, `PMCy` and `TMAz` have the scope `cpu`
|
||||
- All counters names containing `BOX` have the scope `socket`
|
||||
- All `PWRx` counters have scope `socket`, except `"PWR1" : "RAPL_CORE_ENERGY"` has `cpu` scope
|
||||
- All `DFCx` counters have scope `socket`
|
||||
|
||||
### Help with the configuration
|
||||
|
||||
The configuration for the `likwid` collector is quite complicated. Most users don't use LIKWID with the event:counter notation but rely on the performance groups defined by the LIKWID team for each architecture. In order to help with the `likwid` collector configuration, we included a script `scripts/likwid_perfgroup_to_cc_config.py` that creates the configuration of an `eventset` from a performance group (using a LIKWID installation in `$PATH`):
|
||||
|
||||
```
|
||||
$ likwid-perfctr -i
|
||||
[...]
|
||||
@@ -90,7 +50,6 @@ $ scripts/likwid_perfgroup_to_cc_config.py ICX MEM_DP
|
||||
{
|
||||
"events": {
|
||||
"FIXC0": "INSTR_RETIRED_ANY",
|
||||
"FIXC1": "CPU_CLK_UNHALTED_CORE",
|
||||
"..." : "..."
|
||||
},
|
||||
"metrics" : [
|
||||
@@ -99,7 +58,7 @@ $ scripts/likwid_perfgroup_to_cc_config.py ICX MEM_DP
|
||||
"name": "Runtime (RDTSC) [s]",
|
||||
"publish": true,
|
||||
"unit": "seconds"
|
||||
"type": "hwthread"
|
||||
"scope": "cpu"
|
||||
},
|
||||
{
|
||||
"..." : "..."
|
||||
@@ -115,37 +74,22 @@ You can copy this JSON and add it to the `eventsets` list. If you specify multip
|
||||
LIKWID checks the file `/var/run/likwid.lock` before performing any interfering operations. Who is allowed to access the counters is determined by the owner of the file. If it does not exist, it is created for the current user. So, if you want to temporarly allow counter access to a user (e.g. in a job):
|
||||
|
||||
Before (SLURM prolog, ...)
|
||||
|
||||
```bash
|
||||
chown $JOBUSER /var/run/likwid.lock
|
||||
```
|
||||
$ chwon $JOBUSER /var/run/likwid.lock
|
||||
```
|
||||
|
||||
After (SLURM epilog, ...)
|
||||
|
||||
```bash
|
||||
chown $CCUSER /var/run/likwid.lock
|
||||
```
|
||||
|
||||
### `invalid_to_zero` option
|
||||
|
||||
In some cases LIKWID returns `0.0` for some events that are further used in processing and maybe used as divisor in a calculation. After evaluation of a metric, the result might be `NaN` or `+-Inf`. These resulting metrics are commonly not created and forwarded to the router because the [InfluxDB line protocol](https://docs.influxdata.com/influxdb/cloud/reference/syntax/line-protocol/#float) does not support these special floating-point values. If you want to have them sent, this option forces these metric values to be `0.0` instead.
|
||||
|
||||
One might think this does not happen often but often used metrics in the world of performance engineering like Instructions-per-Cycle (IPC) or more frequently the actual CPU clock are derived with events like `CPU_CLK_UNHALTED_CORE` (Intel) which do not increment in halted state (as the name implies). In there are different power management systems in a chip which can cause a hardware thread to go in such a state. Moreover, if no cycles are executed by the core, also many other events are not incremented as well (like `INSTR_RETIRED_ANY` for retired instructions and part of IPC).
|
||||
|
||||
### `send_*_total values` option
|
||||
|
||||
- `send_core_total_values`: Metrics, which are usually collected on a per hardware thread basis, are additionally summed up per CPU core.
|
||||
- `send_socket_total_values` Metrics, which are usually collected on a per hardware thread basis, are additionally summed up per CPU socket.
|
||||
- `send_node_total_values` Metrics, which are usually collected on a per hardware thread basis, are additionally summed up per node.
|
||||
$ chwon $CCUSER /var/run/likwid.lock
|
||||
```
|
||||
|
||||
### Example configuration
|
||||
|
||||
#### AMD Zen3
|
||||
|
||||
```json
|
||||
"likwid": {
|
||||
"force_overwrite" : false,
|
||||
"invalid_to_zero" : false,
|
||||
"nan_to_zero" : false,
|
||||
"eventsets": [
|
||||
{
|
||||
"events": {
|
||||
@@ -164,20 +108,20 @@ One might think this does not happen often but often used metrics in the world o
|
||||
{
|
||||
"name": "ipc",
|
||||
"calc": "PMC0/PMC1",
|
||||
"type": "hwthread",
|
||||
"type": "cpu",
|
||||
"publish": true
|
||||
},
|
||||
{
|
||||
"name": "flops_any",
|
||||
"calc": "0.000001*PMC2/time",
|
||||
"unit": "MFlops/s",
|
||||
"type": "hwthread",
|
||||
"type": "cpu",
|
||||
"publish": true
|
||||
},
|
||||
{
|
||||
"name": "clock",
|
||||
"calc": "0.000001*(FIXC1/FIXC2)/inverseClock",
|
||||
"type": "hwthread",
|
||||
"type": "cpu",
|
||||
"unit": "MHz",
|
||||
"publish": true
|
||||
},
|
||||
@@ -238,10 +182,9 @@ One might think this does not happen often but often used metrics in the world o
|
||||
|
||||
### How to get the eventsets and metrics from LIKWID
|
||||
|
||||
The `likwid` collector reads hardware performance counters at a **hwthread** and **socket** level. The configuration looks quite complicated but it is basically copy&paste from [LIKWID's performance groups](https://github.com/RRZE-HPC/likwid/tree/master/groups). The collector made multiple iterations and tried to use the performance groups but it lacked flexibility. The current way of configuration provides most flexibility.
|
||||
The `likwid` collector reads hardware performance counters at a **cpu** and **socket** level. The configuration looks quite complicated but it is basically copy&paste from [LIKWID's performance groups](https://github.com/RRZE-HPC/likwid/tree/master/groups). The collector made multiple iterations and tried to use the performance groups but it lacked flexibility. The current way of configuration provides most flexibility.
|
||||
|
||||
The logic is as following: There are multiple eventsets, each consisting of a list of counters+events and a list of metrics. If you compare a common performance group with the example setting above, there is not much difference:
|
||||
|
||||
```
|
||||
EVENTSET -> "events": {
|
||||
FIXC1 ACTUAL_CPU_CLOCK -> "FIXC1": "ACTUAL_CPU_CLOCK",
|
||||
@@ -260,10 +203,10 @@ METRICS -> "metrics": [
|
||||
IPC PMC0/PMC1 -> {
|
||||
-> "name" : "IPC",
|
||||
-> "calc" : "PMC0/PMC1",
|
||||
-> "type": "hwthread",
|
||||
-> "scope": "cpu",
|
||||
-> "publish": true
|
||||
-> }
|
||||
-> ]
|
||||
```
|
||||
|
||||
The script `scripts/likwid_perfgroup_to_cc_config.py` might help you.
|
||||
The script `scripts/likwid_perfgroup_to_cc_config.py` might help you.
|
@@ -3,13 +3,14 @@ package collectors
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
//
|
||||
@@ -32,11 +33,11 @@ type LoadavgCollector struct {
|
||||
config struct {
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
}
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *LoadavgCollector) Init(config json.RawMessage) error {
|
||||
m.name = "LoadavgCollector"
|
||||
m.parallel = true
|
||||
m.setup()
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
@@ -64,6 +65,7 @@ func (m *LoadavgCollector) Init(config json.RawMessage) error {
|
||||
for i, name := range m.proc_matches {
|
||||
_, m.proc_skips[i] = stringArrayContains(m.config.ExcludeMetrics, name)
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -72,7 +74,7 @@ func (m *LoadavgCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
buffer, err := os.ReadFile(LOADAVGFILE)
|
||||
buffer, err := ioutil.ReadFile(LOADAVGFILE)
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
@@ -99,6 +101,7 @@ func (m *LoadavgCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
y, err := lp.New(name, m.tags, m.meta, map[string]interface{}{"value": x}, now)
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +121,10 @@ func (m *LoadavgCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
y, err := lp.New(name, m.tags, m.meta, map[string]interface{}{"value": x}, now)
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *LoadavgCollector) Close() {
|
||||
|
@@ -10,8 +10,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const LUSTRE_SYSFS = `/sys/fs/lustre`
|
||||
@@ -37,13 +38,14 @@ type LustreMetricDefinition struct {
|
||||
|
||||
type LustreCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
config LustreCollectorConfig
|
||||
lctl string
|
||||
sudoCmd string
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
definitions []LustreMetricDefinition // Combined list without excluded metrics
|
||||
stats map[string]map[string]int64 // Data for last value per device and metric
|
||||
tags map[string]string
|
||||
config LustreCollectorConfig
|
||||
lctl string
|
||||
sudoCmd string
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
definitions []LustreMetricDefinition // Combined list without excluded metrics
|
||||
stats map[string]map[string]int64 // Data for last value per device and metric
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *LustreCollector) getDeviceDataCommand(device string) []string {
|
||||
@@ -101,7 +103,7 @@ func getMetricData(lines []string, prefix string, offset int) (int64, error) {
|
||||
// llitedir := filepath.Join(LUSTRE_SYSFS, "llite")
|
||||
// devdir := filepath.Join(llitedir, device)
|
||||
// statsfile := filepath.Join(devdir, "stats")
|
||||
// buffer, err := os.ReadFile(statsfile)
|
||||
// buffer, err := ioutil.ReadFile(statsfile)
|
||||
// if err != nil {
|
||||
// return make([]string, 0)
|
||||
// }
|
||||
@@ -288,7 +290,6 @@ var LustreDeriveMetrics = []LustreMetricDefinition{
|
||||
func (m *LustreCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "LustreCollector"
|
||||
m.parallel = true
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
@@ -373,6 +374,7 @@ func (m *LustreCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
}
|
||||
m.lastTimestamp = time.Now()
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -419,11 +421,13 @@ func (m *LustreCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
y.AddMeta("unit", def.unit)
|
||||
}
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
devData[def.name] = use_x
|
||||
}
|
||||
}
|
||||
m.lastTimestamp = now
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *LustreCollector) Close() {
|
||||
|
@@ -12,8 +12,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const MEMSTATFILE = "/proc/meminfo"
|
||||
@@ -32,12 +33,13 @@ type MemstatCollectorNode struct {
|
||||
|
||||
type MemstatCollector struct {
|
||||
metricCollector
|
||||
stats map[string]int64
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config MemstatCollectorConfig
|
||||
nodefiles map[int]MemstatCollectorNode
|
||||
sendMemUsed bool
|
||||
stats map[string]int64
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config MemstatCollectorConfig
|
||||
nodefiles map[int]MemstatCollectorNode
|
||||
sendMemUsed bool
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
type MemstatStats struct {
|
||||
@@ -68,8 +70,7 @@ func getStats(filename string) map[string]MemstatStats {
|
||||
} else if len(linefields) == 5 {
|
||||
v, err := strconv.ParseFloat(linefields[3], 64)
|
||||
if err == nil {
|
||||
cclog.ComponentDebug("getStats", strings.Trim(linefields[2], ":"), v, linefields[4])
|
||||
stats[strings.Trim(linefields[2], ":")] = MemstatStats{
|
||||
stats[strings.Trim(linefields[0], ":")] = MemstatStats{
|
||||
value: v,
|
||||
unit: linefields[4],
|
||||
}
|
||||
@@ -82,7 +83,6 @@ func getStats(filename string) map[string]MemstatStats {
|
||||
func (m *MemstatCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "MemstatCollector"
|
||||
m.parallel = true
|
||||
m.config.NodeStats = true
|
||||
m.config.NumaStats = false
|
||||
if len(config) > 0 {
|
||||
@@ -155,6 +155,7 @@ func (m *MemstatCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
@@ -180,6 +181,7 @@ func (m *MemstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
if len(unit) > 0 {
|
||||
y.AddMeta("unit", unit)
|
||||
}
|
||||
m.statsProcessedMetrics++
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
@@ -188,20 +190,16 @@ func (m *MemstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
unit := ""
|
||||
if totalVal, total := stats["MemTotal"]; total {
|
||||
if freeVal, free := stats["MemFree"]; free {
|
||||
memUsed = totalVal.value - freeVal.value
|
||||
if len(totalVal.unit) > 0 {
|
||||
unit = totalVal.unit
|
||||
} else if len(freeVal.unit) > 0 {
|
||||
unit = freeVal.unit
|
||||
}
|
||||
if bufVal, buffers := stats["Buffers"]; buffers {
|
||||
memUsed -= bufVal.value
|
||||
if len(bufVal.unit) > 0 && len(unit) == 0 {
|
||||
unit = bufVal.unit
|
||||
}
|
||||
if cacheVal, cached := stats["Cached"]; cached {
|
||||
memUsed -= cacheVal.value
|
||||
if len(cacheVal.unit) > 0 && len(unit) == 0 {
|
||||
memUsed = totalVal.value - (freeVal.value + bufVal.value + cacheVal.value)
|
||||
if len(totalVal.unit) > 0 {
|
||||
unit = totalVal.unit
|
||||
} else if len(freeVal.unit) > 0 {
|
||||
unit = freeVal.unit
|
||||
} else if len(bufVal.unit) > 0 {
|
||||
unit = bufVal.unit
|
||||
} else if len(cacheVal.unit) > 0 {
|
||||
unit = cacheVal.unit
|
||||
}
|
||||
}
|
||||
@@ -213,6 +211,7 @@ func (m *MemstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
if len(unit) > 0 {
|
||||
y.AddMeta("unit", unit)
|
||||
}
|
||||
m.statsProcessedMetrics++
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
@@ -229,6 +228,7 @@ func (m *MemstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
sendStats(stats, nodeConf.tags)
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "collected_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *MemstatCollector) Close() {
|
||||
|
@@ -3,25 +3,27 @@ package collectors
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
type MetricCollector interface {
|
||||
Name() string // Name of the metric collector
|
||||
Init(config json.RawMessage) error // Initialize metric collector
|
||||
Initialized() bool // Is metric collector initialized?
|
||||
Parallel() bool
|
||||
Name() string // Name of the metric collector
|
||||
Init(config json.RawMessage) error // Initialize metric collector
|
||||
Initialized() bool // Is metric collector initialized?
|
||||
Read(duration time.Duration, output chan lp.CCMetric) // Read metrics from metric collector
|
||||
Close() // Close / finish metric collector
|
||||
}
|
||||
|
||||
type metricCollector struct {
|
||||
name string // name of the metric
|
||||
init bool // is metric collector initialized?
|
||||
parallel bool // can the metric collector be executed in parallel with others
|
||||
meta map[string]string // static meta data tags
|
||||
name string // name of the metric
|
||||
init bool // is metric collector initialized?
|
||||
meta map[string]string // static meta data tags
|
||||
}
|
||||
|
||||
// Name returns the name of the metric collector
|
||||
@@ -29,11 +31,6 @@ func (c *metricCollector) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
// Name returns the name of the metric collector
|
||||
func (c *metricCollector) Parallel() bool {
|
||||
return c.parallel
|
||||
}
|
||||
|
||||
// Setup is for future use
|
||||
func (c *metricCollector) setup() error {
|
||||
return nil
|
||||
@@ -68,6 +65,58 @@ func stringArrayContains(array []string, str string) (int, bool) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// SocketList returns the list of physical sockets as read from /proc/cpuinfo
|
||||
func SocketList() []int {
|
||||
buffer, err := ioutil.ReadFile("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
ll := strings.Split(string(buffer), "\n")
|
||||
var packs []int
|
||||
for _, line := range ll {
|
||||
if strings.HasPrefix(line, "physical id") {
|
||||
lv := strings.Fields(line)
|
||||
id, err := strconv.ParseInt(lv[3], 10, 32)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return packs
|
||||
}
|
||||
_, found := intArrayContains(packs, int(id))
|
||||
if !found {
|
||||
packs = append(packs, int(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
return packs
|
||||
}
|
||||
|
||||
// CpuList returns the list of physical CPUs (in contrast to logical CPUs) as read from /proc/cpuinfo
|
||||
func CpuList() []int {
|
||||
buffer, err := ioutil.ReadFile("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
ll := strings.Split(string(buffer), "\n")
|
||||
var cpulist []int
|
||||
for _, line := range ll {
|
||||
if strings.HasPrefix(line, "processor") {
|
||||
lv := strings.Fields(line)
|
||||
id, err := strconv.ParseInt(lv[2], 10, 32)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return cpulist
|
||||
}
|
||||
_, found := intArrayContains(cpulist, int(id))
|
||||
if !found {
|
||||
cpulist = append(cpulist, int(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
return cpulist
|
||||
}
|
||||
|
||||
// RemoveFromStringList removes the string r from the array of strings s
|
||||
// If r is not contained in the array an error is returned
|
||||
func RemoveFromStringList(s []string, r string) ([]string, error) {
|
||||
|
@@ -9,8 +9,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const NETSTATFILE = "/proc/net/dev"
|
||||
@@ -32,14 +33,14 @@ type NetstatCollectorMetric struct {
|
||||
|
||||
type NetstatCollector struct {
|
||||
metricCollector
|
||||
config NetstatCollectorConfig
|
||||
matches map[string][]NetstatCollectorMetric
|
||||
lastTimestamp time.Time
|
||||
config NetstatCollectorConfig
|
||||
matches map[string][]NetstatCollectorMetric
|
||||
lastTimestamp time.Time
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *NetstatCollector) Init(config json.RawMessage) error {
|
||||
m.name = "NetstatCollector"
|
||||
m.parallel = true
|
||||
m.setup()
|
||||
m.lastTimestamp = time.Now()
|
||||
|
||||
@@ -102,7 +103,7 @@ func (m *NetstatCollector) Init(config json.RawMessage) error {
|
||||
|
||||
// Check if device is a included device
|
||||
if _, ok := stringArrayContains(m.config.IncludeDevices, dev); ok {
|
||||
tags := map[string]string{"stype": "network", "stype-id": dev, "type": "node"}
|
||||
tags := map[string]string{"device": dev, "type": "node"}
|
||||
meta_unit_byte := map[string]string{"source": m.name, "group": "Network", "unit": "bytes"}
|
||||
meta_unit_byte_per_sec := map[string]string{"source": m.name, "group": "Network", "unit": "bytes/sec"}
|
||||
meta_unit_pkts := map[string]string{"source": m.name, "group": "Network", "unit": "packets"}
|
||||
@@ -149,6 +150,7 @@ func (m *NetstatCollector) Init(config json.RawMessage) error {
|
||||
if len(m.matches) == 0 {
|
||||
return errors.New("no devices to collector metrics found")
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -199,6 +201,7 @@ func (m *NetstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
if m.config.SendAbsoluteValues {
|
||||
if y, err := lp.New(metric.name, metric.tags, metric.meta, map[string]interface{}{"value": v}, now); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
if m.config.SendDerivedValues {
|
||||
@@ -206,6 +209,7 @@ func (m *NetstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
rate := float64(v-metric.lastValue) / timeDiff
|
||||
if y, err := lp.New(metric.name+"_bw", metric.tags, metric.meta_rates, map[string]interface{}{"value": rate}, now); err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
metric.lastValue = v
|
||||
@@ -213,6 +217,7 @@ func (m *NetstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
}
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *NetstatCollector) Close() {
|
||||
|
@@ -23,5 +23,5 @@ Metrics:
|
||||
* `net_pkts_in_bw` (`unit=packets/sec` if `send_derived_values == true`)
|
||||
* `net_pkts_out_bw` (`unit=packets/sec` if `send_derived_values == true`)
|
||||
|
||||
The device name is added as tag `stype=network,stype-id=<device>`.
|
||||
The device name is added as tag `device`.
|
||||
|
||||
|
@@ -11,7 +11,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
// First part contains the code for the general NfsCollector.
|
||||
@@ -32,7 +33,8 @@ type nfsCollector struct {
|
||||
Nfsstats string `json:"nfsstat"`
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
}
|
||||
data map[string]NfsCollectorData
|
||||
data map[string]NfsCollectorData
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *nfsCollector) initStats() error {
|
||||
@@ -113,8 +115,8 @@ func (m *nfsCollector) MainInit(config json.RawMessage) error {
|
||||
}
|
||||
m.data = make(map[string]NfsCollectorData)
|
||||
m.initStats()
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
m.parallel = true
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -144,8 +146,10 @@ func (m *nfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
if err == nil {
|
||||
y.AddMeta("version", m.version)
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *nfsCollector) Close() {
|
||||
|
@@ -1,166 +0,0 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
)
|
||||
|
||||
// These are the fields we read from the JSON configuration
|
||||
type NfsIOStatCollectorConfig struct {
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
ExcludeFilesystem []string `json:"exclude_filesystem,omitempty"`
|
||||
UseServerAddressAsSType bool `json:"use_server_as_stype,omitempty"`
|
||||
}
|
||||
|
||||
// This contains all variables we need during execution and the variables
|
||||
// defined by metricCollector (name, init, ...)
|
||||
type NfsIOStatCollector struct {
|
||||
metricCollector
|
||||
config NfsIOStatCollectorConfig // the configuration structure
|
||||
meta map[string]string // default meta information
|
||||
tags map[string]string // default tags
|
||||
data map[string]map[string]int64 // data storage for difference calculation
|
||||
key string // which device info should be used as subtype ID? 'server' or 'mntpoint', see NfsIOStatCollectorConfig.UseServerAddressAsSType
|
||||
}
|
||||
|
||||
var deviceRegex = regexp.MustCompile(`device (?P<server>[^ ]+) mounted on (?P<mntpoint>[^ ]+) with fstype nfs(?P<version>\d*) statvers=[\d\.]+`)
|
||||
var bytesRegex = regexp.MustCompile(`\s+bytes:\s+(?P<nread>[^ ]+) (?P<nwrite>[^ ]+) (?P<dread>[^ ]+) (?P<dwrite>[^ ]+) (?P<nfsread>[^ ]+) (?P<nfswrite>[^ ]+) (?P<pageread>[^ ]+) (?P<pagewrite>[^ ]+)`)
|
||||
|
||||
func resolve_regex_fields(s string, regex *regexp.Regexp) map[string]string {
|
||||
fields := make(map[string]string)
|
||||
groups := regex.SubexpNames()
|
||||
for _, match := range regex.FindAllStringSubmatch(s, -1) {
|
||||
for groupIdx, group := range match {
|
||||
if len(groups[groupIdx]) > 0 {
|
||||
fields[groups[groupIdx]] = group
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func (m *NfsIOStatCollector) readNfsiostats() map[string]map[string]int64 {
|
||||
data := make(map[string]map[string]int64)
|
||||
filename := "/proc/self/mountstats"
|
||||
stats, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
|
||||
lines := strings.Split(string(stats), "\n")
|
||||
var current map[string]string = nil
|
||||
for _, l := range lines {
|
||||
// Is this a device line with mount point, remote target and NFS version?
|
||||
dev := resolve_regex_fields(l, deviceRegex)
|
||||
if len(dev) > 0 {
|
||||
if _, ok := stringArrayContains(m.config.ExcludeFilesystem, dev[m.key]); !ok {
|
||||
current = dev
|
||||
if len(current["version"]) == 0 {
|
||||
current["version"] = "3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(current) > 0 {
|
||||
// Byte line parsing (if found the device for it)
|
||||
bytes := resolve_regex_fields(l, bytesRegex)
|
||||
if len(bytes) > 0 {
|
||||
data[current[m.key]] = make(map[string]int64)
|
||||
for name, sval := range bytes {
|
||||
if _, ok := stringArrayContains(m.config.ExcludeMetrics, name); !ok {
|
||||
val, err := strconv.ParseInt(sval, 10, 64)
|
||||
if err == nil {
|
||||
data[current[m.key]][name] = val
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
current = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (m *NfsIOStatCollector) Init(config json.RawMessage) error {
|
||||
var err error = nil
|
||||
m.name = "NfsIOStatCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{"source": m.name, "group": "NFS", "unit": "bytes"}
|
||||
m.tags = map[string]string{"type": "node"}
|
||||
m.config.UseServerAddressAsSType = false
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.key = "mntpoint"
|
||||
if m.config.UseServerAddressAsSType {
|
||||
m.key = "server"
|
||||
}
|
||||
m.data = m.readNfsiostats()
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *NfsIOStatCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
timestamp := time.Now()
|
||||
|
||||
// Get the current values for all mountpoints
|
||||
newdata := m.readNfsiostats()
|
||||
|
||||
for mntpoint, values := range newdata {
|
||||
// Was the mount point already present in the last iteration
|
||||
if old, ok := m.data[mntpoint]; ok {
|
||||
// Calculate the difference of old and new values
|
||||
for i := range values {
|
||||
x := values[i] - old[i]
|
||||
y, err := lp.New(fmt.Sprintf("nfsio_%s", i), m.tags, m.meta, map[string]interface{}{"value": x}, timestamp)
|
||||
if err == nil {
|
||||
if strings.HasPrefix(i, "page") {
|
||||
y.AddMeta("unit", "4K_Pages")
|
||||
}
|
||||
y.AddTag("stype", "filesystem")
|
||||
y.AddTag("stype-id", mntpoint)
|
||||
// Send it to output channel
|
||||
output <- y
|
||||
}
|
||||
// Update old to the new value for the next iteration
|
||||
old[i] = values[i]
|
||||
}
|
||||
} else {
|
||||
// First time we see this mount point, store all values
|
||||
m.data[mntpoint] = values
|
||||
}
|
||||
}
|
||||
// Reset entries that do not exist anymore
|
||||
for mntpoint := range m.data {
|
||||
found := false
|
||||
for new := range newdata {
|
||||
if new == mntpoint {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
m.data[mntpoint] = nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *NfsIOStatCollector) Close() {
|
||||
// Unset flag
|
||||
m.init = false
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
## `nfsiostat` collector
|
||||
|
||||
```json
|
||||
"nfsiostat": {
|
||||
"exclude_metrics": [
|
||||
"nfsio_oread"
|
||||
],
|
||||
"exclude_filesystems" : [
|
||||
"/mnt",
|
||||
],
|
||||
"use_server_as_stype": false
|
||||
}
|
||||
```
|
||||
|
||||
The `nfsiostat` collector reads data from `/proc/self/mountstats` and outputs a handful **node** metrics for each NFS filesystem. If a metric or filesystem is not required, it can be excluded from forwarding it to the sink.
|
||||
|
||||
Metrics:
|
||||
* `nfsio_nread`: Bytes transferred by normal `read()` calls
|
||||
* `nfsio_nwrite`: Bytes transferred by normal `write()` calls
|
||||
* `nfsio_oread`: Bytes transferred by `read()` calls with `O_DIRECT`
|
||||
* `nfsio_owrite`: Bytes transferred by `write()` calls with `O_DIRECT`
|
||||
* `nfsio_pageread`: Pages transferred by `read()` calls
|
||||
* `nfsio_pagewrite`: Pages transferred by `write()` calls
|
||||
* `nfsio_nfsread`: Bytes transferred for reading from the server
|
||||
* `nfsio_nfswrite`: Pages transferred by writing to the server
|
||||
|
||||
The `nfsiostat` collector adds the mountpoint to the tags as `stype=filesystem,stype-id=<mountpoint>`. If the server address should be used instead of the mountpoint, use the `use_server_as_stype` config setting.
|
@@ -10,42 +10,34 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
// Non-Uniform Memory Access (NUMA) policy hit/miss statistics
|
||||
//
|
||||
// Numa policy hit/miss statistics
|
||||
//
|
||||
// numa_hit:
|
||||
//
|
||||
// A process wanted to allocate memory from this node, and succeeded.
|
||||
//
|
||||
// A process wanted to allocate memory from this node, and succeeded.
|
||||
// numa_miss:
|
||||
//
|
||||
// A process wanted to allocate memory from another node,
|
||||
// but ended up with memory from this node.
|
||||
//
|
||||
// A process wanted to allocate memory from another node,
|
||||
// but ended up with memory from this node.
|
||||
// numa_foreign:
|
||||
//
|
||||
// A process wanted to allocate on this node,
|
||||
// but ended up with memory from another node.
|
||||
//
|
||||
// A process wanted to allocate on this node,
|
||||
// but ended up with memory from another node.
|
||||
// local_node:
|
||||
//
|
||||
// A process ran on this node's CPU,
|
||||
// and got memory from this node.
|
||||
//
|
||||
// A process ran on this node's CPU,
|
||||
// and got memory from this node.
|
||||
// other_node:
|
||||
//
|
||||
// A process ran on a different node's CPU
|
||||
// and got memory from this node.
|
||||
//
|
||||
// A process ran on a different node's CPU
|
||||
// and got memory from this node.
|
||||
// interleave_hit:
|
||||
//
|
||||
// Interleaving wanted to allocate from this node
|
||||
// and succeeded.
|
||||
// Interleaving wanted to allocate from this node
|
||||
// and succeeded.
|
||||
//
|
||||
// See: https://www.kernel.org/doc/html/latest/admin-guide/numastat.html
|
||||
//
|
||||
type NUMAStatsCollectorTopolgy struct {
|
||||
file string
|
||||
tagSet map[string]string
|
||||
@@ -53,7 +45,8 @@ type NUMAStatsCollectorTopolgy struct {
|
||||
|
||||
type NUMAStatsCollector struct {
|
||||
metricCollector
|
||||
topology []NUMAStatsCollectorTopolgy
|
||||
topology []NUMAStatsCollectorTopolgy
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *NUMAStatsCollector) Init(config json.RawMessage) error {
|
||||
@@ -63,7 +56,6 @@ func (m *NUMAStatsCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
|
||||
m.name = "NUMAStatsCollector"
|
||||
m.parallel = true
|
||||
m.setup()
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
@@ -90,9 +82,7 @@ func (m *NUMAStatsCollector) Init(config json.RawMessage) error {
|
||||
tagSet: map[string]string{"memoryDomain": node},
|
||||
})
|
||||
}
|
||||
|
||||
// Initialized
|
||||
cclog.ComponentDebug(m.name, "initialized", len(m.topology), "NUMA domains")
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -139,11 +129,13 @@ func (m *NUMAStatsCollector) Read(interval time.Duration, output chan lp.CCMetri
|
||||
)
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
|
||||
file.Close()
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "collected_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *NUMAStatsCollector) Close() {
|
||||
|
@@ -1,17 +1,15 @@
|
||||
|
||||
## `numastat` collector
|
||||
|
||||
```json
|
||||
"numastats": {}
|
||||
"numastat": {}
|
||||
```
|
||||
|
||||
The `numastat` collector reads data from `/sys/devices/system/node/node*/numastat` and outputs a handful **memoryDomain** metrics. See: <https://www.kernel.org/doc/html/latest/admin-guide/numastat.html>
|
||||
The `numastat` collector reads data from `/sys/devices/system/node/node*/numastat` and outputs a handful **memoryDomain** metrics. See: https://www.kernel.org/doc/html/latest/admin-guide/numastat.html
|
||||
|
||||
Metrics:
|
||||
|
||||
* `numastats_numa_hit`: A process wanted to allocate memory from this node, and succeeded.
|
||||
* `numastats_numa_miss`: A process wanted to allocate memory from another node, but ended up with memory from this node.
|
||||
* `numastats_numa_foreign`: A process wanted to allocate on this node, but ended up with memory from another node.
|
||||
* `numastats_local_node`: A process ran on this node's CPU, and got memory from this node.
|
||||
* `numastats_other_node`: A process ran on a different node's CPU, and got memory from this node.
|
||||
* `numastats_interleave_hit`: Interleaving wanted to allocate from this node and succeeded.
|
||||
* `numastats_interleave_hit`: Interleaving wanted to allocate from this node and succeeded.
|
File diff suppressed because it is too large
Load Diff
@@ -3,74 +3,38 @@
|
||||
|
||||
```json
|
||||
"nvidia": {
|
||||
"exclude_devices": [
|
||||
"0","1", "0000000:ff:01.0"
|
||||
"exclude_devices" : [
|
||||
"0","1"
|
||||
],
|
||||
"exclude_metrics": [
|
||||
"nv_fb_mem_used",
|
||||
"nv_fb_memory",
|
||||
"nv_fan"
|
||||
],
|
||||
"process_mig_devices": false,
|
||||
"use_pci_info_as_type_id": true,
|
||||
"add_pci_info_tag": false,
|
||||
"add_uuid_meta": false,
|
||||
"add_board_number_meta": false,
|
||||
"add_serial_meta": false,
|
||||
"use_uuid_for_mig_device": false,
|
||||
"use_slice_for_mig_device": false
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `nvidia` collector can be configured to leave out specific devices with the `exclude_devices` option. It takes IDs as supplied to the NVML with `nvmlDeviceGetHandleByIndex()` or the PCI address in NVML format (`%08X:%02X:%02X.0`). Metrics (listed below) that should not be sent to the MetricRouter can be excluded with the `exclude_metrics` option. Commonly only the physical GPUs are monitored. If MIG devices should be analyzed as well, set `process_mig_devices` (adds `stype=mig,stype-id=<mig_index>`). With the options `use_uuid_for_mig_device` and `use_slice_for_mig_device`, the `<mig_index>` can be replaced with the UUID (e.g. `MIG-6a9f7cc8-6d5b-5ce0-92de-750edc4d8849`) or the MIG slice name (e.g. `1g.5gb`).
|
||||
|
||||
The metrics sent by the `nvidia` collector use `accelerator` as `type` tag. For the `type-id`, it uses the device handle index by default. With the `use_pci_info_as_type_id` option, the PCI ID is used instead. If both values should be added as tags, activate the `add_pci_info_tag` option. It uses the device handle index as `type-id` and adds the PCI ID as separate `pci_identifier` tag.
|
||||
|
||||
Optionally, it is possible to add the UUID, the board part number and the serial to the meta informations. They are not sent to the sinks (if not configured otherwise).
|
||||
|
||||
|
||||
Metrics:
|
||||
* `nv_util`
|
||||
* `nv_mem_util`
|
||||
* `nv_fb_mem_total`
|
||||
* `nv_fb_mem_used`
|
||||
* `nv_bar1_mem_total`
|
||||
* `nv_bar1_mem_used`
|
||||
* `nv_mem_total`
|
||||
* `nv_fb_memory`
|
||||
* `nv_temp`
|
||||
* `nv_fan`
|
||||
* `nv_ecc_mode`
|
||||
* `nv_perf_state`
|
||||
* `nv_power_usage`
|
||||
* `nv_graphics_clock`
|
||||
* `nv_sm_clock`
|
||||
* `nv_mem_clock`
|
||||
* `nv_video_clock`
|
||||
* `nv_power_usage_report`
|
||||
* `nv_graphics_clock_report`
|
||||
* `nv_sm_clock_report`
|
||||
* `nv_mem_clock_report`
|
||||
* `nv_max_graphics_clock`
|
||||
* `nv_max_sm_clock`
|
||||
* `nv_max_mem_clock`
|
||||
* `nv_max_video_clock`
|
||||
* `nv_ecc_uncorrected_error`
|
||||
* `nv_ecc_corrected_error`
|
||||
* `nv_power_max_limit`
|
||||
* `nv_ecc_db_error`
|
||||
* `nv_ecc_sb_error`
|
||||
* `nv_power_man_limit`
|
||||
* `nv_encoder_util`
|
||||
* `nv_decoder_util`
|
||||
* `nv_remapped_rows_corrected`
|
||||
* `nv_remapped_rows_uncorrected`
|
||||
* `nv_remapped_rows_pending`
|
||||
* `nv_remapped_rows_failure`
|
||||
* `nv_compute_processes`
|
||||
* `nv_graphics_processes`
|
||||
* `nv_violation_power`
|
||||
* `nv_violation_thermal`
|
||||
* `nv_violation_sync_boost`
|
||||
* `nv_violation_board_limit`
|
||||
* `nv_violation_low_util`
|
||||
* `nv_violation_reliability`
|
||||
* `nv_violation_below_app_clock`
|
||||
* `nv_violation_below_base_clock`
|
||||
* `nv_nvlink_crc_flit_errors`
|
||||
* `nv_nvlink_crc_errors`
|
||||
* `nv_nvlink_ecc_errors`
|
||||
* `nv_nvlink_replay_errors`
|
||||
* `nv_nvlink_recovery_errors`
|
||||
|
||||
Some metrics add the additional sub type tag (`stype`) like the `nv_nvlink_*` metrics set `stype=nvlink,stype-id=<link_number>`.
|
||||
It uses a separate `type` in the metrics. The output metric looks like this:
|
||||
`<name>,type=accelerator,type-id=<nvidia-gpu-id> value=<metric value> <timestamp>`
|
||||
|
||||
|
@@ -1,262 +0,0 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
)
|
||||
|
||||
// running average power limit (RAPL) monitoring attributes for a zone
|
||||
type RAPLZoneInfo struct {
|
||||
// tags describing the RAPL zone:
|
||||
// * zone_name, subzone_name: e.g. psys, dram, core, uncore, package-0
|
||||
// * zone_id: e.g. 0:1 (zone 0 sub zone 1)
|
||||
tags map[string]string
|
||||
energyFilepath string // path to a file containing the zones current energy counter in micro joules
|
||||
energy int64 // current reading of the energy counter in micro joules
|
||||
energyTimestamp time.Time // timestamp when energy counter was read
|
||||
maxEnergyRange int64 // Range of the above energy counter in micro-joules
|
||||
}
|
||||
|
||||
type RAPLCollector struct {
|
||||
metricCollector
|
||||
config struct {
|
||||
// Exclude IDs for RAPL zones, e.g.
|
||||
// * 0 for zone 0
|
||||
// * 0:1 for zone 0 subzone 1
|
||||
ExcludeByID []string `json:"exclude_device_by_id,omitempty"`
|
||||
// Exclude names for RAPL zones, e.g. psys, dram, core, uncore, package-0
|
||||
ExcludeByName []string `json:"exclude_device_by_name,omitempty"`
|
||||
}
|
||||
RAPLZoneInfo []RAPLZoneInfo
|
||||
meta map[string]string // default meta information
|
||||
}
|
||||
|
||||
// Init initializes the running average power limit (RAPL) collector
|
||||
func (m *RAPLCollector) Init(config json.RawMessage) error {
|
||||
|
||||
// Check if already initialized
|
||||
if m.init {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error = nil
|
||||
m.name = "RAPLCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
"group": "energy",
|
||||
"unit": "Watt",
|
||||
}
|
||||
|
||||
// Read in the JSON configuration
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Configure excluded RAPL zones
|
||||
isIDExcluded := make(map[string]bool)
|
||||
if m.config.ExcludeByID != nil {
|
||||
for _, ID := range m.config.ExcludeByID {
|
||||
isIDExcluded[ID] = true
|
||||
}
|
||||
}
|
||||
isNameExcluded := make(map[string]bool)
|
||||
if m.config.ExcludeByName != nil {
|
||||
for _, name := range m.config.ExcludeByName {
|
||||
isNameExcluded[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// readZoneInfo reads RAPL monitoring attributes for a zone given by zonePath
|
||||
// See: https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes
|
||||
readZoneInfo := func(zonePath string) (z struct {
|
||||
name string // zones name e.g. psys, dram, core, uncore, package-0
|
||||
energyFilepath string // path to a file containing the zones current energy counter in micro joules
|
||||
energy int64 // current reading of the energy counter in micro joules
|
||||
energyTimestamp time.Time // timestamp when energy counter was read
|
||||
maxEnergyRange int64 // Range of the above energy counter in micro-joules
|
||||
ok bool // Are all information available?
|
||||
}) {
|
||||
// zones name e.g. psys, dram, core, uncore, package-0
|
||||
foundName := false
|
||||
if v, err :=
|
||||
os.ReadFile(
|
||||
filepath.Join(zonePath, "name")); err == nil {
|
||||
foundName = true
|
||||
z.name = strings.TrimSpace(string(v))
|
||||
}
|
||||
|
||||
// path to a file containing the zones current energy counter in micro joules
|
||||
z.energyFilepath = filepath.Join(zonePath, "energy_uj")
|
||||
|
||||
// current reading of the energy counter in micro joules
|
||||
foundEnergy := false
|
||||
if v, err := os.ReadFile(z.energyFilepath); err == nil {
|
||||
// timestamp when energy counter was read
|
||||
z.energyTimestamp = time.Now()
|
||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||
foundEnergy = true
|
||||
z.energy = i
|
||||
}
|
||||
}
|
||||
|
||||
// Range of the above energy counter in micro-joules
|
||||
foundMaxEnergyRange := false
|
||||
if v, err :=
|
||||
os.ReadFile(
|
||||
filepath.Join(zonePath, "max_energy_range_uj")); err == nil {
|
||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||
foundMaxEnergyRange = true
|
||||
z.maxEnergyRange = i
|
||||
}
|
||||
}
|
||||
|
||||
// Are all information available?
|
||||
z.ok = foundName && foundEnergy && foundMaxEnergyRange
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
powerCapPrefix := "/sys/devices/virtual/powercap"
|
||||
controlType := "intel-rapl"
|
||||
controlTypePath := filepath.Join(powerCapPrefix, controlType)
|
||||
|
||||
// Find all RAPL zones
|
||||
zonePrefix := filepath.Join(controlTypePath, controlType+":")
|
||||
zonesPath, err := filepath.Glob(zonePrefix + "*")
|
||||
if err != nil || zonesPath == nil {
|
||||
return fmt.Errorf("unable to find any zones under %s", controlTypePath)
|
||||
}
|
||||
|
||||
for _, zonePath := range zonesPath {
|
||||
zoneID := strings.TrimPrefix(zonePath, zonePrefix)
|
||||
z := readZoneInfo(zonePath)
|
||||
if z.ok &&
|
||||
!isIDExcluded[zoneID] &&
|
||||
!isNameExcluded[z.name] {
|
||||
|
||||
// Add RAPL monitoring attributes for a zone
|
||||
m.RAPLZoneInfo =
|
||||
append(
|
||||
m.RAPLZoneInfo,
|
||||
RAPLZoneInfo{
|
||||
tags: map[string]string{
|
||||
"id": zoneID,
|
||||
"zone_name": z.name,
|
||||
},
|
||||
energyFilepath: z.energyFilepath,
|
||||
energy: z.energy,
|
||||
energyTimestamp: z.energyTimestamp,
|
||||
maxEnergyRange: z.maxEnergyRange,
|
||||
})
|
||||
}
|
||||
|
||||
// find all sub zones for the given zone
|
||||
subZonePrefix := filepath.Join(zonePath, controlType+":"+zoneID+":")
|
||||
subZonesPath, err := filepath.Glob(subZonePrefix + "*")
|
||||
if err != nil || subZonesPath == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, subZonePath := range subZonesPath {
|
||||
subZoneID := strings.TrimPrefix(subZonePath, subZonePrefix)
|
||||
sz := readZoneInfo(subZonePath)
|
||||
if len(zoneID) > 0 && len(z.name) > 0 &&
|
||||
sz.ok &&
|
||||
!isIDExcluded[zoneID+":"+subZoneID] &&
|
||||
!isNameExcluded[sz.name] {
|
||||
m.RAPLZoneInfo =
|
||||
append(
|
||||
m.RAPLZoneInfo,
|
||||
RAPLZoneInfo{
|
||||
tags: map[string]string{
|
||||
"id": zoneID + ":" + subZoneID,
|
||||
"zone_name": z.name,
|
||||
"sub_zone_name": sz.name,
|
||||
},
|
||||
energyFilepath: sz.energyFilepath,
|
||||
energy: sz.energy,
|
||||
energyTimestamp: sz.energyTimestamp,
|
||||
maxEnergyRange: sz.maxEnergyRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.RAPLZoneInfo == nil {
|
||||
return fmt.Errorf("no running average power limit (RAPL) device found in %s", controlTypePath)
|
||||
|
||||
}
|
||||
|
||||
// Initialized
|
||||
cclog.ComponentDebug(
|
||||
m.name,
|
||||
"initialized",
|
||||
len(m.RAPLZoneInfo),
|
||||
"zones with running average power limit (RAPL) monitoring attributes")
|
||||
m.init = true
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Read reads running average power limit (RAPL) monitoring attributes for all initialized zones
|
||||
// See: https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes
|
||||
func (m *RAPLCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
|
||||
for i := range m.RAPLZoneInfo {
|
||||
p := &m.RAPLZoneInfo[i]
|
||||
|
||||
// Read current value of the energy counter in micro joules
|
||||
if v, err := os.ReadFile(p.energyFilepath); err == nil {
|
||||
energyTimestamp := time.Now()
|
||||
if i, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64); err == nil {
|
||||
energy := i
|
||||
|
||||
// Compute average power (Δ energy / Δ time)
|
||||
energyDiff := energy - p.energy
|
||||
if energyDiff < 0 {
|
||||
// Handle overflow:
|
||||
// ( p.maxEnergyRange - p.energy ) + energy
|
||||
// = p.maxEnergyRange + ( energy - p.energy )
|
||||
// = p.maxEnergyRange + diffEnergy
|
||||
energyDiff += p.maxEnergyRange
|
||||
}
|
||||
timeDiff := energyTimestamp.Sub(p.energyTimestamp)
|
||||
averagePower := float64(energyDiff) / float64(timeDiff.Microseconds())
|
||||
|
||||
y, err := lp.New(
|
||||
"rapl_average_power",
|
||||
p.tags,
|
||||
m.meta,
|
||||
map[string]interface{}{"value": averagePower},
|
||||
energyTimestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
|
||||
// Save current energy counter state
|
||||
p.energy = energy
|
||||
p.energyTimestamp = energyTimestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes running average power limit (RAPL) metric collector
|
||||
func (m *RAPLCollector) Close() {
|
||||
// Unset flag
|
||||
m.init = false
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
# Running average power limit (RAPL) metric collector
|
||||
|
||||
This collector reads running average power limit (RAPL) monitoring attributes to compute average power consumption metrics. See <https://www.kernel.org/doc/html/latest/power/powercap/powercap.html#monitoring-attributes>.
|
||||
|
||||
The Likwid metric collector provides similar functionality.
|
||||
|
||||
## Configuration
|
||||
|
||||
```json
|
||||
"rapl": {
|
||||
"exclude_device_by_id": ["0:1", "0:2"],
|
||||
"exclude_device_by_name": ["psys"]
|
||||
}
|
||||
```
|
||||
|
||||
## Metrics
|
||||
|
||||
* `rapl_average_power`: average power consumption in Watt. The average is computed over the entire runtime from the last measurement to the current measurement
|
@@ -1,319 +0,0 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
"github.com/ClusterCockpit/go-rocm-smi/pkg/rocm_smi"
|
||||
)
|
||||
|
||||
type RocmSmiCollectorConfig struct {
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
ExcludeDevices []string `json:"exclude_devices,omitempty"`
|
||||
AddPciInfoTag bool `json:"add_pci_info_tag,omitempty"`
|
||||
UsePciInfoAsTypeId bool `json:"use_pci_info_as_type_id,omitempty"`
|
||||
AddSerialMeta bool `json:"add_serial_meta,omitempty"`
|
||||
}
|
||||
|
||||
type RocmSmiCollectorDevice struct {
|
||||
device rocm_smi.DeviceHandle
|
||||
index int
|
||||
tags map[string]string // default tags
|
||||
meta map[string]string // default meta information
|
||||
excludeMetrics map[string]bool // copy of exclude metrics from config
|
||||
}
|
||||
|
||||
type RocmSmiCollector struct {
|
||||
metricCollector
|
||||
config RocmSmiCollectorConfig // the configuration structure
|
||||
devices []RocmSmiCollectorDevice
|
||||
}
|
||||
|
||||
// Functions to implement MetricCollector interface
|
||||
// Init(...), Read(...), Close()
|
||||
// See: metricCollector.go
|
||||
|
||||
// Init initializes the sample collector
|
||||
// Called once by the collector manager
|
||||
// All tags, meta data tags and metrics that do not change over the runtime should be set here
|
||||
func (m *RocmSmiCollector) Init(config json.RawMessage) error {
|
||||
var err error = nil
|
||||
// Always set the name early in Init() to use it in cclog.Component* functions
|
||||
m.name = "RocmSmiCollector"
|
||||
// This is for later use, also call it early
|
||||
m.setup()
|
||||
// Define meta information sent with each metric
|
||||
// (Can also be dynamic or this is the basic set with extension through AddMeta())
|
||||
//m.meta = map[string]string{"source": m.name, "group": "AMD"}
|
||||
// Define tags sent with each metric
|
||||
// The 'type' tag is always needed, it defines the granulatity of the metric
|
||||
// node -> whole system
|
||||
// socket -> CPU socket (requires socket ID as 'type-id' tag)
|
||||
// cpu -> single CPU hardware thread (requires cpu ID as 'type-id' tag)
|
||||
//m.tags = map[string]string{"type": "node"}
|
||||
// Read in the JSON configuration
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ret := rocm_smi.Init()
|
||||
if ret != rocm_smi.STATUS_SUCCESS {
|
||||
err = errors.New("failed to initialize ROCm SMI library")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
numDevs, ret := rocm_smi.NumMonitorDevices()
|
||||
if ret != rocm_smi.STATUS_SUCCESS {
|
||||
err = errors.New("failed to get number of GPUs from ROCm SMI library")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
exclDev := func(s string) bool {
|
||||
skip_device := false
|
||||
for _, excl := range m.config.ExcludeDevices {
|
||||
if excl == s {
|
||||
skip_device = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return skip_device
|
||||
}
|
||||
|
||||
m.devices = make([]RocmSmiCollectorDevice, 0)
|
||||
|
||||
for i := 0; i < numDevs; i++ {
|
||||
str_i := fmt.Sprintf("%d", i)
|
||||
if exclDev(str_i) {
|
||||
continue
|
||||
}
|
||||
device, ret := rocm_smi.DeviceGetHandleByIndex(i)
|
||||
if ret != rocm_smi.STATUS_SUCCESS {
|
||||
err = fmt.Errorf("failed to get handle for GPU %d", i)
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
pciInfo, ret := rocm_smi.DeviceGetPciInfo(device)
|
||||
if ret != rocm_smi.STATUS_SUCCESS {
|
||||
err = fmt.Errorf("failed to get PCI information for GPU %d", i)
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
pciId := fmt.Sprintf(
|
||||
"%08X:%02X:%02X.%X",
|
||||
pciInfo.Domain,
|
||||
pciInfo.Bus,
|
||||
pciInfo.Device,
|
||||
pciInfo.Function)
|
||||
|
||||
if exclDev(pciId) {
|
||||
continue
|
||||
}
|
||||
|
||||
dev := RocmSmiCollectorDevice{
|
||||
device: device,
|
||||
tags: map[string]string{
|
||||
"type": "accelerator",
|
||||
"type-id": str_i,
|
||||
},
|
||||
meta: map[string]string{
|
||||
"source": m.name,
|
||||
"group": "AMD",
|
||||
},
|
||||
}
|
||||
if m.config.UsePciInfoAsTypeId {
|
||||
dev.tags["type-id"] = pciId
|
||||
} else if m.config.AddPciInfoTag {
|
||||
dev.tags["pci_identifier"] = pciId
|
||||
}
|
||||
|
||||
if m.config.AddSerialMeta {
|
||||
serial, ret := rocm_smi.DeviceGetSerialNumber(device)
|
||||
if ret != rocm_smi.STATUS_SUCCESS {
|
||||
cclog.ComponentError(m.name, "Unable to get serial number for device at index", i, ":", rocm_smi.StatusStringNoError(ret))
|
||||
} else {
|
||||
dev.meta["serial"] = serial
|
||||
}
|
||||
}
|
||||
// Add excluded metrics
|
||||
dev.excludeMetrics = map[string]bool{}
|
||||
for _, e := range m.config.ExcludeMetrics {
|
||||
dev.excludeMetrics[e] = true
|
||||
}
|
||||
dev.index = i
|
||||
m.devices = append(m.devices, dev)
|
||||
}
|
||||
|
||||
// Set this flag only if everything is initialized properly, all required files exist, ...
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
|
||||
// Read collects all metrics belonging to the sample collector
|
||||
// and sends them through the output channel to the collector manager
|
||||
func (m *RocmSmiCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
// Create a sample metric
|
||||
timestamp := time.Now()
|
||||
|
||||
for _, dev := range m.devices {
|
||||
metrics, ret := rocm_smi.DeviceGetMetrics(dev.device)
|
||||
if ret != rocm_smi.STATUS_SUCCESS {
|
||||
cclog.ComponentError(m.name, "Unable to get metrics for device at index", dev.index, ":", rocm_smi.StatusStringNoError(ret))
|
||||
continue
|
||||
}
|
||||
|
||||
if !dev.excludeMetrics["rocm_gfx_util"] {
|
||||
value := metrics.Average_gfx_activity
|
||||
y, err := lp.New("rocm_gfx_util", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_umc_util"] {
|
||||
value := metrics.Average_umc_activity
|
||||
y, err := lp.New("rocm_umc_util", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_mm_util"] {
|
||||
value := metrics.Average_mm_activity
|
||||
y, err := lp.New("rocm_mm_util", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_avg_power"] {
|
||||
value := metrics.Average_socket_power
|
||||
y, err := lp.New("rocm_avg_power", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_temp_mem"] {
|
||||
value := metrics.Temperature_mem
|
||||
y, err := lp.New("rocm_temp_mem", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_temp_hotspot"] {
|
||||
value := metrics.Temperature_hotspot
|
||||
y, err := lp.New("rocm_temp_hotspot", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_temp_edge"] {
|
||||
value := metrics.Temperature_edge
|
||||
y, err := lp.New("rocm_temp_edge", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_temp_vrgfx"] {
|
||||
value := metrics.Temperature_vrgfx
|
||||
y, err := lp.New("rocm_temp_vrgfx", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_temp_vrsoc"] {
|
||||
value := metrics.Temperature_vrsoc
|
||||
y, err := lp.New("rocm_temp_vrsoc", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_temp_vrmem"] {
|
||||
value := metrics.Temperature_vrmem
|
||||
y, err := lp.New("rocm_temp_vrmem", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_gfx_clock"] {
|
||||
value := metrics.Average_gfxclk_frequency
|
||||
y, err := lp.New("rocm_gfx_clock", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_soc_clock"] {
|
||||
value := metrics.Average_socclk_frequency
|
||||
y, err := lp.New("rocm_soc_clock", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_u_clock"] {
|
||||
value := metrics.Average_uclk_frequency
|
||||
y, err := lp.New("rocm_u_clock", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_v0_clock"] {
|
||||
value := metrics.Average_vclk0_frequency
|
||||
y, err := lp.New("rocm_v0_clock", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_v1_clock"] {
|
||||
value := metrics.Average_vclk1_frequency
|
||||
y, err := lp.New("rocm_v1_clock", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_d0_clock"] {
|
||||
value := metrics.Average_dclk0_frequency
|
||||
y, err := lp.New("rocm_d0_clock", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_d1_clock"] {
|
||||
value := metrics.Average_dclk1_frequency
|
||||
y, err := lp.New("rocm_d1_clock", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if !dev.excludeMetrics["rocm_temp_hbm"] {
|
||||
for i := 0; i < rocm_smi.NUM_HBM_INSTANCES; i++ {
|
||||
value := metrics.Temperature_hbm[i]
|
||||
y, err := lp.New("rocm_temp_hbm", dev.tags, dev.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
y.AddTag("stype", "device")
|
||||
y.AddTag("stype-id", fmt.Sprintf("%d", i))
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Close metric collector: close network connection, close files, close libraries, ...
|
||||
// Called once by the collector manager
|
||||
func (m *RocmSmiCollector) Close() {
|
||||
// Unset flag
|
||||
ret := rocm_smi.Shutdown()
|
||||
if ret != rocm_smi.STATUS_SUCCESS {
|
||||
cclog.ComponentError(m.name, "Failed to shutdown ROCm SMI library")
|
||||
}
|
||||
m.init = false
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
|
||||
## `rocm_smi` collector
|
||||
|
||||
```json
|
||||
"rocm_smi": {
|
||||
"exclude_devices": [
|
||||
"0","1", "0000000:ff:01.0"
|
||||
],
|
||||
"exclude_metrics": [
|
||||
"rocm_mm_util",
|
||||
"rocm_temp_vrsoc"
|
||||
],
|
||||
"use_pci_info_as_type_id": true,
|
||||
"add_pci_info_tag": false,
|
||||
"add_serial_meta": false,
|
||||
}
|
||||
```
|
||||
|
||||
The `rocm_smi` collector can be configured to leave out specific devices with the `exclude_devices` option. It takes logical IDs in the list of available devices or the PCI address similar to NVML format (`%08X:%02X:%02X.0`). Metrics (listed below) that should not be sent to the MetricRouter can be excluded with the `exclude_metrics` option.
|
||||
|
||||
The metrics sent by the `rocm_smi` collector use `accelerator` as `type` tag. For the `type-id`, it uses the device handle index by default. With the `use_pci_info_as_type_id` option, the PCI ID is used instead. If both values should be added as tags, activate the `add_pci_info_tag` option. It uses the device handle index as `type-id` and adds the PCI ID as separate `pci_identifier` tag.
|
||||
|
||||
Optionally, it is possible to add the serial to the meta informations. They are not sent to the sinks (if not configured otherwise).
|
||||
|
||||
|
||||
Metrics:
|
||||
* `rocm_gfx_util`
|
||||
* `rocm_umc_util`
|
||||
* `rocm_mm_util`
|
||||
* `rocm_avg_power`
|
||||
* `rocm_temp_mem`
|
||||
* `rocm_temp_hotspot`
|
||||
* `rocm_temp_edge`
|
||||
* `rocm_temp_vrgfx`
|
||||
* `rocm_temp_vrsoc`
|
||||
* `rocm_temp_vrmem`
|
||||
* `rocm_gfx_clock`
|
||||
* `rocm_soc_clock`
|
||||
* `rocm_u_clock`
|
||||
* `rocm_v0_clock`
|
||||
* `rocm_v1_clock`
|
||||
* `rocm_d0_clock`
|
||||
* `rocm_d1_clock`
|
||||
* `rocm_temp_hbm`
|
||||
|
||||
|
||||
Some metrics add the additional sub type tag (`stype`) like the `rocm_temp_hbm` metrics set `stype=device,stype-id=<HBM_slice_number>`.
|
@@ -4,8 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
// These are the fields we read from the JSON configuration
|
||||
@@ -17,9 +18,10 @@ type SampleCollectorConfig struct {
|
||||
// defined by metricCollector (name, init, ...)
|
||||
type SampleCollector struct {
|
||||
metricCollector
|
||||
config SampleCollectorConfig // the configuration structure
|
||||
meta map[string]string // default meta information
|
||||
tags map[string]string // default tags
|
||||
config SampleTimerCollectorConfig // the configuration structure
|
||||
meta map[string]string // default meta information
|
||||
tags map[string]string // default tags
|
||||
statsCount int64
|
||||
}
|
||||
|
||||
// Functions to implement MetricCollector interface
|
||||
@@ -35,23 +37,14 @@ func (m *SampleCollector) Init(config json.RawMessage) error {
|
||||
m.name = "InternalCollector"
|
||||
// This is for later use, also call it early
|
||||
m.setup()
|
||||
// Tell whether the collector should be run in parallel with others (reading files, ...)
|
||||
// or it should be run serially, mostly for collectors actually doing measurements
|
||||
// because they should not measure the execution of the other collectors
|
||||
m.parallel = true
|
||||
// Define meta information sent with each metric
|
||||
// (Can also be dynamic or this is the basic set with extension through AddMeta())
|
||||
m.meta = map[string]string{"source": m.name, "group": "SAMPLE"}
|
||||
// Define tags sent with each metric
|
||||
// The 'type' tag is always needed, it defines the granularity of the metric
|
||||
// The 'type' tag is always needed, it defines the granulatity of the metric
|
||||
// node -> whole system
|
||||
// socket -> CPU socket (requires socket ID as 'type-id' tag)
|
||||
// die -> CPU die (requires CPU die ID as 'type-id' tag)
|
||||
// memoryDomain -> NUMA domain (requires NUMA domain ID as 'type-id' tag)
|
||||
// llc -> Last level cache (requires last level cache ID as 'type-id' tag)
|
||||
// core -> single CPU core that may consist of multiple hardware threads (SMT) (requires core ID as 'type-id' tag)
|
||||
// hwthtread -> single CPU hardware thread (requires hardware thread ID as 'type-id' tag)
|
||||
// accelerator -> A accelerator device like GPU or FPGA (requires an accelerator ID as 'type-id' tag)
|
||||
// cpu -> single CPU hardware thread (requires cpu ID as 'type-id' tag)
|
||||
m.tags = map[string]string{"type": "node"}
|
||||
// Read in the JSON configuration
|
||||
if len(config) > 0 {
|
||||
@@ -67,6 +60,9 @@ func (m *SampleCollector) Init(config json.RawMessage) error {
|
||||
// for all topological entities (sockets, NUMA domains, ...)
|
||||
// Return some useful error message in case of any failures
|
||||
|
||||
// Initialize counts for statistics
|
||||
m.statsCount = 0
|
||||
|
||||
// Set this flag only if everything is initialized properly, all required files exist, ...
|
||||
m.init = true
|
||||
return err
|
||||
@@ -89,8 +85,11 @@ func (m *SampleCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
if err == nil {
|
||||
// Send it to output channel
|
||||
output <- y
|
||||
// increment count for each sent metric or any other operation
|
||||
m.statsCount++
|
||||
}
|
||||
|
||||
// Set stats for the component
|
||||
stats.ComponentStatInt(m.name, "count", m.statsCount)
|
||||
}
|
||||
|
||||
// Close metric collector: close network connection, close files, close libraries, ...
|
||||
|
@@ -5,8 +5,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
// These are the fields we read from the JSON configuration
|
||||
@@ -38,7 +38,7 @@ func (m *SampleTimerCollector) Init(name string, config json.RawMessage) error {
|
||||
// (Can also be dynamic or this is the basic set with extension through AddMeta())
|
||||
m.meta = map[string]string{"source": m.name, "group": "SAMPLE"}
|
||||
// Define tags sent with each metric
|
||||
// The 'type' tag is always needed, it defines the granularity of the metric
|
||||
// The 'type' tag is always needed, it defines the granulatity of the metric
|
||||
// node -> whole system
|
||||
// socket -> CPU socket (requires socket ID as 'type-id' tag)
|
||||
// cpu -> single CPU hardware thread (requires cpu ID as 'type-id' tag)
|
||||
@@ -60,7 +60,7 @@ func (m *SampleTimerCollector) Init(name string, config json.RawMessage) error {
|
||||
|
||||
// Storage for output channel
|
||||
m.output = nil
|
||||
// Management channel for the timer function.
|
||||
// Mangement channel for the timer function.
|
||||
m.done = make(chan bool)
|
||||
// Create the own ticker
|
||||
m.ticker = time.NewTicker(m.interval)
|
||||
@@ -94,7 +94,7 @@ func (m *SampleTimerCollector) ReadMetrics(timestamp time.Time) {
|
||||
|
||||
value := 1.0
|
||||
|
||||
// If you want to measure something for a specific amount of time, use interval
|
||||
// If you want to measure something for a specific amout of time, use interval
|
||||
// start := readState()
|
||||
// time.Sleep(interval)
|
||||
// stop := readState()
|
||||
|
@@ -1,154 +0,0 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
)
|
||||
|
||||
const SCHEDSTATFILE = `/proc/schedstat`
|
||||
|
||||
// These are the fields we read from the JSON configuration
|
||||
type SchedstatCollectorConfig struct {
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
}
|
||||
|
||||
// This contains all variables we need during execution and the variables
|
||||
// defined by metricCollector (name, init, ...)
|
||||
type SchedstatCollector struct {
|
||||
metricCollector
|
||||
config SchedstatCollectorConfig // the configuration structure
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive values
|
||||
meta map[string]string // default meta information
|
||||
cputags map[string]map[string]string // default tags
|
||||
olddata map[string]map[string]int64 // default tags
|
||||
}
|
||||
|
||||
// Functions to implement MetricCollector interface
|
||||
// Init(...), Read(...), Close()
|
||||
// See: metricCollector.go
|
||||
|
||||
// Init initializes the sample collector
|
||||
// Called once by the collector manager
|
||||
// All tags, meta data tags and metrics that do not change over the runtime should be set here
|
||||
func (m *SchedstatCollector) Init(config json.RawMessage) error {
|
||||
var err error = nil
|
||||
// Always set the name early in Init() to use it in cclog.Component* functions
|
||||
m.name = "SchedstatCollector"
|
||||
// This is for later use, also call it early
|
||||
m.setup()
|
||||
// Tell whether the collector should be run in parallel with others (reading files, ...)
|
||||
// or it should be run serially, mostly for collectors acutally doing measurements
|
||||
// because they should not measure the execution of the other collectors
|
||||
m.parallel = true
|
||||
// Define meta information sent with each metric
|
||||
// (Can also be dynamic or this is the basic set with extension through AddMeta())
|
||||
m.meta = map[string]string{"source": m.name, "group": "SCHEDSTAT"}
|
||||
|
||||
// Read in the JSON configuration
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check input file
|
||||
file, err := os.Open(string(SCHEDSTATFILE))
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Pre-generate tags for all CPUs
|
||||
num_cpus := 0
|
||||
m.cputags = make(map[string]map[string]string)
|
||||
m.olddata = make(map[string]map[string]int64)
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
linefields := strings.Fields(line)
|
||||
if strings.HasPrefix(linefields[0], "cpu") && strings.Compare(linefields[0], "cpu") != 0 {
|
||||
cpustr := strings.TrimLeft(linefields[0], "cpu")
|
||||
cpu, _ := strconv.Atoi(cpustr)
|
||||
running, _ := strconv.ParseInt(linefields[7], 10, 64)
|
||||
waiting, _ := strconv.ParseInt(linefields[8], 10, 64)
|
||||
m.cputags[linefields[0]] = map[string]string{"type": "hwthread", "type-id": fmt.Sprintf("%d", cpu)}
|
||||
m.olddata[linefields[0]] = map[string]int64{"running": running, "waiting": waiting}
|
||||
num_cpus++
|
||||
}
|
||||
}
|
||||
|
||||
// Save current timestamp
|
||||
m.lastTimestamp = time.Now()
|
||||
|
||||
// Set this flag only if everything is initialized properly, all required files exist, ...
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *SchedstatCollector) ParseProcLine(linefields []string, tags map[string]string, output chan lp.CCMetric, now time.Time, tsdelta time.Duration) {
|
||||
running, _ := strconv.ParseInt(linefields[7], 10, 64)
|
||||
waiting, _ := strconv.ParseInt(linefields[8], 10, 64)
|
||||
diff_running := running - m.olddata[linefields[0]]["running"]
|
||||
diff_waiting := waiting - m.olddata[linefields[0]]["waiting"]
|
||||
|
||||
var l_running float64 = float64(diff_running) / tsdelta.Seconds() / (math.Pow(1000, 3))
|
||||
var l_waiting float64 = float64(diff_waiting) / tsdelta.Seconds() / (math.Pow(1000, 3))
|
||||
|
||||
m.olddata[linefields[0]]["running"] = running
|
||||
m.olddata[linefields[0]]["waiting"] = waiting
|
||||
value := l_running + l_waiting
|
||||
|
||||
y, err := lp.New("cpu_load_core", tags, m.meta, map[string]interface{}{"value": value}, now)
|
||||
if err == nil {
|
||||
// Send it to output channel
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
|
||||
// Read collects all metrics belonging to the sample collector
|
||||
// and sends them through the output channel to the collector manager
|
||||
func (m *SchedstatCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
|
||||
//timestamps
|
||||
now := time.Now()
|
||||
tsdelta := now.Sub(m.lastTimestamp)
|
||||
|
||||
file, err := os.Open(string(SCHEDSTATFILE))
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
linefields := strings.Fields(line)
|
||||
if strings.HasPrefix(linefields[0], "cpu") {
|
||||
m.ParseProcLine(linefields, m.cputags[linefields[0]], output, now, tsdelta)
|
||||
}
|
||||
}
|
||||
|
||||
m.lastTimestamp = now
|
||||
|
||||
}
|
||||
|
||||
// Close metric collector: close network connection, close files, close libraries, ...
|
||||
// Called once by the collector manager
|
||||
func (m *SchedstatCollector) Close() {
|
||||
// Unset flag
|
||||
m.init = false
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
|
||||
## `schedstat` collector
|
||||
```json
|
||||
"schedstat": {
|
||||
}
|
||||
```
|
||||
|
||||
The `schedstat` collector reads data from /proc/schedstat and calculates a load value, separated by hwthread. This might be useful to detect bad cpu pinning on shared nodes etc.
|
||||
|
||||
Metric:
|
||||
* `cpu_load_core`
|
@@ -1,144 +0,0 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
)
|
||||
|
||||
type SelfCollectorConfig struct {
|
||||
MemStats bool `json:"read_mem_stats"`
|
||||
GoRoutines bool `json:"read_goroutines"`
|
||||
CgoCalls bool `json:"read_cgo_calls"`
|
||||
Rusage bool `json:"read_rusage"`
|
||||
}
|
||||
|
||||
type SelfCollector struct {
|
||||
metricCollector
|
||||
config SelfCollectorConfig // the configuration structure
|
||||
meta map[string]string // default meta information
|
||||
tags map[string]string // default tags
|
||||
}
|
||||
|
||||
func (m *SelfCollector) Init(config json.RawMessage) error {
|
||||
var err error = nil
|
||||
m.name = "SelfCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{"source": m.name, "group": "Self"}
|
||||
m.tags = map[string]string{"type": "node"}
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *SelfCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
timestamp := time.Now()
|
||||
|
||||
if m.config.MemStats {
|
||||
var memstats runtime.MemStats
|
||||
runtime.ReadMemStats(&memstats)
|
||||
|
||||
y, err := lp.New("total_alloc", m.tags, m.meta, map[string]interface{}{"value": memstats.TotalAlloc}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "Bytes")
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("heap_alloc", m.tags, m.meta, map[string]interface{}{"value": memstats.HeapAlloc}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "Bytes")
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("heap_sys", m.tags, m.meta, map[string]interface{}{"value": memstats.HeapSys}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "Bytes")
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("heap_idle", m.tags, m.meta, map[string]interface{}{"value": memstats.HeapIdle}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "Bytes")
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("heap_inuse", m.tags, m.meta, map[string]interface{}{"value": memstats.HeapInuse}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "Bytes")
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("heap_released", m.tags, m.meta, map[string]interface{}{"value": memstats.HeapReleased}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "Bytes")
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("heap_objects", m.tags, m.meta, map[string]interface{}{"value": memstats.HeapObjects}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if m.config.GoRoutines {
|
||||
y, err := lp.New("num_goroutines", m.tags, m.meta, map[string]interface{}{"value": runtime.NumGoroutine()}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if m.config.CgoCalls {
|
||||
y, err := lp.New("num_cgo_calls", m.tags, m.meta, map[string]interface{}{"value": runtime.NumCgoCall()}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if m.config.Rusage {
|
||||
var rusage syscall.Rusage
|
||||
err := syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
||||
if err == nil {
|
||||
sec, nsec := rusage.Utime.Unix()
|
||||
t := float64(sec) + (float64(nsec) * 1e-9)
|
||||
y, err := lp.New("rusage_user_time", m.tags, m.meta, map[string]interface{}{"value": t}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "seconds")
|
||||
output <- y
|
||||
}
|
||||
sec, nsec = rusage.Stime.Unix()
|
||||
t = float64(sec) + (float64(nsec) * 1e-9)
|
||||
y, err = lp.New("rusage_system_time", m.tags, m.meta, map[string]interface{}{"value": t}, timestamp)
|
||||
if err == nil {
|
||||
y.AddMeta("unit", "seconds")
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("rusage_vol_ctx_switch", m.tags, m.meta, map[string]interface{}{"value": rusage.Nvcsw}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("rusage_invol_ctx_switch", m.tags, m.meta, map[string]interface{}{"value": rusage.Nivcsw}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("rusage_signals", m.tags, m.meta, map[string]interface{}{"value": rusage.Nsignals}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("rusage_major_pgfaults", m.tags, m.meta, map[string]interface{}{"value": rusage.Majflt}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
y, err = lp.New("rusage_minor_pgfaults", m.tags, m.meta, map[string]interface{}{"value": rusage.Minflt}, timestamp)
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SelfCollector) Close() {
|
||||
m.init = false
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
## `self` collector
|
||||
|
||||
```json
|
||||
"self": {
|
||||
"read_mem_stats" : true,
|
||||
"read_goroutines" : true,
|
||||
"read_cgo_calls" : true,
|
||||
"read_rusage" : true
|
||||
}
|
||||
```
|
||||
|
||||
The `self` collector reads the data from the `runtime` and `syscall` packages, so monitors the execution of the cc-metric-collector itself.
|
||||
|
||||
Metrics:
|
||||
* If `read_mem_stats == true`:
|
||||
* `total_alloc`: The metric reports cumulative bytes allocated for heap objects.
|
||||
* `heap_alloc`: The metric reports bytes of allocated heap objects.
|
||||
* `heap_sys`: The metric reports bytes of heap memory obtained from the OS.
|
||||
* `heap_idle`: The metric reports bytes in idle (unused) spans.
|
||||
* `heap_inuse`: The metric reports bytes in in-use spans.
|
||||
* `heap_released`: The metric reports bytes of physical memory returned to the OS.
|
||||
* `heap_objects`: The metric reports the number of allocated heap objects.
|
||||
* If `read_goroutines == true`:
|
||||
* `num_goroutines`: The metric reports the number of goroutines that currently exist.
|
||||
* If `read_cgo_calls == true`:
|
||||
* `num_cgo_calls`: The metric reports the number of cgo calls made by the current process.
|
||||
* If `read_rusage == true`:
|
||||
* `rusage_user_time`: The metric reports the amount of time that this process has been scheduled in user mode.
|
||||
* `rusage_system_time`: The metric reports the amount of time that this process has been scheduled in kernel mode.
|
||||
* `rusage_vol_ctx_switch`: The metric reports the amount of voluntary context switches.
|
||||
* `rusage_invol_ctx_switch`: The metric reports the amount of involuntary context switches.
|
||||
* `rusage_signals`: The metric reports the number of signals received.
|
||||
* `rusage_major_pgfaults`: The metric reports the number of major faults the process has made which have required loading a memory page from disk.
|
||||
* `rusage_minor_pgfaults`: The metric reports the number of minor faults the process has made which have not required loading a memory page from disk.
|
@@ -3,14 +3,15 @@ package collectors
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
// See: https://www.kernel.org/doc/html/latest/hwmon/sysfs-interface.html
|
||||
@@ -40,7 +41,8 @@ type TempCollector struct {
|
||||
ReportMaxTemp bool `json:"report_max_temperature"`
|
||||
ReportCriticalTemp bool `json:"report_critical_temperature"`
|
||||
}
|
||||
sensors []*TempCollectorSensor
|
||||
sensors []*TempCollectorSensor
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
@@ -50,7 +52,6 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
|
||||
m.name = "TempCollector"
|
||||
m.parallel = true
|
||||
m.setup()
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
@@ -83,14 +84,14 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
|
||||
// sensor name
|
||||
nameFile := filepath.Join(filepath.Dir(file), "name")
|
||||
name, err := os.ReadFile(nameFile)
|
||||
name, err := ioutil.ReadFile(nameFile)
|
||||
if err == nil {
|
||||
sensor.name = strings.TrimSpace(string(name))
|
||||
}
|
||||
|
||||
// sensor label
|
||||
labelFile := strings.TrimSuffix(file, "_input") + "_label"
|
||||
label, err := os.ReadFile(labelFile)
|
||||
label, err := ioutil.ReadFile(labelFile)
|
||||
if err == nil {
|
||||
sensor.label = strings.TrimSpace(string(label))
|
||||
}
|
||||
@@ -117,10 +118,6 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
|
||||
// Sensor file
|
||||
_, err = os.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sensor.file = file
|
||||
|
||||
// Sensor tags
|
||||
@@ -139,7 +136,7 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
// max temperature
|
||||
if m.config.ReportMaxTemp {
|
||||
maxTempFile := strings.TrimSuffix(file, "_input") + "_max"
|
||||
if buffer, err := os.ReadFile(maxTempFile); err == nil {
|
||||
if buffer, err := ioutil.ReadFile(maxTempFile); err == nil {
|
||||
if x, err := strconv.ParseInt(strings.TrimSpace(string(buffer)), 10, 64); err == nil {
|
||||
sensor.maxTempName = strings.Replace(sensor.metricName, "temp", "max_temp", 1)
|
||||
sensor.maxTemp = x / 1000
|
||||
@@ -150,7 +147,7 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
// critical temperature
|
||||
if m.config.ReportCriticalTemp {
|
||||
criticalTempFile := strings.TrimSuffix(file, "_input") + "_crit"
|
||||
if buffer, err := os.ReadFile(criticalTempFile); err == nil {
|
||||
if buffer, err := ioutil.ReadFile(criticalTempFile); err == nil {
|
||||
if x, err := strconv.ParseInt(strings.TrimSpace(string(buffer)), 10, 64); err == nil {
|
||||
sensor.critTempName = strings.Replace(sensor.metricName, "temp", "crit_temp", 1)
|
||||
sensor.critTemp = x / 1000
|
||||
@@ -167,6 +164,7 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
|
||||
// Finished initialization
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -175,7 +173,7 @@ func (m *TempCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
|
||||
for _, sensor := range m.sensors {
|
||||
// Read sensor file
|
||||
buffer, err := os.ReadFile(sensor.file)
|
||||
buffer, err := ioutil.ReadFile(sensor.file)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
@@ -199,6 +197,7 @@ func (m *TempCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
)
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
|
||||
// max temperature
|
||||
@@ -212,6 +211,7 @@ func (m *TempCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
)
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +226,11 @@ func (m *TempCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
)
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *TempCollector) Close() {
|
||||
|
@@ -9,7 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
stats "github.com/ClusterCockpit/cc-metric-collector/internal/metricRouter"
|
||||
)
|
||||
|
||||
const MAX_NUM_PROCS = 10
|
||||
@@ -21,14 +22,14 @@ type TopProcsCollectorConfig struct {
|
||||
|
||||
type TopProcsCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
config TopProcsCollectorConfig
|
||||
tags map[string]string
|
||||
config TopProcsCollectorConfig
|
||||
statsProcessedMetrics int64
|
||||
}
|
||||
|
||||
func (m *TopProcsCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "TopProcsCollector"
|
||||
m.parallel = true
|
||||
m.tags = map[string]string{"type": "node"}
|
||||
m.meta = map[string]string{"source": m.name, "group": "TopProcs"}
|
||||
if len(config) > 0 {
|
||||
@@ -49,6 +50,7 @@ func (m *TopProcsCollector) Init(config json.RawMessage) error {
|
||||
if err != nil {
|
||||
return errors.New("failed to execute command")
|
||||
}
|
||||
m.statsProcessedMetrics = 0
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -71,8 +73,10 @@ func (m *TopProcsCollector) Read(interval time.Duration, output chan lp.CCMetric
|
||||
y, err := lp.New(name, m.tags, m.meta, map[string]interface{}{"value": string(lines[i])}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
m.statsProcessedMetrics++
|
||||
}
|
||||
}
|
||||
stats.ComponentStatInt(m.name, "processed_metrics", m.statsProcessedMetrics)
|
||||
}
|
||||
|
||||
func (m *TopProcsCollector) Close() {
|
||||
|
12
config.json
12
config.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"sinks": "./sinks.json",
|
||||
"collectors" : "./collectors.json",
|
||||
"receivers" : "./receivers.json",
|
||||
"router" : "./router.json",
|
||||
"interval": "10s",
|
||||
"duration": "1s"
|
||||
"sinks": "sinks.json",
|
||||
"collectors" : "collectors.json",
|
||||
"receivers" : "receivers.json",
|
||||
"router" : "router.json",
|
||||
"interval": 10,
|
||||
"duration": 1
|
||||
}
|
||||
|
@@ -1,74 +0,0 @@
|
||||
# Building the cc-metric-collector
|
||||
|
||||
In most cases, a simple `make` in the main folder is enough to get a `cc-metric-collector` binary. It is basically a `go build` but some collectors require additional tasks. There is currently no Golang interface to LIKWID, so it uses `cgo` to create bindings but `cgo` requires the LIKWID header files. Therefore, it checks whether LIKWID is installed and if not it downloads LIKWID and copies the headers.
|
||||
|
||||
## System integration
|
||||
|
||||
The main configuration settings for system integration are pre-defined in `scripts/cc-metric-collector.config`. The file contains the UNIX user and group used for execution, the PID file location and other settings. Adjust it accordingly and copy it to `/etc/default/cc-metric-collector`
|
||||
|
||||
```bash
|
||||
$ install --mode 644 \
|
||||
--owner $CC_USER \
|
||||
--group $CC_GROUP \
|
||||
scripts/cc-metric-collector.config /etc/default/cc-metric-collector
|
||||
$ edit /etc/default/cc-metric-collector
|
||||
```
|
||||
|
||||
### SysVinit and similar
|
||||
|
||||
If you are using a init system based in `/etc/init.d` daemons, you can use the sample `scripts/cc-metric-collector.init`. It reads the basic configuration from `/etc/default/cc-metric-collector`
|
||||
|
||||
```bash
|
||||
$ install --mode 755 \
|
||||
--owner $CC_USER \
|
||||
--group $CC_GROUP \
|
||||
scripts/cc-metric-collector.init /etc/init.d/cc-metric-collector
|
||||
```
|
||||
|
||||
### Systemd
|
||||
|
||||
If you are using `systemd` as init system, you can use the sample systemd service file `scripts/cc-metric-collector.service`, the configuration file `scripts/cc-metric-collector.config`.
|
||||
|
||||
```bash
|
||||
$ install --mode 644 \
|
||||
--owner $CC_USER \
|
||||
--group $CC_GROUP \
|
||||
scripts/cc-metric-collector.service /etc/systemd/system/cc-metric-collector.service
|
||||
$ systemctl enable cc-metric-collector
|
||||
```
|
||||
|
||||
## Packaging
|
||||
|
||||
### RPM
|
||||
|
||||
In order to get a RPM packages for cc-metric-collector, just use:
|
||||
|
||||
```bash
|
||||
$ make RPM
|
||||
```
|
||||
|
||||
It uses the RPM SPEC file `scripts/cc-metric-collector.spec` and requires the RPM tools (`rpm` and `rpmspec`) and `git`.
|
||||
|
||||
### DEB
|
||||
|
||||
In order to get very simple Debian packages for cc-metric-collector, just use:
|
||||
|
||||
```bash
|
||||
$ make DEB
|
||||
```
|
||||
|
||||
It uses the DEB control file `scripts/cc-metric-collector.control` and requires `dpkg-deb`, `awk`, `sed` and `git`. It creates only a binary deb package.
|
||||
|
||||
_This option is not well tested and therefore experimental_
|
||||
|
||||
### Customizing RPMs or DEB packages
|
||||
|
||||
If you want to customize the RPMs or DEB packages for your local system, use the following workflow.
|
||||
|
||||
- (if there is already a fork in the private account, delete it and wait until Github realizes the deletion)
|
||||
- Fork the cc-metric-collector repository (if Github hasn't realized it, it creates a fork named cc-metric-collector2)
|
||||
- Go to private cc-metric-collector repository and enable Github Actions
|
||||
- Do changes to the scripts, code, ... Commit and push your changes.
|
||||
- Tag the new commit with `v0.x.y-<myversion>` (`git tag v0.x.y-<myversion>`)
|
||||
- Push tags to repository (`git push --tags`)
|
||||
- Wait until the Release action finishes. It creates fresh RPMs and DEBs in your private repository on the Releases page.
|
@@ -1,23 +0,0 @@
|
||||
# The ClusterCockpit Project
|
||||
|
||||
The ClusterCockpit project is a joined project of computing centers in Europe to set up a cluster monitoring stack for small to mid-sized computing centers under the lead of NHR@FAU.
|
||||
|
||||
# The ClusterCockpit Stack
|
||||
|
||||
In cluster environment, there are commonly a lot of systems dedicated for computation, backend servers for file systems and frontend servers for the user interaction and cluster control. The ClusterCockpit Stack is mainly used for monitoring the compute systems with some interaction to the frontend servers. It consists of multiple components:
|
||||
|
||||
- cc-metric-collector: Monitor resource usage on the compute systems
|
||||
- cc-metric-store: In-memory database
|
||||
- cc-backend & cc-frontend: The web-based visualizer
|
||||
|
||||
# CC Metric Collector
|
||||
|
||||
The CC Metric Collector project was started to provide a useful set of metrics for HPC and data science related compute systems. It runs as a system daemon and gathers system data periodically to forward the metrics to one or more databases. One of the provided backends can be used for the cc-metric-store but many others exist like InfluxDB time-series databases, the Ganglia Monitoring System or the Prometheus Monitoring System.
|
||||
|
||||
The data is gathered by so-called "Collectors", forwarded to an internal router for on-the-fly manipulation (tagging, aggregation, ...) which pushes the metrics to the different metric writers called "Sinks". There is a forth component, the "Receivers", which receive data through some networking system like a HTTP server at any time.
|
||||
|
||||
# CC Metric Store
|
||||
The CC Metric Store is a data management system with short-term in-memory and long-term file-base metric storage.
|
||||
|
||||
# CC Backend and CC Frontend
|
||||
The CC Backend and Frontend form together the web interface for ClusterCockpit.
|
107
go.mod
107
go.mod
@@ -1,106 +1,19 @@
|
||||
module github.com/ClusterCockpit/cc-metric-collector
|
||||
|
||||
go 1.20
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/ClusterCockpit/cc-units v0.4.0
|
||||
github.com/ClusterCockpit/go-rocm-smi v0.3.0
|
||||
github.com/NVIDIA/go-nvml v0.12.0-1
|
||||
github.com/PaesslerAG/gval v1.2.2
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gosnmp/gosnmp v1.37.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.12.3
|
||||
github.com/NVIDIA/go-nvml v0.11.1-0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.7.0
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf
|
||||
github.com/influxdata/line-protocol/v2 v2.2.1
|
||||
github.com/nats-io/nats.go v1.30.2
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/stmcginnis/gofish v0.14.0
|
||||
github.com/tklauser/go-sysconf v0.3.12
|
||||
golang.design/x/thread v0.0.0-20210122121316-335e9adffdf1
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
|
||||
golang.org/x/sys v0.13.0
|
||||
github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9
|
||||
gopkg.in/Knetic/govaluate.v2 v2.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
|
||||
github.com/Joker/jade v1.1.3 // indirect
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.15.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/flosch/pongo2/v4 v4.0.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.5 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/iris-contrib/schema v0.0.6 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kataras/blocks v0.0.8 // indirect
|
||||
github.com/kataras/golog v0.1.9 // indirect
|
||||
github.com/kataras/iris/v12 v12.2.7 // indirect
|
||||
github.com/kataras/pio v0.0.12 // indirect
|
||||
github.com/kataras/sitemap v0.0.6 // indirect
|
||||
github.com/kataras/tunnel v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/labstack/echo/v4 v4.11.1 // indirect
|
||||
github.com/labstack/gommon v0.4.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mailgun/raymond/v2 v2.0.48 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.8.4 // indirect
|
||||
github.com/nats-io/nkeys v0.4.5 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.12.9 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.6.8 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.0 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/yosssi/ace v0.0.5 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.16.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/PaesslerAG/gval v1.1.2
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.7.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
||||
|
398
go.sum
398
go.sum
@@ -1,331 +1,145 @@
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME=
|
||||
github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4=
|
||||
github.com/ClusterCockpit/cc-units v0.4.0 h1:zP5DOu99GmErW0tCDf0gcLrlWt42RQ9dpoONEOh4cI0=
|
||||
github.com/ClusterCockpit/cc-units v0.4.0/go.mod h1:3S3PAhAayS3pbgcT4q9Vn9VJw22Op51X0YimtG77zBw=
|
||||
github.com/ClusterCockpit/go-rocm-smi v0.3.0 h1:1qZnSpG7/NyLtc7AjqnUL9Jb8xtqG1nMVgp69rJfaR8=
|
||||
github.com/ClusterCockpit/go-rocm-smi v0.3.0/go.mod h1:+I3UMeX3OlizXDf1WpGD43W4KGZZGVSGmny6rTeOnWA=
|
||||
github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
|
||||
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
|
||||
github.com/NVIDIA/go-nvml v0.11.6-0/go.mod h1:hy7HYeQy335x6nEss0Ne3PYqleRa6Ct+VKD9RQ4nyFs=
|
||||
github.com/NVIDIA/go-nvml v0.12.0-1 h1:6mdjtlFo+17dWL7VFPfuRMtf0061TF4DKls9pkSw6uM=
|
||||
github.com/NVIDIA/go-nvml v0.12.0-1/go.mod h1:hy7HYeQy335x6nEss0Ne3PYqleRa6Ct+VKD9RQ4nyFs=
|
||||
github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E=
|
||||
github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
|
||||
github.com/NVIDIA/go-nvml v0.11.1-0 h1:XHSz3zZKC4NCP2ja1rI7++DXFhA+uDhdYa3MykCTGHY=
|
||||
github.com/NVIDIA/go-nvml v0.11.1-0/go.mod h1:hy7HYeQy335x6nEss0Ne3PYqleRa6Ct+VKD9RQ4nyFs=
|
||||
github.com/PaesslerAG/gval v1.1.2 h1:EROKxV4/fAKWb0Qoj7NOxmHZA7gcpjOV9XgiRZMRCUU=
|
||||
github.com/PaesslerAG/gval v1.1.2/go.mod h1:Fa8gfkCmUsELXgayr8sfL/sw+VzCVoa03dcOcR/if2w=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0=
|
||||
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
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/deepmap/oapi-codegen v1.15.0 h1:SQqViaeb4k2vMul8gx12oDOIadEtoRqTdLkxjzqtQ90=
|
||||
github.com/deepmap/oapi-codegen v1.15.0/go.mod h1:a6KoHV7lMRwsPoEg2C6NDHiXYV3EQfiFocOlJ8dgJQE=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
|
||||
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
|
||||
github.com/frankban/quicktest v1.11.0/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
|
||||
github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
|
||||
github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=
|
||||
github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24=
|
||||
github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
|
||||
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 h1:EcQR3gusLHN46TAD+G+EbaaqJArt5vHhNpXAa12PQf4=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gosnmp/gosnmp v1.37.0 h1:/Tf8D3b9wrnNuf/SfbvO+44mPrjVphBhRtcGg22V07Y=
|
||||
github.com/gosnmp/gosnmp v1.37.0/go.mod h1:GDH9vNqpsD7f2HvZhKs5dlqSEcAS6s6Qp099oZRCR+M=
|
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.12.3 h1:28nRlNMRIV4QbtIUvxhWqaxn0IpXeMSkY/uJa/O/vC4=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.12.3/go.mod h1:IrrLUbCjjfkmRuaCiGQg4m2GbkaeJDcuWoxiWdQEbA0=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.7.0 h1:QgP5mlBE9sGnzplpnf96pr+p7uqlIlL4W2GAP3n+XZg=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.7.0/go.mod h1:Y/0W1+TZir7ypoQZYd2IrnVOKB3Tq6oegAQeSVN/+EU=
|
||||
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU=
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
|
||||
github.com/influxdata/line-protocol-corpus v0.0.0-20210519164801-ca6fa5da0184/go.mod h1:03nmhxzZ7Xk2pdG+lmMd7mHDfeVOYFyhOgwO61qWU98=
|
||||
github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937 h1:MHJNQ+p99hFATQm6ORoLmpUCF7ovjwEFshs/NHzAbig=
|
||||
github.com/influxdata/line-protocol-corpus v0.0.0-20210922080147-aa28ccfb8937/go.mod h1:BKR9c0uHSmRgM/se9JhFHtTT7JTO67X23MtKMHtZcpo=
|
||||
github.com/influxdata/line-protocol/v2 v2.0.0-20210312151457-c52fdecb625a/go.mod h1:6+9Xt5Sq1rWx+glMgxhcg2c0DUaehK+5TDcPZ76GypY=
|
||||
github.com/influxdata/line-protocol/v2 v2.1.0/go.mod h1:QKw43hdUBg3GTk2iC3iyCxksNj7PX9aUSeYOYE/ceHY=
|
||||
github.com/influxdata/line-protocol/v2 v2.2.1 h1:EAPkqJ9Km4uAxtMRgUubJyqAr6zgWM0dznKMLRauQRE=
|
||||
github.com/influxdata/line-protocol/v2 v2.2.1/go.mod h1:DmB3Cnh+3oxmG6LOBIxce4oaL4CPj3OmMPgvauXh+tM=
|
||||
github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go=
|
||||
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
|
||||
github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/kataras/blocks v0.0.8 h1:MrpVhoFTCR2v1iOOfGng5VJSILKeZZI+7NGfxEh3SUM=
|
||||
github.com/kataras/blocks v0.0.8/go.mod h1:9Jm5zx6BB+06NwA+OhTbHW1xkMOYxahnqTN5DveZ2Yg=
|
||||
github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk=
|
||||
github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY=
|
||||
github.com/kataras/iris/v12 v12.2.7 h1:C9KWZmZT5pB5f2ot1XYWDBdi5XeTz0CGweHRXCDARZg=
|
||||
github.com/kataras/iris/v12 v12.2.7/go.mod h1:mD76k/tIBFy8pHTFIgUPrVrkI4lTKvFbIcfbStJSBnA=
|
||||
github.com/kataras/pio v0.0.12 h1:o52SfVYauS3J5X08fNjlGS5arXHjW/ItLkyLcKjoH6w=
|
||||
github.com/kataras/pio v0.0.12/go.mod h1:ODK/8XBhhQ5WqrAhKy+9lTPS7sBf6O3KcLhc9klfRcY=
|
||||
github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
|
||||
github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4=
|
||||
github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
|
||||
github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/klauspost/compress v1.13.4 h1:0zhec2I8zGnjWcKyLl6i3gPqKANCCn5e9xmviEEeX6s=
|
||||
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
|
||||
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
|
||||
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
|
||||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
|
||||
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nats-io/jwt/v2 v2.5.0 h1:WQQ40AAlqqfx+f6ku+i0pOVm+ASirD4fUh+oQsiE9Ak=
|
||||
github.com/nats-io/nats-server/v2 v2.8.4 h1:0jQzze1T9mECg8YZEl8+WYUXb9JKluJfCBriPUtluB4=
|
||||
github.com/nats-io/nats-server/v2 v2.8.4/go.mod h1:8zZa+Al3WsESfmgSs98Fi06dRWLH5Bnq90m5bKD/eT4=
|
||||
github.com/nats-io/nats.go v1.30.2 h1:aloM0TGpPorZKQhbAkdCzYDj+ZmsJDyeo3Gkbr72NuY=
|
||||
github.com/nats-io/nats.go v1.30.2/go.mod h1:dcfhUgmQNN4GJEfIb2f9R7Fow+gzBF4emzDHrVBd5qM=
|
||||
github.com/nats-io/nkeys v0.4.5 h1:Zdz2BUlFm4fJlierwvGK+yl20IAKUm7eV6AAZXEhkPk=
|
||||
github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
|
||||
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0=
|
||||
github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY=
|
||||
github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
|
||||
github.com/nats-io/nats-server/v2 v2.7.0 h1:UpqcAM93FI7AHlCyI2FD5QcV3QuHNCauQF2LBVU0238=
|
||||
github.com/nats-io/nats-server/v2 v2.7.0/go.mod h1:cjxtMhZsZovK1XS2iiapCduR8HuqB/YpFamL0qntIcw=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc h1:SHr4MUUZJ/fAC0uSm2OzWOJYsHpapmR86mpw7q1qPXU=
|
||||
github.com/nats-io/nats.go v1.13.1-0.20211122170419-d7c1d78a50fc/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
|
||||
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
|
||||
github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stmcginnis/gofish v0.14.0 h1:geECNAiG33JDB2x2xDkerpOOuXFqxp5YP3EFE3vd5iM=
|
||||
github.com/stmcginnis/gofish v0.14.0/go.mod h1:BLDSFTp8pDlf/xDbLZa+F7f7eW0E/CHCboggsu8CznI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tdewolff/minify/v2 v2.12.9 h1:dvn5MtmuQ/DFMwqf5j8QhEVpPX6fi3WGImhv8RUB4zA=
|
||||
github.com/tdewolff/minify/v2 v2.12.9/go.mod h1:qOqdlDfL+7v0/fyymB+OP497nIxJYSvX4MQWA8OoiXU=
|
||||
github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvFMA=
|
||||
github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
|
||||
github.com/tdewolff/test v1.0.9 h1:SswqJCmeN4B+9gEAi/5uqT0qpi1y2/2O47V/1hhGZT0=
|
||||
github.com/tdewolff/test v1.0.9/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.0 h1:hRM0digJwyR6vll33NNAwCFguy5JuBD6jxDmQP3l608=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.0/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
|
||||
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
|
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.design/x/thread v0.0.0-20210122121316-335e9adffdf1 h1:P7S/GeHBAFEZIYp0ePPs2kHXoazz8q2KsyxHyQVGCJg=
|
||||
golang.design/x/thread v0.0.0-20210122121316-335e9adffdf1/go.mod h1:9CWpnTUmlQkfdpdutA1nNf4iE5lAVt3QZOu0Z6hahBE=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
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/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI=
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
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/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210122093101-04d7465088b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/Knetic/govaluate.v2 v2.3.0 h1:naJVc9CZlWA8rC8f5mvECJD7jreTrn7FvGXjBthkHJQ=
|
||||
gopkg.in/Knetic/govaluate.v2 v2.3.0/go.mod h1:NW0gr10J8s7aNghEg6uhdxiEaBvc0+8VgJjVViHUKp4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
32
internal/ccMetric/README.md
Normal file
32
internal/ccMetric/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# ClusterCockpit metrics
|
||||
|
||||
As described in the [ClusterCockpit specifications](https://github.com/ClusterCockpit/cc-specifications), the whole ClusterCockpit stack uses metrics in the InfluxDB line protocol format. This is also the input and output format for the ClusterCockpit Metric Collector but internally it uses an extended format while processing, named CCMetric.
|
||||
|
||||
It is basically a copy of the [InfluxDB line protocol](https://github.com/influxdata/line-protocol) `MutableMetric` interface with one extension. Besides the tags and fields, it contains a list of meta information (re-using the `Tag` structure of the original protocol):
|
||||
|
||||
```golang
|
||||
type ccMetric struct {
|
||||
name string // same as
|
||||
tags []*influx.Tag // original
|
||||
fields []*influx.Field // Influx
|
||||
tm time.Time // line-protocol
|
||||
meta []*influx.Tag
|
||||
}
|
||||
|
||||
type CCMetric interface {
|
||||
influx.MutableMetric // the same functions as defined by influx.MutableMetric
|
||||
RemoveTag(key string) // this is not published by the original influx.MutableMetric
|
||||
Meta() map[string]string
|
||||
MetaList() []*inlux.Tag
|
||||
AddMeta(key, value string)
|
||||
HasMeta(key string) bool
|
||||
GetMeta(key string) (string, bool)
|
||||
RemoveMeta(key string)
|
||||
}
|
||||
```
|
||||
|
||||
The `CCMetric` interface provides the same functions as the `MutableMetric` like `{Add, Remove, Has}{Tag, Field}` and additionally provides `{Add, Remove, Has}Meta`.
|
||||
|
||||
The InfluxDB protocol creates a new metric with `influx.New(name, tags, fields, time)` while CCMetric uses `ccMetric.New(name, tags, meta, fields, time)` where `tags` and `meta` are both of type `map[string]string`.
|
||||
|
||||
You can copy a CCMetric with `FromMetric(other CCMetric) CCMetric`. If you get an `influx.Metric` from a function, like the line protocol parser, you can use `FromInfluxMetric(other influx.Metric) CCMetric` to get a CCMetric out of it (see `NatsReceiver` for an example).
|
@@ -7,7 +7,6 @@ import (
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
write "github.com/influxdata/influxdb-client-go/v2/api/write"
|
||||
lp "github.com/influxdata/line-protocol" // MIT license
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// Most functions are derived from github.com/influxdata/line-protocol/metric.go
|
||||
@@ -51,7 +50,6 @@ type CCMetric interface {
|
||||
GetField(key string) (value interface{}, ok bool) // Get a field addressed by its key
|
||||
HasField(key string) (ok bool) // Check if a field key is present
|
||||
RemoveField(key string) // Remove a field addressed by its key
|
||||
String() string // Return line-protocol like string
|
||||
}
|
||||
|
||||
// String implements the stringer interface for data type ccMetric
|
||||
@@ -65,11 +63,9 @@ func (m *ccMetric) String() string {
|
||||
// ToLineProtocol generates influxDB line protocol for data type ccMetric
|
||||
func (m *ccMetric) ToPoint(metaAsTags map[string]bool) (p *write.Point) {
|
||||
p = influxdb2.NewPoint(m.name, m.tags, m.fields, m.tm)
|
||||
for key, use_as_tag := range metaAsTags {
|
||||
if use_as_tag {
|
||||
if value, ok := m.GetMeta(key); ok {
|
||||
p.AddTag(key, value)
|
||||
}
|
||||
for key, ok1 := range metaAsTags {
|
||||
if val, ok2 := m.GetMeta(key); ok1 && ok2 {
|
||||
p.AddTag(key, val)
|
||||
}
|
||||
}
|
||||
return p
|
||||
@@ -196,13 +192,19 @@ func New(
|
||||
) (CCMetric, error) {
|
||||
m := &ccMetric{
|
||||
name: name,
|
||||
tags: maps.Clone(tags),
|
||||
meta: maps.Clone(meta),
|
||||
tags: make(map[string]string, len(tags)),
|
||||
meta: make(map[string]string, len(meta)),
|
||||
fields: make(map[string]interface{}, len(fields)),
|
||||
tm: tm,
|
||||
}
|
||||
|
||||
// deep copy fields
|
||||
// deep copy tags, meta data tags and fields
|
||||
for k, v := range tags {
|
||||
m.tags[k] = v
|
||||
}
|
||||
for k, v := range meta {
|
||||
m.meta[k] = v
|
||||
}
|
||||
for k, v := range fields {
|
||||
v := convertField(v)
|
||||
if v == nil {
|
||||
@@ -215,15 +217,26 @@ func New(
|
||||
}
|
||||
|
||||
// FromMetric copies the metric <other>
|
||||
func FromMetric(other CCMetric) CCMetric {
|
||||
|
||||
return &ccMetric{
|
||||
func FromMetric(other ccMetric) CCMetric {
|
||||
m := &ccMetric{
|
||||
name: other.Name(),
|
||||
tags: maps.Clone(other.Tags()),
|
||||
meta: maps.Clone(other.Meta()),
|
||||
fields: maps.Clone(other.Fields()),
|
||||
tags: make(map[string]string, len(other.tags)),
|
||||
meta: make(map[string]string, len(other.meta)),
|
||||
fields: make(map[string]interface{}, len(other.fields)),
|
||||
tm: other.Time(),
|
||||
}
|
||||
|
||||
// deep copy tags, meta data tags and fields
|
||||
for key, value := range other.tags {
|
||||
m.tags[key] = value
|
||||
}
|
||||
for key, value := range other.meta {
|
||||
m.meta[key] = value
|
||||
}
|
||||
for key, value := range other.fields {
|
||||
m.fields[key] = value
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// FromInfluxMetric copies the influxDB line protocol metric <other>
|
||||
@@ -247,10 +260,8 @@ func FromInfluxMetric(other lp.Metric) CCMetric {
|
||||
}
|
||||
|
||||
// convertField converts data types of fields by the following schemata:
|
||||
//
|
||||
// *float32, *float64, float32, float64 -> float64
|
||||
// *int, *int8, *int16, *int32, *int64, int, int8, int16, int32, int64 -> int64
|
||||
//
|
||||
// *float32, *float64, float32, float64 -> float64
|
||||
// *int, *int8, *int16, *int32, *int64, int, int8, int16, int32, int64 -> int64
|
||||
// *uint, *uint8, *uint16, *uint32, *uint64, uint, uint8, uint16, uint32, uint64 -> uint64
|
||||
// *[]byte, *string, []byte, string -> string
|
||||
// *bool, bool -> bool
|
427
internal/ccTopology/ccTopology.go
Normal file
427
internal/ccTopology/ccTopology.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package ccTopology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cclogger "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
)
|
||||
|
||||
const SYSFS_NUMABASE = `/sys/devices/system/node`
|
||||
const SYSFS_CPUBASE = `/sys/devices/system/cpu`
|
||||
const PROCFS_CPUINFO = `/proc/cpuinfo`
|
||||
|
||||
// intArrayContains scans an array of ints if the value str is present in the array
|
||||
// If the specified value is found, the corresponding array index is returned.
|
||||
// The bool value is used to signal success or failure
|
||||
func intArrayContains(array []int, str int) (int, bool) {
|
||||
for i, a := range array {
|
||||
if a == str {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func fileToInt(path string) int {
|
||||
buffer, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
cclogger.ComponentError("ccTopology", "Reading", path, ":", err.Error())
|
||||
return -1
|
||||
}
|
||||
sbuffer := strings.Replace(string(buffer), "\n", "", -1)
|
||||
var id int64
|
||||
//_, err = fmt.Scanf("%d", sbuffer, &id)
|
||||
id, err = strconv.ParseInt(sbuffer, 10, 32)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("ccTopology", "Parsing", path, ":", sbuffer, err.Error())
|
||||
return -1
|
||||
}
|
||||
return int(id)
|
||||
}
|
||||
|
||||
func SocketList() []int {
|
||||
buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
ll := strings.Split(string(buffer), "\n")
|
||||
var packs []int
|
||||
for _, line := range ll {
|
||||
if strings.HasPrefix(line, "physical id") {
|
||||
lv := strings.Fields(line)
|
||||
id, err := strconv.ParseInt(lv[3], 10, 32)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return packs
|
||||
}
|
||||
_, found := intArrayContains(packs, int(id))
|
||||
if !found {
|
||||
packs = append(packs, int(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
return packs
|
||||
}
|
||||
|
||||
func CpuList() []int {
|
||||
buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
ll := strings.Split(string(buffer), "\n")
|
||||
cpulist := make([]int, 0)
|
||||
for _, line := range ll {
|
||||
if strings.HasPrefix(line, "processor") {
|
||||
lv := strings.Fields(line)
|
||||
id, err := strconv.ParseInt(lv[2], 10, 32)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return cpulist
|
||||
}
|
||||
_, found := intArrayContains(cpulist, int(id))
|
||||
if !found {
|
||||
cpulist = append(cpulist, int(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
return cpulist
|
||||
}
|
||||
|
||||
func CoreList() []int {
|
||||
buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return nil
|
||||
}
|
||||
ll := strings.Split(string(buffer), "\n")
|
||||
corelist := make([]int, 0)
|
||||
for _, line := range ll {
|
||||
if strings.HasPrefix(line, "core id") {
|
||||
lv := strings.Fields(line)
|
||||
id, err := strconv.ParseInt(lv[3], 10, 32)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return corelist
|
||||
}
|
||||
_, found := intArrayContains(corelist, int(id))
|
||||
if !found {
|
||||
corelist = append(corelist, int(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
return corelist
|
||||
}
|
||||
|
||||
func NumaNodeList() []int {
|
||||
numaList := make([]int, 0)
|
||||
globPath := filepath.Join(string(SYSFS_NUMABASE), "node*")
|
||||
regexPath := filepath.Join(string(SYSFS_NUMABASE), "node(\\d+)")
|
||||
regex := regexp.MustCompile(regexPath)
|
||||
files, err := filepath.Glob(globPath)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "NumaNodeList", err.Error())
|
||||
}
|
||||
for _, f := range files {
|
||||
if !regex.MatchString(f) {
|
||||
continue
|
||||
}
|
||||
finfo, err := os.Lstat(f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !finfo.IsDir() {
|
||||
continue
|
||||
}
|
||||
matches := regex.FindStringSubmatch(f)
|
||||
if len(matches) == 2 {
|
||||
id, err := strconv.Atoi(matches[1])
|
||||
if err == nil {
|
||||
if _, found := intArrayContains(numaList, id); !found {
|
||||
numaList = append(numaList, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return numaList
|
||||
}
|
||||
|
||||
func DieList() []int {
|
||||
cpulist := CpuList()
|
||||
dielist := make([]int, 0)
|
||||
for _, c := range cpulist {
|
||||
diepath := filepath.Join(string(SYSFS_CPUBASE), fmt.Sprintf("cpu%d", c), "topology/die_id")
|
||||
dieid := fileToInt(diepath)
|
||||
if dieid > 0 {
|
||||
_, found := intArrayContains(dielist, int(dieid))
|
||||
if !found {
|
||||
dielist = append(dielist, int(dieid))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(dielist) > 0 {
|
||||
return dielist
|
||||
}
|
||||
return SocketList()
|
||||
}
|
||||
|
||||
type CpuEntry struct {
|
||||
Cpuid int
|
||||
SMT int
|
||||
Core int
|
||||
Socket int
|
||||
Numadomain int
|
||||
Die int
|
||||
}
|
||||
|
||||
func CpuData() []CpuEntry {
|
||||
|
||||
fileToInt := func(path string) int {
|
||||
buffer, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
//cclogger.ComponentError("ccTopology", "Reading", path, ":", err.Error())
|
||||
return -1
|
||||
}
|
||||
sbuffer := strings.Replace(string(buffer), "\n", "", -1)
|
||||
var id int64
|
||||
//_, err = fmt.Scanf("%d", sbuffer, &id)
|
||||
id, err = strconv.ParseInt(sbuffer, 10, 32)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("ccTopology", "Parsing", path, ":", sbuffer, err.Error())
|
||||
return -1
|
||||
}
|
||||
return int(id)
|
||||
}
|
||||
getCore := func(basepath string) int {
|
||||
return fileToInt(fmt.Sprintf("%s/core_id", basepath))
|
||||
}
|
||||
|
||||
getSocket := func(basepath string) int {
|
||||
return fileToInt(fmt.Sprintf("%s/physical_package_id", basepath))
|
||||
}
|
||||
|
||||
getDie := func(basepath string) int {
|
||||
return fileToInt(fmt.Sprintf("%s/die_id", basepath))
|
||||
}
|
||||
|
||||
getSMT := func(cpuid int, basepath string) int {
|
||||
buffer, err := ioutil.ReadFile(fmt.Sprintf("%s/thread_siblings_list", basepath))
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "CpuData:getSMT", err.Error())
|
||||
}
|
||||
threadlist := make([]int, 0)
|
||||
sbuffer := strings.Replace(string(buffer), "\n", "", -1)
|
||||
for _, x := range strings.Split(sbuffer, ",") {
|
||||
id, err := strconv.ParseInt(x, 10, 32)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "CpuData:getSMT", err.Error())
|
||||
}
|
||||
threadlist = append(threadlist, int(id))
|
||||
}
|
||||
for i, x := range threadlist {
|
||||
if x == cpuid {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
getNumaDomain := func(basepath string) int {
|
||||
globPath := filepath.Join(basepath, "node*")
|
||||
regexPath := filepath.Join(basepath, "node(\\d+)")
|
||||
regex := regexp.MustCompile(regexPath)
|
||||
files, err := filepath.Glob(globPath)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "CpuData:getNumaDomain", err.Error())
|
||||
}
|
||||
for _, f := range files {
|
||||
finfo, err := os.Lstat(f)
|
||||
if err == nil && finfo.IsDir() {
|
||||
matches := regex.FindStringSubmatch(f)
|
||||
if len(matches) == 2 {
|
||||
id, err := strconv.Atoi(matches[1])
|
||||
if err == nil {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
clist := make([]CpuEntry, 0)
|
||||
for _, c := range CpuList() {
|
||||
clist = append(clist, CpuEntry{Cpuid: c})
|
||||
}
|
||||
for i, centry := range clist {
|
||||
centry.Socket = -1
|
||||
centry.Numadomain = -1
|
||||
centry.Die = -1
|
||||
centry.Core = -1
|
||||
// Set base directory for topology lookup
|
||||
cpustr := fmt.Sprintf("cpu%d", centry.Cpuid)
|
||||
base := filepath.Join("/sys/devices/system/cpu", cpustr)
|
||||
topoBase := filepath.Join(base, "topology")
|
||||
|
||||
// Lookup CPU core id
|
||||
centry.Core = getCore(topoBase)
|
||||
|
||||
// Lookup CPU socket id
|
||||
centry.Socket = getSocket(topoBase)
|
||||
|
||||
// Lookup CPU die id
|
||||
centry.Die = getDie(topoBase)
|
||||
if centry.Die < 0 {
|
||||
centry.Die = centry.Socket
|
||||
}
|
||||
|
||||
// Lookup SMT thread id
|
||||
centry.SMT = getSMT(centry.Cpuid, topoBase)
|
||||
|
||||
// Lookup NUMA domain id
|
||||
centry.Numadomain = getNumaDomain(base)
|
||||
|
||||
// Update values in output list
|
||||
clist[i] = centry
|
||||
}
|
||||
return clist
|
||||
}
|
||||
|
||||
type CpuInformation struct {
|
||||
NumHWthreads int
|
||||
SMTWidth int
|
||||
NumSockets int
|
||||
NumDies int
|
||||
NumCores int
|
||||
NumNumaDomains int
|
||||
}
|
||||
|
||||
func CpuInfo() CpuInformation {
|
||||
var c CpuInformation
|
||||
|
||||
smtList := make([]int, 0)
|
||||
numaList := make([]int, 0)
|
||||
dieList := make([]int, 0)
|
||||
socketList := make([]int, 0)
|
||||
coreList := make([]int, 0)
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if _, ok := intArrayContains(smtList, d.SMT); !ok {
|
||||
smtList = append(smtList, d.SMT)
|
||||
}
|
||||
if _, ok := intArrayContains(numaList, d.Numadomain); !ok {
|
||||
numaList = append(numaList, d.Numadomain)
|
||||
}
|
||||
if _, ok := intArrayContains(dieList, d.Die); !ok {
|
||||
dieList = append(dieList, d.Die)
|
||||
}
|
||||
if _, ok := intArrayContains(socketList, d.Socket); !ok {
|
||||
socketList = append(socketList, d.Socket)
|
||||
}
|
||||
if _, ok := intArrayContains(coreList, d.Core); !ok {
|
||||
coreList = append(coreList, d.Core)
|
||||
}
|
||||
}
|
||||
c.NumNumaDomains = len(numaList)
|
||||
c.SMTWidth = len(smtList)
|
||||
c.NumDies = len(dieList)
|
||||
c.NumCores = len(coreList)
|
||||
c.NumSockets = len(socketList)
|
||||
c.NumHWthreads = len(cdata)
|
||||
return c
|
||||
}
|
||||
|
||||
func GetCpuSocket(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
return d.Socket
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetCpuNumaDomain(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
return d.Numadomain
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetCpuDie(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
return d.Die
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetCpuCore(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
return d.Core
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetSocketCpus(socket int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
if d.Socket == socket {
|
||||
cpulist = append(cpulist, d.Cpuid)
|
||||
}
|
||||
}
|
||||
return cpulist
|
||||
}
|
||||
|
||||
func GetNumaDomainCpus(domain int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
if d.Numadomain == domain {
|
||||
cpulist = append(cpulist, d.Cpuid)
|
||||
}
|
||||
}
|
||||
return cpulist
|
||||
}
|
||||
|
||||
func GetDieCpus(die int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
if d.Die == die {
|
||||
cpulist = append(cpulist, d.Cpuid)
|
||||
}
|
||||
}
|
||||
return cpulist
|
||||
}
|
||||
|
||||
func GetCoreCpus(core int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
if d.Core == core {
|
||||
cpulist = append(cpulist, d.Cpuid)
|
||||
}
|
||||
}
|
||||
return cpulist
|
||||
}
|
@@ -9,10 +9,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
topo "github.com/ClusterCockpit/cc-metric-collector/pkg/ccTopology"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
topo "github.com/ClusterCockpit/cc-metric-collector/internal/ccTopology"
|
||||
|
||||
"github.com/PaesslerAG/gval"
|
||||
)
|
||||
@@ -121,12 +121,7 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
||||
vars["endtime"] = endtime
|
||||
for _, f := range c.functions {
|
||||
cclog.ComponentDebug("MetricCache", "COLLECT", f.Name, "COND", f.Condition)
|
||||
var valuesFloat64 []float64
|
||||
var valuesFloat32 []float32
|
||||
var valuesInt []int
|
||||
var valuesInt32 []int32
|
||||
var valuesInt64 []int64
|
||||
var valuesBool []bool
|
||||
values := make([]float64, 0)
|
||||
matches := make([]lp.CCMetric, 0)
|
||||
for _, m := range metrics {
|
||||
vars["metric"] = m
|
||||
@@ -141,17 +136,17 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
||||
if valid {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
valuesFloat64 = append(valuesFloat64, x)
|
||||
values = append(values, x)
|
||||
case float32:
|
||||
valuesFloat32 = append(valuesFloat32, x)
|
||||
case int:
|
||||
valuesInt = append(valuesInt, x)
|
||||
case int32:
|
||||
valuesInt32 = append(valuesInt32, x)
|
||||
case int64:
|
||||
valuesInt64 = append(valuesInt64, x)
|
||||
values = append(values, float64(x))
|
||||
case bool:
|
||||
valuesBool = append(valuesBool, x)
|
||||
if x {
|
||||
values = append(values, float64(1.0))
|
||||
} else {
|
||||
values = append(values, float64(0.0))
|
||||
}
|
||||
default:
|
||||
cclog.ComponentError("MetricCache", "COLLECT ADD VALUE", v, "FAILED")
|
||||
}
|
||||
@@ -160,59 +155,13 @@ func (c *metricAggregator) Eval(starttime time.Time, endtime time.Time, metrics
|
||||
}
|
||||
}
|
||||
delete(vars, "metric")
|
||||
|
||||
// Check, that only values of one type were collected
|
||||
countValueTypes := 0
|
||||
if len(valuesFloat64) > 0 {
|
||||
countValueTypes += 1
|
||||
}
|
||||
if len(valuesFloat32) > 0 {
|
||||
countValueTypes += 1
|
||||
}
|
||||
if len(valuesInt) > 0 {
|
||||
countValueTypes += 1
|
||||
}
|
||||
if len(valuesInt32) > 0 {
|
||||
countValueTypes += 1
|
||||
}
|
||||
if len(valuesInt64) > 0 {
|
||||
countValueTypes += 1
|
||||
}
|
||||
if len(valuesBool) > 0 {
|
||||
countValueTypes += 1
|
||||
}
|
||||
if countValueTypes > 1 {
|
||||
cclog.ComponentError("MetricCache", "Collected values of different types")
|
||||
}
|
||||
|
||||
var len_values int
|
||||
switch {
|
||||
case len(valuesFloat64) > 0:
|
||||
vars["values"] = valuesFloat64
|
||||
len_values = len(valuesFloat64)
|
||||
case len(valuesFloat32) > 0:
|
||||
vars["values"] = valuesFloat32
|
||||
len_values = len(valuesFloat32)
|
||||
case len(valuesInt) > 0:
|
||||
vars["values"] = valuesInt
|
||||
len_values = len(valuesInt)
|
||||
case len(valuesInt32) > 0:
|
||||
vars["values"] = valuesInt32
|
||||
len_values = len(valuesInt32)
|
||||
case len(valuesInt64) > 0:
|
||||
vars["values"] = valuesInt64
|
||||
len_values = len(valuesInt64)
|
||||
case len(valuesBool) > 0:
|
||||
vars["values"] = valuesBool
|
||||
len_values = len(valuesBool)
|
||||
}
|
||||
cclog.ComponentDebug("MetricCache", "EVALUATE", f.Name, "METRICS", len_values, "CALC", f.Function)
|
||||
|
||||
cclog.ComponentDebug("MetricCache", "EVALUATE", f.Name, "METRICS", len(values), "CALC", f.Function)
|
||||
vars["values"] = values
|
||||
vars["metrics"] = matches
|
||||
if len_values > 0 {
|
||||
if len(values) > 0 {
|
||||
value, err := gval.Evaluate(f.Function, vars, c.language)
|
||||
if err != nil {
|
||||
cclog.ComponentError("MetricCache", "EVALUATE", f.Name, "METRICS", len_values, "CALC", f.Function, ":", err.Error())
|
||||
cclog.ComponentError("MetricCache", "EVALUATE", f.Name, "METRICS", len(values), "CALC", f.Function, ":", err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
@@ -367,7 +316,7 @@ func EvalBoolCondition(condition string, params map[string]interface{}) (bool, e
|
||||
return value, err
|
||||
}
|
||||
|
||||
func EvalFloat64Condition(condition string, params map[string]float64) (float64, error) {
|
||||
func EvalFloat64Condition(condition string, params map[string]interface{}) (float64, error) {
|
||||
evaluables.mutex.Lock()
|
||||
evaluable, ok := evaluables.mapping[condition]
|
||||
evaluables.mutex.Unlock()
|
||||
|
@@ -3,167 +3,162 @@ package metricAggregator
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
topo "github.com/ClusterCockpit/cc-metric-collector/pkg/ccTopology"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
topo "github.com/ClusterCockpit/cc-metric-collector/internal/ccTopology"
|
||||
)
|
||||
|
||||
/*
|
||||
* Arithmetic functions on value arrays
|
||||
*/
|
||||
|
||||
func sumAnyType[T float64 | float32 | int | int32 | int64](values []T) (T, error) {
|
||||
if len(values) == 0 {
|
||||
return 0.0, errors.New("sum function requires at least one argument")
|
||||
// Sum up values
|
||||
func sumfunc(args ...interface{}) (interface{}, error) {
|
||||
s := 0.0
|
||||
values, ok := args[0].([]float64)
|
||||
if ok {
|
||||
cclog.ComponentDebug("MetricCache", "SUM FUNC START")
|
||||
for _, x := range values {
|
||||
s += x
|
||||
}
|
||||
cclog.ComponentDebug("MetricCache", "SUM FUNC END", s)
|
||||
} else {
|
||||
cclog.ComponentDebug("MetricCache", "SUM FUNC CAST FAILED")
|
||||
}
|
||||
var sum T
|
||||
for _, value := range values {
|
||||
sum += value
|
||||
}
|
||||
return sum, nil
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Sum up values
|
||||
func sumfunc(args interface{}) (interface{}, error) {
|
||||
|
||||
var err error
|
||||
switch values := args.(type) {
|
||||
// Get the minimum value
|
||||
func minfunc(args ...interface{}) (interface{}, error) {
|
||||
var err error = nil
|
||||
switch values := args[0].(type) {
|
||||
case []float64:
|
||||
return sumAnyType(values)
|
||||
var s float64 = math.MaxFloat64
|
||||
for _, x := range values {
|
||||
if x < s {
|
||||
s = x
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
case []float32:
|
||||
return sumAnyType(values)
|
||||
var s float32 = math.MaxFloat32
|
||||
for _, x := range values {
|
||||
if x < s {
|
||||
s = x
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
case []int:
|
||||
return sumAnyType(values)
|
||||
var s int = int(math.MaxInt32)
|
||||
for _, x := range values {
|
||||
if x < s {
|
||||
s = x
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
case []int64:
|
||||
return sumAnyType(values)
|
||||
var s int64 = math.MaxInt64
|
||||
for _, x := range values {
|
||||
if x < s {
|
||||
s = x
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
case []int32:
|
||||
return sumAnyType(values)
|
||||
var s int32 = math.MaxInt32
|
||||
for _, x := range values {
|
||||
if x < s {
|
||||
s = x
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
default:
|
||||
err = errors.New("function 'sum' only on list of values (float64, float32, int, int32, int64)")
|
||||
err = errors.New("function 'min' only on list of values (float64, float32, int, int32, int64)")
|
||||
}
|
||||
|
||||
return 0.0, err
|
||||
}
|
||||
|
||||
func minAnyType[T float64 | float32 | int | int32 | int64](values []T) (T, error) {
|
||||
if len(values) == 0 {
|
||||
return 0.0, errors.New("min function requires at least one argument")
|
||||
}
|
||||
return slices.Min(values), nil
|
||||
}
|
||||
|
||||
// Get the minimum value
|
||||
func minfunc(args interface{}) (interface{}, error) {
|
||||
switch values := args.(type) {
|
||||
case []float64:
|
||||
return minAnyType(values)
|
||||
case []float32:
|
||||
return minAnyType(values)
|
||||
case []int:
|
||||
return minAnyType(values)
|
||||
case []int64:
|
||||
return minAnyType(values)
|
||||
case []int32:
|
||||
return minAnyType(values)
|
||||
default:
|
||||
return 0.0, errors.New("function 'min' only on list of values (float64, float32, int, int32, int64)")
|
||||
}
|
||||
}
|
||||
|
||||
func avgAnyType[T float64 | float32 | int | int32 | int64](values []T) (float64, error) {
|
||||
if len(values) == 0 {
|
||||
return 0.0, errors.New("average function requires at least one argument")
|
||||
}
|
||||
sum, err := sumAnyType[T](values)
|
||||
return float64(sum) / float64(len(values)), err
|
||||
}
|
||||
|
||||
// Get the average or mean value
|
||||
func avgfunc(args interface{}) (interface{}, error) {
|
||||
switch values := args.(type) {
|
||||
func avgfunc(args ...interface{}) (interface{}, error) {
|
||||
switch values := args[0].(type) {
|
||||
case []float64:
|
||||
return avgAnyType(values)
|
||||
var s float64 = 0
|
||||
for _, x := range values {
|
||||
s += x
|
||||
}
|
||||
return s / float64(len(values)), nil
|
||||
case []float32:
|
||||
return avgAnyType(values)
|
||||
var s float32 = 0
|
||||
for _, x := range values {
|
||||
s += x
|
||||
}
|
||||
return s / float32(len(values)), nil
|
||||
case []int:
|
||||
return avgAnyType(values)
|
||||
var s int = 0
|
||||
for _, x := range values {
|
||||
s += x
|
||||
}
|
||||
return s / len(values), nil
|
||||
case []int64:
|
||||
return avgAnyType(values)
|
||||
case []int32:
|
||||
return avgAnyType(values)
|
||||
default:
|
||||
return 0.0, errors.New("function 'average' only on list of values (float64, float32, int, int32, int64)")
|
||||
var s int64 = 0
|
||||
for _, x := range values {
|
||||
s += x
|
||||
}
|
||||
return s / int64(len(values)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func maxAnyType[T float64 | float32 | int | int32 | int64](values []T) (T, error) {
|
||||
if len(values) == 0 {
|
||||
return 0.0, errors.New("max function requires at least one argument")
|
||||
}
|
||||
return slices.Max(values), nil
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
// Get the maximum value
|
||||
func maxfunc(args interface{}) (interface{}, error) {
|
||||
switch values := args.(type) {
|
||||
case []float64:
|
||||
return maxAnyType(values)
|
||||
case []float32:
|
||||
return maxAnyType(values)
|
||||
case []int:
|
||||
return maxAnyType(values)
|
||||
case []int64:
|
||||
return maxAnyType(values)
|
||||
case []int32:
|
||||
return maxAnyType(values)
|
||||
default:
|
||||
return 0.0, errors.New("function 'max' only on list of values (float64, float32, int, int32, int64)")
|
||||
func maxfunc(args ...interface{}) (interface{}, error) {
|
||||
s := 0.0
|
||||
values, ok := args[0].([]float64)
|
||||
if ok {
|
||||
for _, x := range values {
|
||||
if x > s {
|
||||
s = x
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func medianAnyType[T float64 | float32 | int | int32 | int64](values []T) (T, error) {
|
||||
if len(values) == 0 {
|
||||
return 0.0, errors.New("median function requires at least one argument")
|
||||
}
|
||||
slices.Sort(values)
|
||||
var median T
|
||||
if midPoint := len(values) % 2; midPoint == 0 {
|
||||
median = (values[midPoint-1] + values[midPoint]) / 2
|
||||
} else {
|
||||
median = values[midPoint]
|
||||
}
|
||||
return median, nil
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Get the median value
|
||||
func medianfunc(args interface{}) (interface{}, error) {
|
||||
switch values := args.(type) {
|
||||
func medianfunc(args ...interface{}) (interface{}, error) {
|
||||
switch values := args[0].(type) {
|
||||
case []float64:
|
||||
return medianAnyType(values)
|
||||
case []float32:
|
||||
return medianAnyType(values)
|
||||
sort.Float64s(values)
|
||||
return values[len(values)/2], nil
|
||||
// case []float32:
|
||||
// sort.Float64s(values)
|
||||
// return values[len(values)/2], nil
|
||||
case []int:
|
||||
return medianAnyType(values)
|
||||
case []int64:
|
||||
return medianAnyType(values)
|
||||
case []int32:
|
||||
return medianAnyType(values)
|
||||
default:
|
||||
return 0.0, errors.New("function 'median' only on list of values (float64, float32, int, int32, int64)")
|
||||
sort.Ints(values)
|
||||
return values[len(values)/2], nil
|
||||
|
||||
// case []int64:
|
||||
// sort.Ints(values)
|
||||
// return values[len(values)/2], nil
|
||||
// case []int32:
|
||||
// sort.Ints(values)
|
||||
// return values[len(values)/2], nil
|
||||
}
|
||||
return 0.0, errors.New("function 'median()' only on lists of type float64 and int")
|
||||
}
|
||||
|
||||
/*
|
||||
* Get number of values in list. Returns always an int
|
||||
*/
|
||||
|
||||
func lenfunc(args interface{}) (interface{}, error) {
|
||||
func lenfunc(args ...interface{}) (interface{}, error) {
|
||||
var err error = nil
|
||||
var length int = 0
|
||||
switch values := args.(type) {
|
||||
switch values := args[0].(type) {
|
||||
case []float64:
|
||||
length = len(values)
|
||||
case []float32:
|
||||
@@ -248,49 +243,49 @@ func matchfunc(args ...interface{}) (interface{}, error) {
|
||||
*/
|
||||
|
||||
// for a given cpuid, it returns the core id
|
||||
func getCpuCoreFunc(args interface{}) (interface{}, error) {
|
||||
switch cpuid := args.(type) {
|
||||
func getCpuCoreFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetHwthreadCore(cpuid), nil
|
||||
return topo.GetCpuCore(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuCore' accepts only an 'int' cpuid")
|
||||
}
|
||||
|
||||
// for a given cpuid, it returns the socket id
|
||||
func getCpuSocketFunc(args interface{}) (interface{}, error) {
|
||||
switch cpuid := args.(type) {
|
||||
func getCpuSocketFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetHwthreadSocket(cpuid), nil
|
||||
return topo.GetCpuSocket(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuCore' accepts only an 'int' cpuid")
|
||||
}
|
||||
|
||||
// for a given cpuid, it returns the id of the NUMA node
|
||||
func getCpuNumaDomainFunc(args interface{}) (interface{}, error) {
|
||||
switch cpuid := args.(type) {
|
||||
func getCpuNumaDomainFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetHwthreadNumaDomain(cpuid), nil
|
||||
return topo.GetCpuNumaDomain(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuNuma' accepts only an 'int' cpuid")
|
||||
}
|
||||
|
||||
// for a given cpuid, it returns the id of the CPU die
|
||||
func getCpuDieFunc(args interface{}) (interface{}, error) {
|
||||
switch cpuid := args.(type) {
|
||||
func getCpuDieFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetHwthreadDie(cpuid), nil
|
||||
return topo.GetCpuDie(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuDie' accepts only an 'int' cpuid")
|
||||
}
|
||||
|
||||
// for a given core id, it returns the list of cpuids
|
||||
func getCpuListOfCoreFunc(args interface{}) (interface{}, error) {
|
||||
func getCpuListOfCoreFunc(args ...interface{}) (interface{}, error) {
|
||||
cpulist := make([]int, 0)
|
||||
switch in := args.(type) {
|
||||
switch in := args[0].(type) {
|
||||
case int:
|
||||
for _, c := range topo.CpuData() {
|
||||
if c.Core == in {
|
||||
cpulist = append(cpulist, c.CpuID)
|
||||
cpulist = append(cpulist, c.Cpuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,13 +293,13 @@ func getCpuListOfCoreFunc(args interface{}) (interface{}, error) {
|
||||
}
|
||||
|
||||
// for a given socket id, it returns the list of cpuids
|
||||
func getCpuListOfSocketFunc(args interface{}) (interface{}, error) {
|
||||
func getCpuListOfSocketFunc(args ...interface{}) (interface{}, error) {
|
||||
cpulist := make([]int, 0)
|
||||
switch in := args.(type) {
|
||||
switch in := args[0].(type) {
|
||||
case int:
|
||||
for _, c := range topo.CpuData() {
|
||||
if c.Socket == in {
|
||||
cpulist = append(cpulist, c.CpuID)
|
||||
cpulist = append(cpulist, c.Cpuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,13 +307,13 @@ func getCpuListOfSocketFunc(args interface{}) (interface{}, error) {
|
||||
}
|
||||
|
||||
// for a given id of a NUMA domain, it returns the list of cpuids
|
||||
func getCpuListOfNumaDomainFunc(args interface{}) (interface{}, error) {
|
||||
func getCpuListOfNumaDomainFunc(args ...interface{}) (interface{}, error) {
|
||||
cpulist := make([]int, 0)
|
||||
switch in := args.(type) {
|
||||
switch in := args[0].(type) {
|
||||
case int:
|
||||
for _, c := range topo.CpuData() {
|
||||
if c.NumaDomain == in {
|
||||
cpulist = append(cpulist, c.CpuID)
|
||||
if c.Numadomain == in {
|
||||
cpulist = append(cpulist, c.Cpuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,13 +321,13 @@ func getCpuListOfNumaDomainFunc(args interface{}) (interface{}, error) {
|
||||
}
|
||||
|
||||
// for a given CPU die id, it returns the list of cpuids
|
||||
func getCpuListOfDieFunc(args interface{}) (interface{}, error) {
|
||||
func getCpuListOfDieFunc(args ...interface{}) (interface{}, error) {
|
||||
cpulist := make([]int, 0)
|
||||
switch in := args.(type) {
|
||||
switch in := args[0].(type) {
|
||||
case int:
|
||||
for _, c := range topo.CpuData() {
|
||||
if c.Die == in {
|
||||
cpulist = append(cpulist, c.CpuID)
|
||||
cpulist = append(cpulist, c.Cpuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,8 +335,8 @@ func getCpuListOfDieFunc(args interface{}) (interface{}, error) {
|
||||
}
|
||||
|
||||
// wrapper function to get a list of all cpuids of the node
|
||||
func getCpuListOfNode() (interface{}, error) {
|
||||
return topo.HwthreadList(), nil
|
||||
func getCpuListOfNode(args ...interface{}) (interface{}, error) {
|
||||
return topo.CpuList(), nil
|
||||
}
|
||||
|
||||
// helper function to get the cpuid list for a CCMetric type tag set (type and type-id)
|
||||
@@ -353,14 +348,14 @@ func getCpuListOfType(args ...interface{}) (interface{}, error) {
|
||||
case string:
|
||||
switch typ {
|
||||
case "node":
|
||||
return topo.HwthreadList(), nil
|
||||
return topo.CpuList(), nil
|
||||
case "socket":
|
||||
return getCpuListOfSocketFunc(args[1])
|
||||
case "numadomain":
|
||||
return getCpuListOfNumaDomainFunc(args[1])
|
||||
case "core":
|
||||
return getCpuListOfCoreFunc(args[1])
|
||||
case "hwthread":
|
||||
case "cpu":
|
||||
var cpu int
|
||||
|
||||
switch id := args[1].(type) {
|
||||
|
@@ -52,11 +52,6 @@ The CCMetric router sits in between the collectors and the sinks and can be used
|
||||
],
|
||||
"rename_metrics" : {
|
||||
"metric_12345" : "mymetric"
|
||||
},
|
||||
"normalize_units" : true,
|
||||
"change_unit_prefix" : {
|
||||
"mem_used" : "G",
|
||||
"mem_total" : "G"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -197,14 +192,6 @@ This option takes a list of evaluable conditions and performs them one after the
|
||||
```
|
||||
The first line is comparable with the example in `drop_metrics`, it drops all metrics starting with `drop_metric_` and ending with a number. The second line drops all metrics of the first hardware thread (**not** recommended)
|
||||
|
||||
# Manipulating the metric units
|
||||
|
||||
## The `normalize_units` option
|
||||
The cc-metric-collector tries to read the data from the system as it is reported. If available, it tries to read the metric unit from the system as well (e.g. from `/proc/meminfo`). The problem is that, depending on the source, the metric units are named differently. Just think about `byte`, `Byte`, `B`, `bytes`, ...
|
||||
The [cc-units](https://github.com/ClusterCockpit/cc-units) package provides us a normalization option to use the same metric unit name for all metrics. It this option is set to true, all `unit` meta tags are normalized.
|
||||
|
||||
## The `change_unit_prefix` section
|
||||
It is often the case that metrics are reported by the system using a rather outdated unit prefix (like `/proc/meminfo` still uses kByte despite current memory sizes are in the GByte range). If you want to change the prefix of a unit, you can do that with the help of [cc-units](https://github.com/ClusterCockpit/cc-units). The setting works on the metric name and requires the new prefix for the metric. The cc-units package determines the scaling factor.
|
||||
|
||||
# Aggregate metric values of the current interval with the `interval_aggregates` option
|
||||
|
||||
@@ -252,22 +239,3 @@ Use cases for `interval_aggregates`:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Order of operations
|
||||
|
||||
The router performs the above mentioned options in a specific order. In order to get the logic you want for a specific metric, it is crucial to know the processing order:
|
||||
|
||||
- Add the `hostname` tag (c)
|
||||
- Manipulate the timestamp to the interval timestamp (c,r)
|
||||
- Drop metrics based on `drop_metrics` and `drop_metrics_if` (c,r)
|
||||
- Add tags based on `add_tags` (c,r)
|
||||
- Delete tags based on `del_tags` (c,r)
|
||||
- Rename metric based on `rename_metric` (c,r)
|
||||
- Add tags based on `add_tags` to still work if the configuration uses the new name (c,r)
|
||||
- Delete tags based on `del_tags` to still work if the configuration uses the new name (c,r)
|
||||
- Normalize units when `normalize_units` is set (c,r)
|
||||
- Convert unit prefix based on `change_unit_prefix` (c,r)
|
||||
|
||||
Legend:
|
||||
- 'c' if metric is coming from a collector
|
||||
- 'r' if metric is coming from a receiver
|
||||
|
17
internal/metricRouter/StatsApi.md
Normal file
17
internal/metricRouter/StatsApi.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Stats API
|
||||
|
||||
The Stats API can be used for debugging. It publishes counts at an HTTP endpoint as JSON from different componenets of the CC Metric Collector.
|
||||
|
||||
# Configuration
|
||||
|
||||
The Stats API has an own configuration file to specify the listen host and port. The defaults are `localhost` and `8080`.
|
||||
|
||||
```json
|
||||
{
|
||||
"bindhost" : "",
|
||||
"port" : "8080",
|
||||
"publish_collectorstate" : true
|
||||
}
|
||||
```
|
||||
|
||||
The `bindhost` and `port` can be used to specify the listen host and port. The `publish_collectorstate` needs to be `true`, otherwise nothing is presented. This option is for future use if we need to publish more infos using different domains.
|
232
internal/metricRouter/metricApi.go
Normal file
232
internal/metricRouter/metricApi.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package metricRouter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/internal/multiChanTicker"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type statsApiConfig struct {
|
||||
PublishCollectorState bool `json:"publish_collectorstate"`
|
||||
Host string `json:"bindhost"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
// Metric cache data structure
|
||||
type statsApi struct {
|
||||
name string
|
||||
input chan lp.CCMetric
|
||||
indone chan bool
|
||||
outdone chan bool
|
||||
config statsApiConfig
|
||||
wg *sync.WaitGroup
|
||||
statsWg sync.WaitGroup
|
||||
ticker mct.MultiChanTicker
|
||||
tickchan chan time.Time
|
||||
server *http.Server
|
||||
router *mux.Router
|
||||
lock sync.Mutex
|
||||
baseurl string
|
||||
stats map[string]map[string]int64
|
||||
outStats map[string]map[string]int64
|
||||
}
|
||||
|
||||
type StatsApi interface {
|
||||
Start()
|
||||
Close()
|
||||
StatsFunc(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var statsApiServer *statsApi = nil
|
||||
|
||||
func (a *statsApi) updateStats(point lp.CCMetric) {
|
||||
switch point.Name() {
|
||||
case "_stats":
|
||||
if name, nok := point.GetMeta("source"); nok {
|
||||
var compStats map[string]int64
|
||||
var ok bool
|
||||
|
||||
if compStats, ok = a.stats[name]; !ok {
|
||||
a.stats[name] = make(map[string]int64)
|
||||
compStats = a.stats[name]
|
||||
}
|
||||
for k, v := range point.Fields() {
|
||||
switch value := v.(type) {
|
||||
case int:
|
||||
compStats[k] = int64(value)
|
||||
case uint:
|
||||
compStats[k] = int64(value)
|
||||
case int32:
|
||||
compStats[k] = int64(value)
|
||||
case uint32:
|
||||
compStats[k] = int64(value)
|
||||
case int64:
|
||||
compStats[k] = int64(value)
|
||||
case uint64:
|
||||
compStats[k] = int64(value)
|
||||
default:
|
||||
cclog.ComponentDebug(a.name, "Unusable stats for", k, ". Values should be int64")
|
||||
}
|
||||
}
|
||||
a.stats[name] = compStats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *statsApi) Start() {
|
||||
a.ticker.AddChannel(a.tickchan)
|
||||
a.wg.Add(1)
|
||||
a.statsWg.Add(1)
|
||||
go func() {
|
||||
a.stats = make(map[string]map[string]int64)
|
||||
defer a.statsWg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-a.indone:
|
||||
cclog.ComponentDebug(a.name, "INPUT DONE")
|
||||
close(a.indone)
|
||||
return
|
||||
case p := <-a.input:
|
||||
a.lock.Lock()
|
||||
a.updateStats(p)
|
||||
a.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
a.statsWg.Add(1)
|
||||
go func() {
|
||||
a.outStats = make(map[string]map[string]int64)
|
||||
defer a.statsWg.Done()
|
||||
a.lock.Lock()
|
||||
for comp, compData := range a.stats {
|
||||
var outData map[string]int64
|
||||
var ok bool
|
||||
if outData, ok = a.outStats[comp]; !ok {
|
||||
outData = make(map[string]int64)
|
||||
}
|
||||
for k, v := range compData {
|
||||
outData[k] = v
|
||||
}
|
||||
a.outStats[comp] = outData
|
||||
}
|
||||
a.lock.Unlock()
|
||||
for {
|
||||
select {
|
||||
case <-a.outdone:
|
||||
cclog.ComponentDebug(a.name, "OUTPUT DONE")
|
||||
close(a.outdone)
|
||||
return
|
||||
case <-a.tickchan:
|
||||
a.lock.Lock()
|
||||
for comp, compData := range a.stats {
|
||||
var outData map[string]int64
|
||||
var ok bool
|
||||
if outData, ok = a.outStats[comp]; !ok {
|
||||
outData = make(map[string]int64)
|
||||
}
|
||||
for k, v := range compData {
|
||||
outData[k] = v
|
||||
}
|
||||
a.outStats[comp] = outData
|
||||
}
|
||||
a.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
a.statsWg.Add(1)
|
||||
go func() {
|
||||
defer a.statsWg.Done()
|
||||
err := a.server.ListenAndServe()
|
||||
if err != nil && err.Error() != "http: Server closed" {
|
||||
cclog.ComponentError(a.name, err.Error())
|
||||
}
|
||||
cclog.ComponentDebug(a.name, "SERVER DONE")
|
||||
}()
|
||||
cclog.ComponentDebug(a.name, "STARTED")
|
||||
}
|
||||
|
||||
func (a *statsApi) StatsFunc(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := json.Marshal(a.outStats)
|
||||
if err == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// Close finishes / stops the metric cache
|
||||
func (a *statsApi) Close() {
|
||||
cclog.ComponentDebug(a.name, "CLOSE")
|
||||
a.indone <- true
|
||||
a.outdone <- true
|
||||
a.server.Shutdown(context.Background())
|
||||
// wait for close of channel r.done
|
||||
<-a.indone
|
||||
<-a.outdone
|
||||
a.statsWg.Wait()
|
||||
a.wg.Done()
|
||||
|
||||
//a.wg.Wait()
|
||||
}
|
||||
|
||||
func NewStatsApi(ticker mct.MultiChanTicker, wg *sync.WaitGroup, statsApiConfigfile string) (StatsApi, error) {
|
||||
a := new(statsApi)
|
||||
a.name = "StatsApi"
|
||||
a.config.Host = "localhost"
|
||||
a.config.Port = "8080"
|
||||
configFile, err := os.Open(statsApiConfigfile)
|
||||
if err != nil {
|
||||
cclog.ComponentError(a.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
defer configFile.Close()
|
||||
jsonParser := json.NewDecoder(configFile)
|
||||
err = jsonParser.Decode(&a.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(a.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
a.input = make(chan lp.CCMetric)
|
||||
a.ticker = ticker
|
||||
a.tickchan = make(chan time.Time)
|
||||
a.wg = wg
|
||||
a.indone = make(chan bool)
|
||||
a.outdone = make(chan bool)
|
||||
a.router = mux.NewRouter()
|
||||
a.baseurl = fmt.Sprintf("%s:%s", a.config.Host, a.config.Port)
|
||||
a.server = &http.Server{Addr: a.baseurl, Handler: a.router}
|
||||
if a.config.PublishCollectorState {
|
||||
a.router.HandleFunc("/", a.StatsFunc)
|
||||
}
|
||||
statsApiServer = a
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func ComponentStatInt(component string, key string, value int64) {
|
||||
if statsApiServer == nil {
|
||||
return
|
||||
}
|
||||
y, err := lp.New("_stats", map[string]string{}, map[string]string{"source": component}, map[string]interface{}{key: value}, time.Now())
|
||||
if err == nil {
|
||||
statsApiServer.input <- y
|
||||
}
|
||||
}
|
||||
|
||||
func ComponentStatString(component string, key string, value int64) {
|
||||
if statsApiServer == nil {
|
||||
return
|
||||
}
|
||||
y, err := lp.New("_stats", map[string]string{}, map[string]string{"source": component}, map[string]interface{}{key: value}, time.Now())
|
||||
if err == nil {
|
||||
statsApiServer.input <- y
|
||||
}
|
||||
}
|
@@ -4,11 +4,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
agg "github.com/ClusterCockpit/cc-metric-collector/internal/metricAggregator"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/pkg/multiChanTicker"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/internal/multiChanTicker"
|
||||
)
|
||||
|
||||
type metricCachePeriod struct {
|
||||
|
@@ -7,12 +7,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
agg "github.com/ClusterCockpit/cc-metric-collector/internal/metricAggregator"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/pkg/multiChanTicker"
|
||||
units "github.com/ClusterCockpit/cc-units"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/internal/multiChanTicker"
|
||||
)
|
||||
|
||||
const ROUTER_MAX_FORWARD = 50
|
||||
@@ -36,26 +35,31 @@ type metricRouterConfig struct {
|
||||
IntervalStamp bool `json:"interval_timestamp"` // Update timestamp periodically by ticker each interval?
|
||||
NumCacheIntervals int `json:"num_cache_intervals"` // Number of intervals of cached metrics for evaluation
|
||||
MaxForward int `json:"max_forward"` // Number of maximal forwarded metrics at one select
|
||||
NormalizeUnits bool `json:"normalize_units"` // Check unit meta flag and normalize it using cc-units
|
||||
ChangeUnitPrefix map[string]string `json:"change_unit_prefix"` // Add prefix that should be applied to the metrics
|
||||
dropMetrics map[string]bool // Internal map for O(1) lookup
|
||||
}
|
||||
|
||||
// Metric router data structure
|
||||
type metricRouter struct {
|
||||
hostname string // Hostname used in tags
|
||||
coll_input chan lp.CCMetric // Input channel from CollectorManager
|
||||
recv_input chan lp.CCMetric // Input channel from ReceiveManager
|
||||
cache_input chan lp.CCMetric // Input channel from MetricCache
|
||||
outputs []chan lp.CCMetric // List of all output channels
|
||||
done chan bool // channel to finish / stop metric router
|
||||
wg *sync.WaitGroup // wait group for all goroutines in cc-metric-collector
|
||||
timestamp time.Time // timestamp periodically updated by ticker each interval
|
||||
ticker mct.MultiChanTicker // periodically ticking once each interval
|
||||
config metricRouterConfig // json encoded config for metric router
|
||||
cache MetricCache // pointer to MetricCache
|
||||
cachewg sync.WaitGroup // wait group for MetricCache
|
||||
maxForward int // number of metrics to forward maximally in one iteration
|
||||
hostname string // Hostname used in tags
|
||||
coll_input chan lp.CCMetric // Input channel from CollectorManager
|
||||
recv_input chan lp.CCMetric // Input channel from ReceiveManager
|
||||
cache_input chan lp.CCMetric // Input channel from MetricCache
|
||||
outputs []chan lp.CCMetric // List of all output channels
|
||||
done chan bool // channel to finish / stop metric router
|
||||
wg *sync.WaitGroup // wait group for all goroutines in cc-metric-collector
|
||||
timestamp time.Time // timestamp periodically updated by ticker each interval
|
||||
timerdone chan bool // channel to finish / stop timestamp updater
|
||||
ticker mct.MultiChanTicker // periodically ticking once each interval
|
||||
config metricRouterConfig // json encoded config for metric router
|
||||
cache MetricCache // pointer to MetricCache
|
||||
cachewg sync.WaitGroup // wait group for MetricCache
|
||||
maxForward int // number of metrics to forward maximally in one iteration
|
||||
statsCollForward int64
|
||||
statsRecvForward int64
|
||||
statsCacheForward int64
|
||||
statsTotalForward int64
|
||||
statsDropped int64
|
||||
statsRenamed int64
|
||||
}
|
||||
|
||||
// MetricRouter access functions
|
||||
@@ -123,9 +127,39 @@ func (r *metricRouter) Init(ticker mct.MultiChanTicker, wg *sync.WaitGroup, rout
|
||||
for _, mname := range r.config.DropMetrics {
|
||||
r.config.dropMetrics[mname] = true
|
||||
}
|
||||
r.statsCollForward = 0
|
||||
r.statsRecvForward = 0
|
||||
r.statsCacheForward = 0
|
||||
r.statsTotalForward = 0
|
||||
r.statsDropped = 0
|
||||
r.statsRenamed = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartTimer starts a timer which updates timestamp periodically
|
||||
func (r *metricRouter) StartTimer() {
|
||||
m := make(chan time.Time)
|
||||
r.ticker.AddChannel(m)
|
||||
r.timerdone = make(chan bool)
|
||||
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-r.timerdone:
|
||||
close(r.timerdone)
|
||||
cclog.ComponentDebug("MetricRouter", "TIMER DONE")
|
||||
return
|
||||
case t := <-m:
|
||||
cclog.ComponentDebug("MetricRouter", "INTERVAL_TICK", t.Unix())
|
||||
r.timestamp = t
|
||||
}
|
||||
}
|
||||
}()
|
||||
cclog.ComponentDebug("MetricRouter", "TIMER START")
|
||||
}
|
||||
|
||||
func getParamMap(point lp.CCMetric) map[string]interface{} {
|
||||
params := make(map[string]interface{})
|
||||
params["metric"] = point
|
||||
@@ -210,45 +244,12 @@ func (r *metricRouter) dropMetric(point lp.CCMetric) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *metricRouter) prepareUnit(point lp.CCMetric) bool {
|
||||
if r.config.NormalizeUnits {
|
||||
if in_unit, ok := point.GetMeta("unit"); ok {
|
||||
u := units.NewUnit(in_unit)
|
||||
if u.Valid() {
|
||||
point.AddMeta("unit", u.Short())
|
||||
}
|
||||
}
|
||||
}
|
||||
if newP, ok := r.config.ChangeUnitPrefix[point.Name()]; ok {
|
||||
|
||||
newPrefix := units.NewPrefix(newP)
|
||||
|
||||
if in_unit, ok := point.GetMeta("unit"); ok && newPrefix != units.InvalidPrefix {
|
||||
u := units.NewUnit(in_unit)
|
||||
if u.Valid() {
|
||||
cclog.ComponentDebug("MetricRouter", "Change prefix to", newP, "for metric", point.Name())
|
||||
conv, out_unit := units.GetUnitPrefixFactor(u, newPrefix)
|
||||
if conv != nil && out_unit.Valid() {
|
||||
if val, ok := point.GetField("value"); ok {
|
||||
point.AddField("value", conv(val))
|
||||
point.AddMeta("unit", out_unit.Short())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Start starts the metric router
|
||||
func (r *metricRouter) Start() {
|
||||
// start timer if configured
|
||||
r.timestamp = time.Now()
|
||||
timeChan := make(chan time.Time)
|
||||
if r.config.IntervalStamp {
|
||||
r.ticker.AddChannel(timeChan)
|
||||
r.StartTimer()
|
||||
}
|
||||
|
||||
// Router manager is done
|
||||
@@ -265,13 +266,13 @@ func (r *metricRouter) Start() {
|
||||
r.DoDelTags(point)
|
||||
name := point.Name()
|
||||
if new, ok := r.config.RenameMetrics[name]; ok {
|
||||
r.statsRenamed++
|
||||
ComponentStatInt("MetricRouter", "renamed", r.statsRenamed)
|
||||
point.SetName(new)
|
||||
point.AddMeta("oldname", name)
|
||||
r.DoAddTags(point)
|
||||
r.DoDelTags(point)
|
||||
}
|
||||
|
||||
r.prepareUnit(point)
|
||||
r.DoAddTags(point)
|
||||
r.DoDelTags(point)
|
||||
|
||||
for _, o := range r.outputs {
|
||||
o <- point
|
||||
@@ -286,7 +287,14 @@ func (r *metricRouter) Start() {
|
||||
p.SetTime(r.timestamp)
|
||||
}
|
||||
if !r.dropMetric(p) {
|
||||
r.statsCollForward++
|
||||
r.statsTotalForward++
|
||||
ComponentStatInt("MetricRouter", "collector_forward", r.statsCollForward)
|
||||
ComponentStatInt("MetricRouter", "total_forward", r.statsTotalForward)
|
||||
forward(p)
|
||||
} else {
|
||||
r.statsDropped++
|
||||
ComponentStatInt("MetricRouter", "dropped", r.statsDropped)
|
||||
}
|
||||
// even if the metric is dropped, it is stored in the cache for
|
||||
// aggregations
|
||||
@@ -302,7 +310,14 @@ func (r *metricRouter) Start() {
|
||||
p.SetTime(r.timestamp)
|
||||
}
|
||||
if !r.dropMetric(p) {
|
||||
r.statsRecvForward++
|
||||
r.statsTotalForward++
|
||||
ComponentStatInt("MetricRouter", "receiver_forward", r.statsRecvForward)
|
||||
ComponentStatInt("MetricRouter", "total_forward", r.statsTotalForward)
|
||||
forward(p)
|
||||
} else {
|
||||
r.statsDropped++
|
||||
ComponentStatInt("MetricRouter", "dropped", r.statsDropped)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +326,14 @@ func (r *metricRouter) Start() {
|
||||
// receive from metric collector
|
||||
if !r.dropMetric(p) {
|
||||
p.AddTag(r.config.HostnameTagName, r.hostname)
|
||||
r.statsCacheForward++
|
||||
r.statsTotalForward++
|
||||
ComponentStatInt("MetricRouter", "cache_forward", r.statsCacheForward)
|
||||
ComponentStatInt("MetricRouter", "total_forward", r.statsTotalForward)
|
||||
forward(p)
|
||||
} else {
|
||||
r.statsDropped++
|
||||
ComponentStatInt("MetricRouter", "dropped", r.statsDropped)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,10 +352,6 @@ func (r *metricRouter) Start() {
|
||||
done()
|
||||
return
|
||||
|
||||
case timestamp := <-timeChan:
|
||||
r.timestamp = timestamp
|
||||
cclog.ComponentDebug("MetricRouter", "Update timestamp", r.timestamp.UnixNano())
|
||||
|
||||
case p := <-r.coll_input:
|
||||
coll_forward(p)
|
||||
for i := 0; len(r.coll_input) > 0 && i < (r.maxForward-1); i++ {
|
||||
@@ -379,6 +397,14 @@ func (r *metricRouter) Close() {
|
||||
// wait for close of channel r.done
|
||||
<-r.done
|
||||
|
||||
// stop timer
|
||||
if r.config.IntervalStamp {
|
||||
cclog.ComponentDebug("MetricRouter", "TIMER CLOSE")
|
||||
r.timerdone <- true
|
||||
// wait for close of channel r.timerdone
|
||||
<-r.timerdone
|
||||
}
|
||||
|
||||
// stop metric cache
|
||||
if r.config.NumCacheIntervals > 0 {
|
||||
cclog.ComponentDebug("MetricRouter", "CACHE CLOSE")
|
||||
|
@@ -3,7 +3,7 @@ package multiChanTicker
|
||||
import (
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
)
|
||||
|
||||
type multiChanTicker struct {
|
@@ -1,57 +0,0 @@
|
||||
# ClusterCockpit metrics
|
||||
|
||||
As described in the [ClusterCockpit specifications](https://github.com/ClusterCockpit/cc-specifications), the whole ClusterCockpit stack uses metrics in the InfluxDB line protocol format. This is also the input and output format for the ClusterCockpit Metric Collector but internally it uses an extended format while processing, named CCMetric.
|
||||
|
||||
It is basically a copy of the [InfluxDB line protocol](https://github.com/influxdata/line-protocol) `MutableMetric` interface with one extension. Besides the tags and fields, it contains a list of meta information (re-using the `Tag` structure of the original protocol):
|
||||
|
||||
```golang
|
||||
type ccMetric struct {
|
||||
name string // Measurement name
|
||||
meta map[string]string // map of meta data tags
|
||||
tags map[string]string // map of of tags
|
||||
fields map[string]interface{} // map of of fields
|
||||
tm time.Time // timestamp
|
||||
}
|
||||
|
||||
type CCMetric interface {
|
||||
ToPoint(metaAsTags map[string]bool) *write.Point // Generate influxDB point for data type ccMetric
|
||||
ToLineProtocol(metaAsTags map[string]bool) string // Generate influxDB line protocol for data type ccMetric
|
||||
String() string // Return line-protocol like string
|
||||
|
||||
Name() string // Get metric name
|
||||
SetName(name string) // Set metric name
|
||||
|
||||
Time() time.Time // Get timestamp
|
||||
SetTime(t time.Time) // Set timestamp
|
||||
|
||||
Tags() map[string]string // Map of tags
|
||||
AddTag(key, value string) // Add a tag
|
||||
GetTag(key string) (value string, ok bool) // Get a tag by its key
|
||||
HasTag(key string) (ok bool) // Check if a tag key is present
|
||||
RemoveTag(key string) // Remove a tag by its key
|
||||
|
||||
Meta() map[string]string // Map of meta data tags
|
||||
AddMeta(key, value string) // Add a meta data tag
|
||||
GetMeta(key string) (value string, ok bool) // Get a meta data tab addressed by its key
|
||||
HasMeta(key string) (ok bool) // Check if a meta data key is present
|
||||
RemoveMeta(key string) // Remove a meta data tag by its key
|
||||
|
||||
Fields() map[string]interface{} // Map of fields
|
||||
AddField(key string, value interface{}) // Add a field
|
||||
GetField(key string) (value interface{}, ok bool) // Get a field addressed by its key
|
||||
HasField(key string) (ok bool) // Check if a field key is present
|
||||
RemoveField(key string) // Remove a field addressed by its key
|
||||
}
|
||||
|
||||
func New(name string, tags map[string]string, meta map[string]string, fields map[string]interface{}, tm time.Time) (CCMetric, error)
|
||||
func FromMetric(other CCMetric) CCMetric
|
||||
func FromInfluxMetric(other lp.Metric) CCMetric
|
||||
```
|
||||
|
||||
The `CCMetric` interface provides the same functions as the `MutableMetric` like `{Add, Get, Remove, Has}{Tag, Field}` and additionally provides `{Add, Get, Remove, Has}Meta`.
|
||||
|
||||
The InfluxDB protocol creates a new metric with `influx.New(name, tags, fields, time)` while CCMetric uses `ccMetric.New(name, tags, meta, fields, time)` where `tags` and `meta` are both of type `map[string]string`.
|
||||
|
||||
You can copy a CCMetric with `FromMetric(other CCMetric) CCMetric`. If you get an `influx.Metric` from a function, like the line protocol parser, you can use `FromInfluxMetric(other influx.Metric) CCMetric` to get a CCMetric out of it (see `NatsReceiver` for an example).
|
||||
|
||||
Although the [cc-specifications](https://github.com/ClusterCockpit/cc-specifications/blob/master/interfaces/lineprotocol/README.md) defines that there is only a `value` field for the metric value, the CCMetric still can have multiple values similar to the InfluxDB line protocol.
|
@@ -1,425 +0,0 @@
|
||||
package ccTopology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
cclogger "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const SYSFS_CPUBASE = `/sys/devices/system/cpu`
|
||||
|
||||
// Structure holding all information about a hardware thread
|
||||
// See https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-devices-system-cpu
|
||||
type HwthreadEntry struct {
|
||||
// for each CPUx:
|
||||
CpuID int // CPU / hardware thread ID
|
||||
SMT int // Simultaneous Multithreading ID
|
||||
CoreCPUsList []int // CPUs within the same core
|
||||
Core int // Socket local core ID
|
||||
Socket int // Sockets (physical) ID
|
||||
Die int // Die ID
|
||||
NumaDomain int // NUMA Domain
|
||||
}
|
||||
|
||||
var cache struct {
|
||||
HwthreadList []int // List of CPU hardware threads
|
||||
SMTList []int // List of symmetric hyper threading IDs
|
||||
CoreList []int // List of CPU core IDs
|
||||
SocketList []int // List of CPU sockets (physical) IDs
|
||||
DieList []int // List of CPU Die IDs
|
||||
NumaDomainList []int // List of NUMA Domains
|
||||
|
||||
CpuData []HwthreadEntry
|
||||
}
|
||||
|
||||
// fileToInt reads an integer value from a sysfs file
|
||||
// In case of an error -1 is returned
|
||||
func fileToInt(path string) int {
|
||||
buffer, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
cclogger.ComponentError("ccTopology", "fileToInt", "Reading", path, ":", err.Error())
|
||||
return -1
|
||||
}
|
||||
stringBuffer := strings.TrimSpace(string(buffer))
|
||||
id, err := strconv.Atoi(stringBuffer)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("ccTopology", "fileToInt", "Parsing", path, ":", stringBuffer, err.Error())
|
||||
return -1
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// fileToList reads a list from a sysfs file
|
||||
// A list consists of value ranges separated by colon
|
||||
// A range can be a single value or a range of values given by a startValue-endValue
|
||||
// In case of an error nil is returned
|
||||
func fileToList(path string) []int {
|
||||
// Read list
|
||||
buffer, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
cclogger.ComponentError("ccTopology", "fileToList", "Reading", path, ":", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create list
|
||||
list := make([]int, 0)
|
||||
stringBuffer := strings.TrimSpace(string(buffer))
|
||||
for _, valueRangeString := range strings.Split(stringBuffer, ",") {
|
||||
valueRange := strings.Split(valueRangeString, "-")
|
||||
switch len(valueRange) {
|
||||
case 1:
|
||||
singleValue, err := strconv.Atoi(valueRange[0])
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "fileToList", "Parsing", valueRange[0], ":", err.Error())
|
||||
return nil
|
||||
}
|
||||
list = append(list, singleValue)
|
||||
case 2:
|
||||
startValue, err := strconv.Atoi(valueRange[0])
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "fileToList", "Parsing", valueRange[0], ":", err.Error())
|
||||
return nil
|
||||
}
|
||||
endValue, err := strconv.Atoi(valueRange[1])
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "fileToList", "Parsing", valueRange[1], ":", err.Error())
|
||||
return nil
|
||||
}
|
||||
for value := startValue; value <= endValue; value++ {
|
||||
list = append(list, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// init initializes the cache structure
|
||||
func init() {
|
||||
|
||||
getHWThreads :=
|
||||
func() []int {
|
||||
globPath := filepath.Join(SYSFS_CPUBASE, "cpu[0-9]*")
|
||||
regexPath := filepath.Join(SYSFS_CPUBASE, "cpu([[:digit:]]+)")
|
||||
regex := regexp.MustCompile(regexPath)
|
||||
|
||||
// File globbing for hardware threads
|
||||
files, err := filepath.Glob(globPath)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "init:getHWThreads", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
hwThreadIDs := make([]int, len(files))
|
||||
for i, file := range files {
|
||||
// Extract hardware thread ID
|
||||
matches := regex.FindStringSubmatch(file)
|
||||
if len(matches) != 2 {
|
||||
cclogger.ComponentError("CCTopology", "init:getHWThreads: Failed to extract hardware thread ID from ", file)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert hardware thread ID to int
|
||||
id, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "init:getHWThreads: Failed to convert to int hardware thread ID ", matches[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
hwThreadIDs[i] = id
|
||||
}
|
||||
|
||||
// Sort hardware thread IDs
|
||||
slices.Sort(hwThreadIDs)
|
||||
return hwThreadIDs
|
||||
}
|
||||
|
||||
getNumaDomain :=
|
||||
func(basePath string) int {
|
||||
globPath := filepath.Join(basePath, "node*")
|
||||
regexPath := filepath.Join(basePath, "node([[:digit:]]+)")
|
||||
regex := regexp.MustCompile(regexPath)
|
||||
|
||||
// File globbing for NUMA node
|
||||
files, err := filepath.Glob(globPath)
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "init:getNumaDomain", err.Error())
|
||||
return -1
|
||||
}
|
||||
|
||||
// Check, that exactly one NUMA domain was found
|
||||
if len(files) != 1 {
|
||||
cclogger.ComponentError("CCTopology", "init:getNumaDomain", "Number of NUMA domains != 1: ", len(files))
|
||||
return -1
|
||||
}
|
||||
|
||||
// Extract NUMA node ID
|
||||
matches := regex.FindStringSubmatch(files[0])
|
||||
if len(matches) != 2 {
|
||||
cclogger.ComponentError("CCTopology", "init:getNumaDomain", "Failed to extract NUMA node ID from: ", files[0])
|
||||
return -1
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
cclogger.ComponentError("CCTopology", "init:getNumaDomain", "Failed to parse NUMA node ID from: ", matches[1])
|
||||
return -1
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
cache.HwthreadList = getHWThreads()
|
||||
cache.CoreList = make([]int, len(cache.HwthreadList))
|
||||
cache.SocketList = make([]int, len(cache.HwthreadList))
|
||||
cache.DieList = make([]int, len(cache.HwthreadList))
|
||||
cache.SMTList = make([]int, len(cache.HwthreadList))
|
||||
cache.NumaDomainList = make([]int, len(cache.HwthreadList))
|
||||
cache.CpuData = make([]HwthreadEntry, len(cache.HwthreadList))
|
||||
for i, c := range cache.HwthreadList {
|
||||
// Set cpuBase directory for topology lookup
|
||||
cpuBase := filepath.Join(SYSFS_CPUBASE, fmt.Sprintf("cpu%d", c))
|
||||
topoBase := filepath.Join(cpuBase, "topology")
|
||||
|
||||
// Lookup Core ID
|
||||
cache.CoreList[i] = fileToInt(filepath.Join(topoBase, "core_id"))
|
||||
|
||||
// Lookup socket / physical package ID
|
||||
cache.SocketList[i] = fileToInt(filepath.Join(topoBase, "physical_package_id"))
|
||||
|
||||
// Lookup CPU die id
|
||||
cache.DieList[i] = fileToInt(filepath.Join(topoBase, "die_id"))
|
||||
if cache.DieList[i] < 0 {
|
||||
cache.DieList[i] = cache.SocketList[i]
|
||||
}
|
||||
|
||||
// Lookup List of CPUs within the same core
|
||||
coreCPUsList := fileToList(filepath.Join(topoBase, "core_cpus_list"))
|
||||
|
||||
// Find index of CPU ID in List of CPUs within the same core
|
||||
// if not found return -1
|
||||
cache.SMTList[i] = slices.Index(coreCPUsList, c)
|
||||
|
||||
// Lookup NUMA domain id
|
||||
cache.NumaDomainList[i] = getNumaDomain(cpuBase)
|
||||
|
||||
cache.CpuData[i] =
|
||||
HwthreadEntry{
|
||||
CpuID: cache.HwthreadList[i],
|
||||
SMT: cache.SMTList[i],
|
||||
CoreCPUsList: coreCPUsList,
|
||||
Socket: cache.SocketList[i],
|
||||
NumaDomain: cache.NumaDomainList[i],
|
||||
Die: cache.DieList[i],
|
||||
Core: cache.CoreList[i],
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(cache.HwthreadList)
|
||||
cache.HwthreadList = slices.Compact(cache.HwthreadList)
|
||||
|
||||
slices.Sort(cache.SMTList)
|
||||
cache.SMTList = slices.Compact(cache.SMTList)
|
||||
|
||||
slices.Sort(cache.CoreList)
|
||||
cache.CoreList = slices.Compact(cache.CoreList)
|
||||
|
||||
slices.Sort(cache.SocketList)
|
||||
cache.SocketList = slices.Compact(cache.SocketList)
|
||||
|
||||
slices.Sort(cache.DieList)
|
||||
cache.DieList = slices.Compact(cache.DieList)
|
||||
|
||||
slices.Sort(cache.NumaDomainList)
|
||||
cache.NumaDomainList = slices.Compact(cache.NumaDomainList)
|
||||
}
|
||||
|
||||
// SocketList gets the list of CPU socket IDs
|
||||
func SocketList() []int {
|
||||
return slices.Clone(cache.SocketList)
|
||||
}
|
||||
|
||||
// HwthreadList gets the list of hardware thread IDs in the order of listing in /proc/cpuinfo
|
||||
func HwthreadList() []int {
|
||||
return slices.Clone(cache.HwthreadList)
|
||||
}
|
||||
|
||||
// Get list of hardware thread IDs in the order of listing in /proc/cpuinfo
|
||||
// Deprecated! Use HwthreadList()
|
||||
func CpuList() []int {
|
||||
return HwthreadList()
|
||||
}
|
||||
|
||||
// CoreList gets the list of CPU core IDs in the order of listing in /proc/cpuinfo
|
||||
func CoreList() []int {
|
||||
return slices.Clone(cache.CoreList)
|
||||
}
|
||||
|
||||
// Get list of NUMA node IDs
|
||||
func NumaNodeList() []int {
|
||||
return slices.Clone(cache.NumaDomainList)
|
||||
}
|
||||
|
||||
// DieList gets the list of CPU die IDs
|
||||
func DieList() []int {
|
||||
if len(cache.DieList) > 0 {
|
||||
return slices.Clone(cache.DieList)
|
||||
}
|
||||
return SocketList()
|
||||
}
|
||||
|
||||
// GetTypeList gets the list of specified type using the naming format inside ClusterCockpit
|
||||
func GetTypeList(topology_type string) []int {
|
||||
switch topology_type {
|
||||
case "node":
|
||||
return []int{0}
|
||||
case "socket":
|
||||
return SocketList()
|
||||
case "die":
|
||||
return DieList()
|
||||
case "memoryDomain":
|
||||
return NumaNodeList()
|
||||
case "core":
|
||||
return CoreList()
|
||||
case "hwthread":
|
||||
return HwthreadList()
|
||||
}
|
||||
return []int{}
|
||||
}
|
||||
|
||||
// CpuData returns CPU data for each hardware thread
|
||||
func CpuData() []HwthreadEntry {
|
||||
// return a deep copy to protect cache data
|
||||
c := slices.Clone(cache.CpuData)
|
||||
for i := range c {
|
||||
c[i].CoreCPUsList = slices.Clone(cache.CpuData[i].CoreCPUsList)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Structure holding basic information about a CPU
|
||||
type CpuInformation struct {
|
||||
NumHWthreads int
|
||||
SMTWidth int
|
||||
NumSockets int
|
||||
NumDies int
|
||||
NumCores int
|
||||
NumNumaDomains int
|
||||
}
|
||||
|
||||
// CpuInformation reports basic information about the CPU
|
||||
func CpuInfo() CpuInformation {
|
||||
return CpuInformation{
|
||||
NumNumaDomains: len(cache.NumaDomainList),
|
||||
SMTWidth: len(cache.SMTList),
|
||||
NumDies: len(cache.DieList),
|
||||
NumCores: len(cache.CoreList),
|
||||
NumSockets: len(cache.SocketList),
|
||||
NumHWthreads: len(cache.HwthreadList),
|
||||
}
|
||||
}
|
||||
|
||||
// GetHwthreadSocket gets the CPU socket ID for a given hardware thread ID
|
||||
// In case hardware thread ID is not found -1 is returned
|
||||
func GetHwthreadSocket(cpuID int) int {
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.CpuID == cpuID {
|
||||
return d.Socket
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// GetHwthreadNumaDomain gets the NUMA domain ID for a given hardware thread ID
|
||||
// In case hardware thread ID is not found -1 is returned
|
||||
func GetHwthreadNumaDomain(cpuID int) int {
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.CpuID == cpuID {
|
||||
return d.NumaDomain
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// GetHwthreadDie gets the CPU die ID for a given hardware thread ID
|
||||
// In case hardware thread ID is not found -1 is returned
|
||||
func GetHwthreadDie(cpuID int) int {
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.CpuID == cpuID {
|
||||
return d.Die
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// GetHwthreadCore gets the CPU core ID for a given hardware thread ID
|
||||
// In case hardware thread ID is not found -1 is returned
|
||||
func GetHwthreadCore(cpuID int) int {
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.CpuID == cpuID {
|
||||
return d.Core
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// GetSocketHwthreads gets all hardware thread IDs associated with a CPU socket
|
||||
func GetSocketHwthreads(socket int) []int {
|
||||
cpuList := make([]int, 0)
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.Socket == socket {
|
||||
cpuList = append(cpuList, d.CpuID)
|
||||
}
|
||||
}
|
||||
return cpuList
|
||||
}
|
||||
|
||||
// GetNumaDomainHwthreads gets the all hardware thread IDs associated with a NUMA domain
|
||||
func GetNumaDomainHwthreads(numaDomain int) []int {
|
||||
cpuList := make([]int, 0)
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.NumaDomain == numaDomain {
|
||||
cpuList = append(cpuList, d.CpuID)
|
||||
}
|
||||
}
|
||||
return cpuList
|
||||
}
|
||||
|
||||
// GetDieHwthreads gets all hardware thread IDs associated with a CPU die
|
||||
func GetDieHwthreads(die int) []int {
|
||||
cpuList := make([]int, 0)
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.Die == die {
|
||||
cpuList = append(cpuList, d.CpuID)
|
||||
}
|
||||
}
|
||||
return cpuList
|
||||
}
|
||||
|
||||
// GetCoreHwthreads get all hardware thread IDs associated with a CPU core
|
||||
func GetCoreHwthreads(core int) []int {
|
||||
cpuList := make([]int, 0)
|
||||
for i := range cache.CpuData {
|
||||
d := &cache.CpuData[i]
|
||||
if d.Core == core {
|
||||
cpuList = append(cpuList, d.CpuID)
|
||||
}
|
||||
}
|
||||
return cpuList
|
||||
}
|
@@ -1,125 +0,0 @@
|
||||
package hostlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Expand(in string) (result []string, err error) {
|
||||
|
||||
// Create ranges regular expression
|
||||
reStNumber := "[[:digit:]]+"
|
||||
reStRange := reStNumber + "-" + reStNumber
|
||||
reStOptionalNumberOrRange := "(" + reStNumber + ",|" + reStRange + ",)*"
|
||||
reStNumberOrRange := "(" + reStNumber + "|" + reStRange + ")"
|
||||
reStBraceLeft := "[[]"
|
||||
reStBraceRight := "[]]"
|
||||
reStRanges := reStBraceLeft +
|
||||
reStOptionalNumberOrRange +
|
||||
reStNumberOrRange +
|
||||
reStBraceRight
|
||||
reRanges := regexp.MustCompile(reStRanges)
|
||||
|
||||
// Create host list regular expression
|
||||
reStDNSChars := "[a-zA-Z0-9-]+"
|
||||
reStPrefix := "^(" + reStDNSChars + ")"
|
||||
reStOptionalSuffix := "(" + reStDNSChars + ")?"
|
||||
re := regexp.MustCompile(reStPrefix + "([[][0-9,-]+[]])?" + reStOptionalSuffix)
|
||||
|
||||
// Remove all delimiters from the input
|
||||
in = strings.TrimLeft(in, ", ")
|
||||
|
||||
for len(in) > 0 {
|
||||
if v := re.FindStringSubmatch(in); v != nil {
|
||||
|
||||
// Remove matched part from the input
|
||||
lenPrefix := len(v[0])
|
||||
in = in[lenPrefix:]
|
||||
|
||||
// Remove all delimiters from the input
|
||||
in = strings.TrimLeft(in, ", ")
|
||||
|
||||
// matched prefix, range and suffix
|
||||
hlPrefix := v[1]
|
||||
hlRanges := v[2]
|
||||
hlSuffix := v[3]
|
||||
|
||||
// Single node without ranges
|
||||
if hlRanges == "" {
|
||||
result = append(result, hlPrefix)
|
||||
continue
|
||||
}
|
||||
|
||||
// Node with ranges
|
||||
if v := reRanges.FindStringSubmatch(hlRanges); v != nil {
|
||||
|
||||
// Remove braces
|
||||
hlRanges = hlRanges[1 : len(hlRanges)-1]
|
||||
|
||||
// Split host ranges at ,
|
||||
for _, hlRange := range strings.Split(hlRanges, ",") {
|
||||
|
||||
// Split host range at -
|
||||
RangeStartEnd := strings.Split(hlRange, "-")
|
||||
|
||||
// Range is only a single number
|
||||
if len(RangeStartEnd) == 1 {
|
||||
result = append(result, hlPrefix+RangeStartEnd[0]+hlSuffix)
|
||||
continue
|
||||
}
|
||||
|
||||
// Range has a start and an end
|
||||
widthRangeStart := len(RangeStartEnd[0])
|
||||
widthRangeEnd := len(RangeStartEnd[1])
|
||||
iStart, _ := strconv.ParseUint(RangeStartEnd[0], 10, 64)
|
||||
iEnd, _ := strconv.ParseUint(RangeStartEnd[1], 10, 64)
|
||||
if iStart > iEnd {
|
||||
return nil, fmt.Errorf("single range start is greater than end: %s", hlRange)
|
||||
}
|
||||
|
||||
// Create print format string for range numbers
|
||||
doPadding := widthRangeStart == widthRangeEnd
|
||||
widthPadding := widthRangeStart
|
||||
var formatString string
|
||||
if doPadding {
|
||||
formatString = "%0" + fmt.Sprint(widthPadding) + "d"
|
||||
} else {
|
||||
formatString = "%d"
|
||||
}
|
||||
formatString = hlPrefix + formatString + hlSuffix
|
||||
|
||||
// Add nodes from this range
|
||||
for i := iStart; i <= iEnd; i++ {
|
||||
result = append(result, fmt.Sprintf(formatString, i))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("not at hostlist range: %s", hlRanges)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("not a hostlist: %s", in)
|
||||
}
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
// sort
|
||||
sort.Strings(result)
|
||||
|
||||
// uniq
|
||||
previous := 1
|
||||
for current := 1; current < len(result); current++ {
|
||||
if result[current-1] != result[current] {
|
||||
if previous != current {
|
||||
result[previous] = result[current]
|
||||
}
|
||||
previous++
|
||||
}
|
||||
}
|
||||
result = result[:previous]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@@ -1,126 +0,0 @@
|
||||
package hostlist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpand(t *testing.T) {
|
||||
|
||||
// Compare two slices of strings
|
||||
equal := func(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type testDefinition struct {
|
||||
input string
|
||||
resultExpected []string
|
||||
errorExpected bool
|
||||
}
|
||||
|
||||
expandTests := []testDefinition{
|
||||
{
|
||||
// Single node
|
||||
input: "n1",
|
||||
resultExpected: []string{"n1"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Single node, duplicated
|
||||
input: "n1,n1",
|
||||
resultExpected: []string{"n1"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Single node with padding
|
||||
input: "n[01]",
|
||||
resultExpected: []string{"n01"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Single node with suffix
|
||||
input: "n[01]-p",
|
||||
resultExpected: []string{"n01-p"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Multiple nodes with a single range
|
||||
input: "n[1-2]",
|
||||
resultExpected: []string{"n1", "n2"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Multiple nodes with a single range and a single index
|
||||
input: "n[1-2,3]",
|
||||
resultExpected: []string{"n1", "n2", "n3"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Multiple nodes with different prefixes
|
||||
input: "n[1-2],m[1,2]",
|
||||
resultExpected: []string{"m1", "m2", "n1", "n2"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Multiple nodes with different suffixes
|
||||
input: "n[1-2]-p,n[1,2]-q",
|
||||
resultExpected: []string{"n1-p", "n1-q", "n2-p", "n2-q"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Multiple nodes with and without node ranges
|
||||
input: " n09, n[01-04,06-07,09] , , n10,n04",
|
||||
resultExpected: []string{"n01", "n02", "n03", "n04", "n06", "n07", "n09", "n10"},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
// Forbidden DNS character
|
||||
input: "n@",
|
||||
resultExpected: []string{},
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
// Forbidden range
|
||||
input: "n[1-2-2,3]",
|
||||
resultExpected: []string{},
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
// Forbidden range limits
|
||||
input: "n[2-1]",
|
||||
resultExpected: []string{},
|
||||
errorExpected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, expandTest := range expandTests {
|
||||
result, err := Expand(expandTest.input)
|
||||
|
||||
hasError := err != nil
|
||||
if hasError != expandTest.errorExpected && hasError {
|
||||
t.Errorf("Expand('%s') failed: unexpected error '%v'",
|
||||
expandTest.input, err)
|
||||
continue
|
||||
}
|
||||
if hasError != expandTest.errorExpected && !hasError {
|
||||
t.Errorf("Expand('%s') did not fail as expected: got result '%+v'",
|
||||
expandTest.input, result)
|
||||
continue
|
||||
}
|
||||
if !hasError && !equal(result, expandTest.resultExpected) {
|
||||
t.Errorf("Expand('%s') failed: got result '%+v', expected result '%v'",
|
||||
expandTest.input, result, expandTest.resultExpected)
|
||||
continue
|
||||
}
|
||||
|
||||
t.Logf("Checked hostlist.Expand('%s'): result = '%+v', err = '%v'",
|
||||
expandTest.input, result, err)
|
||||
}
|
||||
}
|
@@ -1,44 +1,8 @@
|
||||
{
|
||||
"natsrecv": {
|
||||
"natsrecv" : {
|
||||
"type": "nats",
|
||||
"address": "nats://my-url",
|
||||
"port": "4222",
|
||||
"port" : "4222",
|
||||
"database": "testcluster"
|
||||
},
|
||||
"redfish_recv": {
|
||||
"type": "redfish",
|
||||
"endpoint": "https://%h-bmc",
|
||||
"client_config": [
|
||||
{
|
||||
"host_list": "my-host-1-[1-2]",
|
||||
"username": "username-1",
|
||||
"password": "password-1"
|
||||
},
|
||||
{
|
||||
"host_list": "my-host-2-[1,2]",
|
||||
"username": "username-2",
|
||||
"password": "password-2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ipmi_recv": {
|
||||
"type": "ipmi",
|
||||
"endpoint": "ipmi-sensors://%h-ipmi",
|
||||
"exclude_metrics": [
|
||||
"fan_speed",
|
||||
"voltage"
|
||||
],
|
||||
"client_config": [
|
||||
{
|
||||
"username": "username-1",
|
||||
"password": "password-1",
|
||||
"host_list": "my-host-1-[1-2]"
|
||||
},
|
||||
{
|
||||
"username": "username-2",
|
||||
"password": "password-2",
|
||||
"host_list": "my-host-2-[1,2]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
This folder contains the ReceiveManager and receiver implementations for the cc-metric-collector.
|
||||
|
||||
## Configuration
|
||||
# Configuration
|
||||
|
||||
The configuration file for the receivers is a list of configurations. The `type` field in each specifies which receiver to initialize.
|
||||
|
||||
@@ -22,12 +22,8 @@ This allows to specify
|
||||
- [`nats`](./natsReceiver.md): Receive metrics from the NATS network
|
||||
- [`prometheus`](./prometheusReceiver.md): Scrape data from a Prometheus client
|
||||
- [`http`](./httpReceiver.md): Listen for HTTP Post requests transporting metrics in InfluxDB line protocol
|
||||
- [`ipmi`](./ipmiReceiver.md): Read IPMI sensor readings
|
||||
- [`redfish`](redfishReceiver.md) Use the Redfish (specification) to query thermal and power metrics
|
||||
- [`snmp`](./snmpReceiver.md) Query SNMP endpoints in the network
|
||||
|
||||
## Contributing own receivers
|
||||
|
||||
# Contributing own receivers
|
||||
A receiver contains a few functions and is derived from the type `Receiver` (in `metricReceiver.go`):
|
||||
|
||||
For an example, check the [sample receiver](./sampleReceiver.go)
|
||||
|
@@ -5,14 +5,15 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
influx "github.com/influxdata/line-protocol/v2/lineprotocol"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
"github.com/gorilla/mux"
|
||||
influx "github.com/influxdata/line-protocol"
|
||||
)
|
||||
|
||||
const HTTP_RECEIVER_PORT = "8080"
|
||||
@@ -22,39 +23,22 @@ type HttpReceiverConfig struct {
|
||||
Addr string `json:"address"`
|
||||
Port string `json:"port"`
|
||||
Path string `json:"path"`
|
||||
|
||||
// Maximum amount of time to wait for the next request when keep-alives are enabled
|
||||
// should be larger than the measurement interval to keep the connection open
|
||||
IdleTimeout string `json:"idle_timeout"`
|
||||
idleTimeout time.Duration
|
||||
|
||||
// Controls whether HTTP keep-alives are enabled. By default, keep-alives are enabled
|
||||
KeepAlivesEnabled bool `json:"keep_alives_enabled"`
|
||||
|
||||
// Basic authentication
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
useBasicAuth bool
|
||||
}
|
||||
|
||||
type HttpReceiver struct {
|
||||
receiver
|
||||
meta map[string]string
|
||||
config HttpReceiverConfig
|
||||
server *http.Server
|
||||
wg sync.WaitGroup
|
||||
handler *influx.MetricHandler
|
||||
parser *influx.Parser
|
||||
meta map[string]string
|
||||
config HttpReceiverConfig
|
||||
router *mux.Router
|
||||
server *http.Server
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func (r *HttpReceiver) Init(name string, config json.RawMessage) error {
|
||||
r.name = fmt.Sprintf("HttpReceiver(%s)", name)
|
||||
|
||||
// Set default values
|
||||
r.config.Port = HTTP_RECEIVER_PORT
|
||||
r.config.KeepAlivesEnabled = true
|
||||
// should be larger than the measurement interval to keep the connection open
|
||||
r.config.IdleTimeout = "120s"
|
||||
|
||||
// Read config
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &r.config)
|
||||
if err != nil {
|
||||
@@ -65,47 +49,20 @@ func (r *HttpReceiver) Init(name string, config json.RawMessage) error {
|
||||
if len(r.config.Port) == 0 {
|
||||
return errors.New("not all configuration variables set required by HttpReceiver")
|
||||
}
|
||||
|
||||
// Check idle timeout config
|
||||
if len(r.config.IdleTimeout) > 0 {
|
||||
t, err := time.ParseDuration(r.config.IdleTimeout)
|
||||
if err == nil {
|
||||
cclog.ComponentDebug(r.name, "idleTimeout", t)
|
||||
r.config.idleTimeout = t
|
||||
}
|
||||
}
|
||||
|
||||
// Check basic authentication config
|
||||
if len(r.config.Username) > 0 || len(r.config.Password) > 0 {
|
||||
r.config.useBasicAuth = true
|
||||
}
|
||||
if r.config.useBasicAuth && len(r.config.Username) == 0 {
|
||||
return errors.New("basic authentication requires username")
|
||||
}
|
||||
if r.config.useBasicAuth && len(r.config.Password) == 0 {
|
||||
return errors.New("basic authentication requires password")
|
||||
}
|
||||
|
||||
r.meta = map[string]string{"source": r.name}
|
||||
p := r.config.Path
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
p = "/" + p
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%s", r.config.Addr, r.config.Port)
|
||||
uri := addr + p
|
||||
cclog.ComponentDebug(r.name, "INIT", "listen on:", uri)
|
||||
|
||||
// Register handler function r.ServerHttp for path p in the DefaultServeMux
|
||||
http.HandleFunc(p, r.ServerHttp)
|
||||
|
||||
// Create http server
|
||||
r.server = &http.Server{
|
||||
Addr: addr,
|
||||
Handler: nil, // handler to invoke, http.DefaultServeMux if nil
|
||||
IdleTimeout: r.config.idleTimeout,
|
||||
}
|
||||
r.server.SetKeepAlivesEnabled(r.config.KeepAlivesEnabled)
|
||||
uri := fmt.Sprintf("%s:%s%s", r.config.Addr, r.config.Port, p)
|
||||
cclog.ComponentDebug(r.name, "INIT", uri)
|
||||
r.handler = influx.NewMetricHandler()
|
||||
r.parser = influx.NewParser(r.handler)
|
||||
r.parser.SetTimeFunc(DefaultTime)
|
||||
|
||||
r.router = mux.NewRouter()
|
||||
r.router.Path(p).HandlerFunc(r.ServerHttp)
|
||||
r.server = &http.Server{Addr: uri, Handler: r.router}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,97 +79,31 @@ func (r *HttpReceiver) Start() {
|
||||
}
|
||||
|
||||
func (r *HttpReceiver) ServerHttp(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
// Check request method, only post method is handled
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Check basic authentication
|
||||
if r.config.useBasicAuth {
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok || username != r.config.Username || password != r.config.Password {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
metrics, err := r.parser.Parse(body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
d := influx.NewDecoder(req.Body)
|
||||
for d.Next() {
|
||||
|
||||
// Decode measurement name
|
||||
measurement, err := d.Measurement()
|
||||
if err != nil {
|
||||
msg := "ServerHttp: Failed to decode measurement: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
http.Error(w, msg, http.StatusInternalServerError)
|
||||
return
|
||||
for _, m := range metrics {
|
||||
y := lp.FromInfluxMetric(m)
|
||||
for k, v := range r.meta {
|
||||
y.AddMeta(k, v)
|
||||
}
|
||||
|
||||
// Decode tags
|
||||
tags := make(map[string]string)
|
||||
for {
|
||||
key, value, err := d.NextTag()
|
||||
if err != nil {
|
||||
msg := "ServerHttp: Failed to decode tag: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
http.Error(w, msg, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if key == nil {
|
||||
break
|
||||
}
|
||||
tags[string(key)] = string(value)
|
||||
}
|
||||
|
||||
// Decode fields
|
||||
fields := make(map[string]interface{})
|
||||
for {
|
||||
key, value, err := d.NextField()
|
||||
if err != nil {
|
||||
msg := "ServerHttp: Failed to decode field: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
http.Error(w, msg, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if key == nil {
|
||||
break
|
||||
}
|
||||
fields[string(key)] = value.Interface()
|
||||
}
|
||||
|
||||
// Decode time stamp
|
||||
t, err := d.Time(influx.Nanosecond, time.Time{})
|
||||
if err != nil {
|
||||
msg := "ServerHttp: Failed to decode time stamp: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
http.Error(w, msg, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
y, _ := lp.New(
|
||||
string(measurement),
|
||||
tags,
|
||||
r.meta,
|
||||
fields,
|
||||
t,
|
||||
)
|
||||
|
||||
if r.sink != nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
|
||||
// Check for IO errors
|
||||
err := d.Err()
|
||||
if err != nil {
|
||||
msg := "ServerHttp: Failed to decode: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
http.Error(w, msg, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
|
@@ -10,10 +10,7 @@ The `http` receiver can be used receive metrics through HTTP POST requests.
|
||||
"type": "http",
|
||||
"address" : "",
|
||||
"port" : "8080",
|
||||
"path" : "/write",
|
||||
"idle_timeout": "120s",
|
||||
"username": "myUser",
|
||||
"password": "myPW"
|
||||
"path" : "/write"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -22,22 +19,5 @@ The `http` receiver can be used receive metrics through HTTP POST requests.
|
||||
- `address`: Listen address
|
||||
- `port`: Listen port
|
||||
- `path`: URL path for the write endpoint
|
||||
- `idle_timeout`: Maximum amount of time to wait for the next request when keep-alives are enabled should be larger than the measurement interval to keep the connection open
|
||||
- `keep_alives_enabled`: Controls whether HTTP keep-alives are enabled. By default, keep-alives are enabled.
|
||||
- `username`: username for basic authentication
|
||||
- `password`: password for basic authentication
|
||||
|
||||
The HTTP endpoint listens to `http://<address>:<port>/<path>`
|
||||
|
||||
### Debugging
|
||||
|
||||
- Install [curl](https://curl.se/)
|
||||
- Use curl to send message to `http` receiver
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/write \
|
||||
--user "myUser:myPW" \
|
||||
--data \
|
||||
"myMetric,hostname=myHost,type=hwthread,type-id=0,unit=Hz value=400000i 1694777161164284635
|
||||
myMetric,hostname=myHost,type=hwthread,type-id=1,unit=Hz value=400001i 1694777161164284635"
|
||||
```
|
||||
|
@@ -1,534 +0,0 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
"github.com/ClusterCockpit/cc-metric-collector/pkg/hostlist"
|
||||
)
|
||||
|
||||
type IPMIReceiverClientConfig struct {
|
||||
|
||||
// Hostname the IPMI service belongs to
|
||||
Protocol string // Protocol / tool to use for IPMI sensor reading
|
||||
DriverType string // Out of band IPMI driver
|
||||
Fanout int // Maximum number of simultaneous IPMI connections
|
||||
NumHosts int // Number of remote IPMI devices with the same configuration
|
||||
IPMIHosts string // List of remote IPMI devices to communicate with
|
||||
IPMI2HostMapping map[string]string // Mapping between IPMI device name and host name
|
||||
Username string // User name to authenticate with
|
||||
Password string // Password to use for authentication
|
||||
CLIOptions []string // Additional command line options for ipmi-sensors
|
||||
isExcluded map[string]bool // is metric excluded
|
||||
}
|
||||
|
||||
type IPMIReceiver struct {
|
||||
receiver
|
||||
config struct {
|
||||
Interval time.Duration
|
||||
|
||||
// Client config for each IPMI hosts
|
||||
ClientConfigs []IPMIReceiverClientConfig
|
||||
}
|
||||
|
||||
// Storage for static information
|
||||
meta map[string]string
|
||||
|
||||
done chan bool // channel to finish / stop IPMI receiver
|
||||
wg sync.WaitGroup // wait group for IPMI receiver
|
||||
}
|
||||
|
||||
// doReadMetrics reads metrics from all configure IPMI hosts.
|
||||
func (r *IPMIReceiver) doReadMetric() {
|
||||
for i := range r.config.ClientConfigs {
|
||||
clientConfig := &r.config.ClientConfigs[i]
|
||||
var cmd_options []string
|
||||
if clientConfig.Protocol == "ipmi-sensors" {
|
||||
cmd_options = append(cmd_options,
|
||||
"--always-prefix",
|
||||
"--sdr-cache-recreate",
|
||||
// Attempt to interpret OEM data, such as event data, sensor readings, or general extra info
|
||||
"--interpret-oem-data",
|
||||
// Ignore not-available (i.e. N/A) sensors in output
|
||||
"--ignore-not-available-sensors",
|
||||
// Ignore unrecognized sensor events
|
||||
"--ignore-unrecognized-events",
|
||||
// Output fields in comma separated format
|
||||
"--comma-separated-output",
|
||||
// Do not output column headers
|
||||
"--no-header-output",
|
||||
// Output non-abbreviated units (e.g. 'Amps' instead of 'A').
|
||||
// May aid in disambiguation of units (e.g. 'C' for Celsius or Coulombs).
|
||||
"--non-abbreviated-units",
|
||||
"--fanout", fmt.Sprint(clientConfig.Fanout),
|
||||
"--driver-type", clientConfig.DriverType,
|
||||
"--hostname", clientConfig.IPMIHosts,
|
||||
"--username", clientConfig.Username,
|
||||
"--password", clientConfig.Password,
|
||||
)
|
||||
cmd_options := append(cmd_options, clientConfig.CLIOptions...)
|
||||
|
||||
command := exec.Command("ipmi-sensors", cmd_options...)
|
||||
stdout, _ := command.StdoutPipe()
|
||||
errBuf := new(bytes.Buffer)
|
||||
command.Stderr = errBuf
|
||||
|
||||
// start command
|
||||
if err := command.Start(); err != nil {
|
||||
cclog.ComponentError(
|
||||
r.name,
|
||||
fmt.Sprintf("doReadMetric(): Failed to start command \"%s\": %v", command.String(), err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Read command output
|
||||
const (
|
||||
idxID = iota
|
||||
idxName
|
||||
idxType
|
||||
idxReading
|
||||
idxUnits
|
||||
idxEvent
|
||||
)
|
||||
numPrefixRegex := regexp.MustCompile("^[[:digit:]][[:digit:]]-(.*)$")
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
// Read host
|
||||
v1 := strings.Split(scanner.Text(), ": ")
|
||||
if len(v1) != 2 {
|
||||
continue
|
||||
}
|
||||
host, ok := clientConfig.IPMI2HostMapping[v1[0]]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read sensors
|
||||
v2 := strings.Split(v1[1], ",")
|
||||
if len(v2) != 6 {
|
||||
continue
|
||||
}
|
||||
// Skip sensors with non available sensor readings
|
||||
if v2[idxReading] == "N/A" {
|
||||
continue
|
||||
}
|
||||
|
||||
metric := strings.ToLower(v2[idxType])
|
||||
name := strings.ToLower(
|
||||
strings.Replace(
|
||||
strings.TrimSpace(
|
||||
v2[idxName]), " ", "_", -1))
|
||||
// remove prefix enumeration like 01-...
|
||||
if v := numPrefixRegex.FindStringSubmatch(name); v != nil {
|
||||
name = v[1]
|
||||
}
|
||||
unit := v2[idxUnits]
|
||||
if unit == "Watts" {
|
||||
|
||||
// Power
|
||||
metric = "power"
|
||||
name = strings.TrimSuffix(name, "_power")
|
||||
name = strings.TrimSuffix(name, "_pwr")
|
||||
name = strings.TrimPrefix(name, "pwr_")
|
||||
} else if metric == "voltage" &&
|
||||
unit == "Volts" {
|
||||
|
||||
// Voltage
|
||||
name = strings.TrimPrefix(name, "volt_")
|
||||
} else if metric == "current" &&
|
||||
unit == "Amps" {
|
||||
|
||||
// Current
|
||||
unit = "Ampere"
|
||||
} else if metric == "temperature" &&
|
||||
unit == "degrees C" {
|
||||
|
||||
// Temperature
|
||||
name = strings.TrimSuffix(name, "_temp")
|
||||
unit = "degC"
|
||||
} else if metric == "temperature" &&
|
||||
unit == "degrees F" {
|
||||
|
||||
// Temperature
|
||||
name = strings.TrimSuffix(name, "_temp")
|
||||
unit = "degF"
|
||||
} else if metric == "fan" && unit == "RPM" {
|
||||
|
||||
// Fan speed
|
||||
metric = "fan_speed"
|
||||
name = strings.TrimSuffix(name, "_tach")
|
||||
name = strings.TrimPrefix(name, "spd_")
|
||||
} else if (metric == "cooling device" ||
|
||||
metric == "other units based sensor") &&
|
||||
name == "system_air_flow" &&
|
||||
unit == "CFM" {
|
||||
|
||||
// Air flow
|
||||
metric = "air_flow"
|
||||
name = strings.TrimSuffix(name, "_air_flow")
|
||||
unit = "CubicFeetPerMinute"
|
||||
} else if (metric == "processor" ||
|
||||
metric == "other units based sensor") &&
|
||||
(name == "cpu_utilization" ||
|
||||
name == "io_utilization" ||
|
||||
name == "mem_utilization" ||
|
||||
name == "sys_utilization") &&
|
||||
(unit == "unspecified" ||
|
||||
unit == "%") {
|
||||
|
||||
// Utilization
|
||||
metric = "utilization"
|
||||
name = strings.TrimSuffix(name, "_utilization")
|
||||
unit = "percent"
|
||||
} else {
|
||||
if false {
|
||||
// Debug output for unprocessed metrics
|
||||
fmt.Printf(
|
||||
"host: '%s', metric: '%s', name: '%s', unit: '%s'\n",
|
||||
host, metric, name, unit)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip excluded metrics
|
||||
if clientConfig.isExcluded[metric] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse sensor value
|
||||
value, err := strconv.ParseFloat(v2[idxReading], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
y, err := lp.New(
|
||||
metric,
|
||||
map[string]string{
|
||||
"hostname": host,
|
||||
"type": "node",
|
||||
"name": name,
|
||||
},
|
||||
map[string]string{
|
||||
"source": r.name,
|
||||
"group": "IPMI",
|
||||
"unit": unit,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"value": value,
|
||||
},
|
||||
time.Now())
|
||||
if err == nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for command end
|
||||
if err := command.Wait(); err != nil {
|
||||
errMsg, _ := io.ReadAll(errBuf)
|
||||
cclog.ComponentError(
|
||||
r.name,
|
||||
fmt.Sprintf("doReadMetric(): Failed to wait for the end of command \"%s\": %v\n",
|
||||
strings.Replace(command.String(), clientConfig.Password, "<PW>", -1), err),
|
||||
fmt.Sprintf("doReadMetric(): command stderr: \"%s\"\n", string(errMsg)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *IPMIReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
|
||||
// Start IPMI receiver
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
|
||||
// Create ticker
|
||||
ticker := time.NewTicker(r.config.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
r.doReadMetric()
|
||||
|
||||
select {
|
||||
case tickerTime := <-ticker.C:
|
||||
// Check if we missed the ticker event
|
||||
if since := time.Since(tickerTime); since > 5*time.Second {
|
||||
cclog.ComponentInfo(r.name, "Missed ticker event for more then", since)
|
||||
}
|
||||
|
||||
// process ticker event -> continue
|
||||
continue
|
||||
case <-r.done:
|
||||
// process done event
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cclog.ComponentDebug(r.name, "STARTED")
|
||||
}
|
||||
|
||||
// Close receiver: close network connection, close files, close libraries, ...
|
||||
func (r *IPMIReceiver) Close() {
|
||||
cclog.ComponentDebug(r.name, "CLOSE")
|
||||
|
||||
// Send the signal and wait
|
||||
close(r.done)
|
||||
r.wg.Wait()
|
||||
|
||||
cclog.ComponentDebug(r.name, "DONE")
|
||||
}
|
||||
|
||||
// NewIPMIReceiver creates a new instance of the redfish receiver
|
||||
// Initialize the receiver by giving it a name and reading in the config JSON
|
||||
func NewIPMIReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(IPMIReceiver)
|
||||
|
||||
// Config options from config file
|
||||
configJSON := struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
// How often the IPMI sensor metrics should be read and send to the sink (default: 30 s)
|
||||
IntervalString string `json:"interval,omitempty"`
|
||||
|
||||
// Maximum number of simultaneous IPMI connections (default: 64)
|
||||
Fanout int `json:"fanout,omitempty"`
|
||||
|
||||
// Out of band IPMI driver (default: LAN_2_0)
|
||||
DriverType string `json:"driver_type,omitempty"`
|
||||
|
||||
// Default client username, password and endpoint
|
||||
Username *string `json:"username"` // User name to authenticate with
|
||||
Password *string `json:"password"` // Password to use for authentication
|
||||
Endpoint *string `json:"endpoint"` // URL of the IPMI device
|
||||
|
||||
// Globally excluded metrics
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
|
||||
ClientConfigs []struct {
|
||||
Fanout int `json:"fanout,omitempty"` // Maximum number of simultaneous IPMI connections (default: 64)
|
||||
DriverType string `json:"driver_type,omitempty"` // Out of band IPMI driver (default: LAN_2_0)
|
||||
HostList string `json:"host_list"` // List of hosts with the same client configuration
|
||||
Username *string `json:"username"` // User name to authenticate with
|
||||
Password *string `json:"password"` // Password to use for authentication
|
||||
Endpoint *string `json:"endpoint"` // URL of the IPMI service
|
||||
|
||||
// Per client excluded metrics
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
|
||||
// Additional command line options for ipmi-sensors
|
||||
CLIOptions []string `json:"cli_options,omitempty"`
|
||||
} `json:"client_config"`
|
||||
}{
|
||||
// Set defaults values
|
||||
// Allow overwriting these defaults by reading config JSON
|
||||
Fanout: 64,
|
||||
DriverType: "LAN_2_0",
|
||||
IntervalString: "30s",
|
||||
}
|
||||
|
||||
// Set name of IPMIReceiver
|
||||
r.name = fmt.Sprintf("IPMIReceiver(%s)", name)
|
||||
|
||||
// Create done channel
|
||||
r.done = make(chan bool)
|
||||
|
||||
// Set static information
|
||||
r.meta = map[string]string{"source": r.name}
|
||||
|
||||
// Read the IPMI receiver specific JSON config
|
||||
if len(config) > 0 {
|
||||
d := json.NewDecoder(bytes.NewReader(config))
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(&configJSON); err != nil {
|
||||
cclog.ComponentError(r.name, "Error reading config:", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Convert interval string representation to duration
|
||||
var err error
|
||||
r.config.Interval, err = time.ParseDuration(configJSON.IntervalString)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Failed to parse duration string interval='%s': %w",
|
||||
configJSON.IntervalString,
|
||||
err,
|
||||
)
|
||||
cclog.Error(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create client config from JSON config
|
||||
totalNumHosts := 0
|
||||
for i := range configJSON.ClientConfigs {
|
||||
clientConfigJSON := &configJSON.ClientConfigs[i]
|
||||
|
||||
var endpoint string
|
||||
if clientConfigJSON.Endpoint != nil {
|
||||
endpoint = *clientConfigJSON.Endpoint
|
||||
} else if configJSON.Endpoint != nil {
|
||||
endpoint = *configJSON.Endpoint
|
||||
} else {
|
||||
err := fmt.Errorf("client config number %v requires endpoint", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fanout := configJSON.Fanout
|
||||
if clientConfigJSON.Fanout != 0 {
|
||||
fanout = clientConfigJSON.Fanout
|
||||
}
|
||||
|
||||
driverType := configJSON.DriverType
|
||||
if clientConfigJSON.DriverType != "" {
|
||||
driverType = clientConfigJSON.DriverType
|
||||
}
|
||||
if driverType != "LAN" && driverType != "LAN_2_0" {
|
||||
err := fmt.Errorf("client config number %v has invalid driver type %s", i, driverType)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var protocol string
|
||||
var host_pattern string
|
||||
if e := strings.Split(endpoint, "://"); len(e) == 2 {
|
||||
protocol = e[0]
|
||||
host_pattern = e[1]
|
||||
} else {
|
||||
err := fmt.Errorf("client config number %v has invalid endpoint %s", i, endpoint)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var username string
|
||||
if clientConfigJSON.Username != nil {
|
||||
username = *clientConfigJSON.Username
|
||||
} else if configJSON.Username != nil {
|
||||
username = *configJSON.Username
|
||||
} else {
|
||||
err := fmt.Errorf("client config number %v requires username", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var password string
|
||||
if clientConfigJSON.Password != nil {
|
||||
password = *clientConfigJSON.Password
|
||||
} else if configJSON.Password != nil {
|
||||
password = *configJSON.Password
|
||||
} else {
|
||||
err := fmt.Errorf("client config number %v requires password", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create mapping between IPMI host name and node host name
|
||||
// This also guaranties that all IPMI host names are unique
|
||||
ipmi2HostMapping := make(map[string]string)
|
||||
hostList, err := hostlist.Expand(clientConfigJSON.HostList)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("client config number %d failed to parse host list %s: %v",
|
||||
i, clientConfigJSON.HostList, err)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
for _, host := range hostList {
|
||||
ipmiHost := strings.Replace(host_pattern, "%h", host, -1)
|
||||
ipmi2HostMapping[ipmiHost] = host
|
||||
}
|
||||
|
||||
numHosts := len(ipmi2HostMapping)
|
||||
totalNumHosts += numHosts
|
||||
ipmiHostList := make([]string, 0, numHosts)
|
||||
for ipmiHost := range ipmi2HostMapping {
|
||||
ipmiHostList = append(ipmiHostList, ipmiHost)
|
||||
}
|
||||
|
||||
// Additional command line options
|
||||
for _, v := range clientConfigJSON.CLIOptions {
|
||||
switch {
|
||||
case v == "-u" || strings.HasPrefix(v, "--username"):
|
||||
err := fmt.Errorf("client config number %v: do not set username in cli_options. Use json config username instead", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
case v == "-p" || strings.HasPrefix(v, "--password"):
|
||||
err := fmt.Errorf("client config number %v: do not set password in cli_options. Use json config password instead", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
case v == "-h" || strings.HasPrefix(v, "--hostname"):
|
||||
err := fmt.Errorf("client config number %v: do not set hostname in cli_options. Use json config host_list instead", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
case v == "-D" || strings.HasPrefix(v, "--driver-type"):
|
||||
err := fmt.Errorf("client config number %v: do not set driver type in cli_options. Use json config driver_type instead", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
case v == "-F" || strings.HasPrefix(v, " --fanout"):
|
||||
err := fmt.Errorf("client config number %v: do not set fanout in cli_options. Use json config fanout instead", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
case v == "--always-prefix" ||
|
||||
v == "--sdr-cache-recreate" ||
|
||||
v == "--interpret-oem-data" ||
|
||||
v == "--ignore-not-available-sensors" ||
|
||||
v == "--ignore-unrecognized-events" ||
|
||||
v == "--comma-separated-output" ||
|
||||
v == "--no-header-output" ||
|
||||
v == "--non-abbreviated-units":
|
||||
err := fmt.Errorf("client config number %v: Do not use option %s in cli_options, it is used internally", i, v)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cliOptions := make([]string, 0)
|
||||
cliOptions = append(cliOptions, clientConfigJSON.CLIOptions...)
|
||||
|
||||
// Is metrics excluded globally or per client
|
||||
isExcluded := make(map[string]bool)
|
||||
for _, key := range clientConfigJSON.ExcludeMetrics {
|
||||
isExcluded[key] = true
|
||||
}
|
||||
for _, key := range configJSON.ExcludeMetrics {
|
||||
isExcluded[key] = true
|
||||
}
|
||||
|
||||
r.config.ClientConfigs = append(
|
||||
r.config.ClientConfigs,
|
||||
IPMIReceiverClientConfig{
|
||||
Protocol: protocol,
|
||||
Fanout: fanout,
|
||||
DriverType: driverType,
|
||||
NumHosts: numHosts,
|
||||
IPMIHosts: strings.Join(ipmiHostList, ","),
|
||||
IPMI2HostMapping: ipmi2HostMapping,
|
||||
Username: username,
|
||||
Password: password,
|
||||
CLIOptions: cliOptions,
|
||||
isExcluded: isExcluded,
|
||||
})
|
||||
}
|
||||
|
||||
if totalNumHosts == 0 {
|
||||
err := fmt.Errorf("at least one IPMI host config is required")
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cclog.ComponentInfo(r.name, "monitoring", totalNumHosts, "IPMI hosts")
|
||||
return r, nil
|
||||
}
|
@@ -1,48 +0,0 @@
|
||||
## IPMI Receiver
|
||||
|
||||
The IPMI Receiver uses `ipmi-sensors` from the [FreeIPMI](https://www.gnu.org/software/freeipmi/) project to read IPMI sensor readings and sensor data repository (SDR) information. The available metrics depend on the sensors provided by the hardware vendor but typically contain temperature, fan speed, voltage and power metrics.
|
||||
|
||||
### Configuration structure
|
||||
|
||||
```json
|
||||
{
|
||||
"<IPMI receiver name>": {
|
||||
"type": "ipmi",
|
||||
"interval": "30s",
|
||||
"fanout": 256,
|
||||
"username": "<Username>",
|
||||
"password": "<Password>",
|
||||
"endpoint": "ipmi-sensors://%h-bmc",
|
||||
"exclude_metrics": [ "fan_speed", "voltage" ],
|
||||
"client_config": [
|
||||
{
|
||||
"host_list": "n[1,2-4]"
|
||||
},
|
||||
{
|
||||
"host_list": "n[5-6]",
|
||||
"driver_type": "LAN",
|
||||
"cli_options": [ "--workaround-flags=..." ],
|
||||
"password": "<Password 2>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Global settings:
|
||||
|
||||
- `interval`: How often the IPMI sensor metrics should be read and send to the sink (default: 30 s)
|
||||
|
||||
Global and per IPMI device settings (per IPMI device settings overwrite the global settings):
|
||||
|
||||
- `exclude_metrics`: list of excluded metrics e.g. fan_speed, power, temperature, utilization, voltage
|
||||
- `fanout`: Maximum number of simultaneous IPMI connections (default: 64)
|
||||
- `driver_type`: Out of band IPMI driver (default: LAN_2_0)
|
||||
- `username`: User name to authenticate with
|
||||
- `password`: Password to use for authentication
|
||||
- `endpoint`: URL of the IPMI device (placeholder `%h` gets replaced by the hostname)
|
||||
|
||||
Per IPMI device settings:
|
||||
|
||||
- `host_list`: List of hosts with the same client configuration
|
||||
- `cli_options`: Additional command line options for ipmi-sensors
|
@@ -1,7 +1,7 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
type defaultReceiverConfig struct {
|
||||
|
@@ -6,9 +6,9 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
influx "github.com/influxdata/line-protocol/v2/lineprotocol"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
influx "github.com/influxdata/line-protocol"
|
||||
nats "github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -21,85 +21,37 @@ type NatsReceiverConfig struct {
|
||||
|
||||
type NatsReceiver struct {
|
||||
receiver
|
||||
nc *nats.Conn
|
||||
meta map[string]string
|
||||
config NatsReceiverConfig
|
||||
nc *nats.Conn
|
||||
handler *influx.MetricHandler
|
||||
parser *influx.Parser
|
||||
meta map[string]string
|
||||
config NatsReceiverConfig
|
||||
}
|
||||
|
||||
var DefaultTime = func() time.Time {
|
||||
return time.Unix(42, 0)
|
||||
}
|
||||
|
||||
// Start subscribes to the configured NATS subject
|
||||
// Messages wil be handled by r._NatsReceive
|
||||
func (r *NatsReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
r.nc.Subscribe(r.config.Subject, r._NatsReceive)
|
||||
}
|
||||
|
||||
// _NatsReceive receives subscribed messages from the NATS server
|
||||
func (r *NatsReceiver) _NatsReceive(m *nats.Msg) {
|
||||
|
||||
d := influx.NewDecoderWithBytes(m.Data)
|
||||
for d.Next() {
|
||||
|
||||
// Decode measurement name
|
||||
measurement, err := d.Measurement()
|
||||
if err != nil {
|
||||
msg := "_NatsReceive: Failed to decode measurement: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode tags
|
||||
tags := make(map[string]string)
|
||||
for {
|
||||
key, value, err := d.NextTag()
|
||||
if err != nil {
|
||||
msg := "_NatsReceive: Failed to decode tag: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
return
|
||||
metrics, err := r.parser.Parse(m.Data)
|
||||
if err == nil {
|
||||
for _, m := range metrics {
|
||||
y := lp.FromInfluxMetric(m)
|
||||
for k, v := range r.meta {
|
||||
y.AddMeta(k, v)
|
||||
}
|
||||
if key == nil {
|
||||
break
|
||||
if r.sink != nil {
|
||||
r.sink <- y
|
||||
}
|
||||
tags[string(key)] = string(value)
|
||||
}
|
||||
|
||||
// Decode fields
|
||||
fields := make(map[string]interface{})
|
||||
for {
|
||||
key, value, err := d.NextField()
|
||||
if err != nil {
|
||||
msg := "_NatsReceive: Failed to decode field: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
return
|
||||
}
|
||||
if key == nil {
|
||||
break
|
||||
}
|
||||
fields[string(key)] = value.Interface()
|
||||
}
|
||||
|
||||
// Decode time stamp
|
||||
t, err := d.Time(influx.Nanosecond, time.Time{})
|
||||
if err != nil {
|
||||
msg := "_NatsReceive: Failed to decode time: " + err.Error()
|
||||
cclog.ComponentError(r.name, msg)
|
||||
return
|
||||
}
|
||||
|
||||
y, _ := lp.New(
|
||||
string(measurement),
|
||||
tags,
|
||||
r.meta,
|
||||
fields,
|
||||
t,
|
||||
)
|
||||
|
||||
if r.sink != nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the connection to the NATS server
|
||||
func (r *NatsReceiver) Close() {
|
||||
if r.nc != nil {
|
||||
cclog.ComponentDebug(r.name, "CLOSE")
|
||||
@@ -107,13 +59,10 @@ func (r *NatsReceiver) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// NewNatsReceiver creates a new Receiver which subscribes to messages from a NATS server
|
||||
func NewNatsReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(NatsReceiver)
|
||||
r.name = fmt.Sprintf("NatsReceiver(%s)", name)
|
||||
|
||||
// Read configuration file, allow overwriting default config
|
||||
r.config.Addr = "localhost"
|
||||
r.config.Addr = nats.DefaultURL
|
||||
r.config.Port = "4222"
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &r.config)
|
||||
@@ -127,21 +76,17 @@ func NewNatsReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
len(r.config.Subject) == 0 {
|
||||
return nil, errors.New("not all configuration variables set required by NatsReceiver")
|
||||
}
|
||||
|
||||
// Set metadata
|
||||
r.meta = map[string]string{
|
||||
"source": r.name,
|
||||
}
|
||||
|
||||
// Connect to NATS server
|
||||
url := fmt.Sprintf("nats://%s:%s", r.config.Addr, r.config.Port)
|
||||
cclog.ComponentDebug(r.name, "NewNatsReceiver", url, "Subject", r.config.Subject)
|
||||
if nc, err := nats.Connect(url); err == nil {
|
||||
r.meta = map[string]string{"source": r.name}
|
||||
uri := fmt.Sprintf("%s:%s", r.config.Addr, r.config.Port)
|
||||
cclog.ComponentDebug(r.name, "NewNatsReceiver", uri, "Subject", r.config.Subject)
|
||||
if nc, err := nats.Connect(uri); err == nil {
|
||||
r.nc = nc
|
||||
} else {
|
||||
r.nc = nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.handler = influx.NewMetricHandler()
|
||||
r.parser = influx.NewParser(r.handler)
|
||||
r.parser.SetTimeFunc(DefaultTime)
|
||||
return r, nil
|
||||
}
|
||||
|
@@ -19,32 +19,3 @@ The `nats` receiver can be used receive metrics from the NATS network. The `nats
|
||||
- `address`: Address of the NATS control server
|
||||
- `port`: Port of the NATS control server
|
||||
- `subject`: Subscribes to this subject and receive metrics
|
||||
|
||||
### Debugging
|
||||
|
||||
- Install NATS server and command line client
|
||||
- Start NATS server
|
||||
|
||||
```bash
|
||||
nats-server --net nats-server.example.org --port 4222
|
||||
```
|
||||
|
||||
- Check NATS server works as expected
|
||||
|
||||
```bash
|
||||
nats --server=nats-server-db.example.org:4222 server check
|
||||
```
|
||||
|
||||
- Use NATS command line client to subscribe to all messages
|
||||
|
||||
```bash
|
||||
nats --server=nats-server-db.example.org:4222 sub ">"
|
||||
```
|
||||
|
||||
- Use NATS command line client to send message to NATS receiver
|
||||
|
||||
```bash
|
||||
nats --server=nats-server-db.example.org:4222 pub subject \
|
||||
"myMetric,hostname=myHost,type=hwthread,type-id=0,unit=Hz value=400000i 1694777161164284635
|
||||
myMetric,hostname=myHost,type=hwthread,type-id=1,unit=Hz value=400001i 1694777161164284635"
|
||||
```
|
||||
|
@@ -12,8 +12,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
type PrometheusReceiverConfig struct {
|
||||
|
@@ -2,25 +2,22 @@ package receivers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
var AvailableReceivers = map[string]func(name string, config json.RawMessage) (Receiver, error){
|
||||
"http": NewHttpReceiver,
|
||||
"ipmi": NewIPMIReceiver,
|
||||
"nats": NewNatsReceiver,
|
||||
"redfish": NewRedfishReceiver,
|
||||
"snmp": NewSNMPReceiver,
|
||||
"nats": NewNatsReceiver,
|
||||
}
|
||||
|
||||
type receiveManager struct {
|
||||
inputs []Receiver
|
||||
output chan lp.CCMetric
|
||||
done chan bool
|
||||
wg *sync.WaitGroup
|
||||
config []json.RawMessage
|
||||
}
|
||||
|
||||
@@ -36,6 +33,8 @@ func (rm *receiveManager) Init(wg *sync.WaitGroup, receiverConfigFile string) er
|
||||
// Initialize struct fields
|
||||
rm.inputs = make([]Receiver, 0)
|
||||
rm.output = nil
|
||||
rm.done = make(chan bool)
|
||||
rm.wg = wg
|
||||
rm.config = make([]json.RawMessage, 0)
|
||||
|
||||
configFile, err := os.Open(receiverConfigFile)
|
||||
@@ -59,7 +58,7 @@ func (rm *receiveManager) Init(wg *sync.WaitGroup, receiverConfigFile string) er
|
||||
}
|
||||
|
||||
func (rm *receiveManager) Start() {
|
||||
cclog.ComponentDebug("ReceiveManager", "START")
|
||||
rm.wg.Add(1)
|
||||
|
||||
for _, r := range rm.inputs {
|
||||
cclog.ComponentDebug("ReceiveManager", "START", r.Name())
|
||||
@@ -75,13 +74,9 @@ func (rm *receiveManager) AddInput(name string, rawConfig json.RawMessage) error
|
||||
cclog.ComponentError("ReceiveManager", "SKIP", config.Type, "JSON config error:", err.Error())
|
||||
return err
|
||||
}
|
||||
if config.Type == "" {
|
||||
cclog.ComponentError("ReceiveManager", "SKIP", "JSON config for receiver", name, "does not contain a receiver type")
|
||||
return fmt.Errorf("JSON config for receiver %s does not contain a receiver type", name)
|
||||
}
|
||||
if _, found := AvailableReceivers[config.Type]; !found {
|
||||
cclog.ComponentError("ReceiveManager", "SKIP", "unknown receiver type:", config.Type)
|
||||
return fmt.Errorf("unknown receiver type: %s", config.Type)
|
||||
cclog.ComponentError("ReceiveManager", "SKIP", config.Type, "unknown receiver:", err.Error())
|
||||
return err
|
||||
}
|
||||
r, err := AvailableReceivers[config.Type](name, rawConfig)
|
||||
if err != nil {
|
||||
@@ -102,19 +97,16 @@ func (rm *receiveManager) AddOutput(output chan lp.CCMetric) {
|
||||
}
|
||||
|
||||
func (rm *receiveManager) Close() {
|
||||
cclog.ComponentDebug("ReceiveManager", "CLOSE")
|
||||
|
||||
// Close all receivers
|
||||
for _, r := range rm.inputs {
|
||||
cclog.ComponentDebug("ReceiveManager", "CLOSE", r.Name())
|
||||
r.Close()
|
||||
}
|
||||
|
||||
cclog.ComponentDebug("ReceiveManager", "DONE")
|
||||
rm.wg.Done()
|
||||
cclog.ComponentDebug("ReceiveManager", "CLOSE")
|
||||
}
|
||||
|
||||
func New(wg *sync.WaitGroup, receiverConfigFile string) (ReceiveManager, error) {
|
||||
r := new(receiveManager)
|
||||
r := &receiveManager{}
|
||||
err := r.Init(wg, receiverConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@@ -1,843 +0,0 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
"github.com/ClusterCockpit/cc-metric-collector/pkg/hostlist"
|
||||
|
||||
// See: https://pkg.go.dev/github.com/stmcginnis/gofish
|
||||
"github.com/stmcginnis/gofish"
|
||||
"github.com/stmcginnis/gofish/common"
|
||||
"github.com/stmcginnis/gofish/redfish"
|
||||
)
|
||||
|
||||
type RedfishReceiverClientConfig struct {
|
||||
|
||||
// Hostname the redfish service belongs to
|
||||
Hostname string
|
||||
|
||||
// is metric excluded globally or per client
|
||||
isExcluded map[string](bool)
|
||||
|
||||
doPowerMetric bool
|
||||
doProcessorMetrics bool
|
||||
doThermalMetrics bool
|
||||
|
||||
skipProcessorMetricsURL map[string]bool
|
||||
|
||||
gofish gofish.ClientConfig
|
||||
}
|
||||
|
||||
// RedfishReceiver configuration:
|
||||
type RedfishReceiver struct {
|
||||
receiver
|
||||
|
||||
config struct {
|
||||
fanout int
|
||||
Interval time.Duration
|
||||
HttpTimeout time.Duration
|
||||
|
||||
// Client config for each redfish service
|
||||
ClientConfigs []RedfishReceiverClientConfig
|
||||
}
|
||||
|
||||
done chan bool // channel to finish / stop redfish receiver
|
||||
wg sync.WaitGroup // wait group for redfish receiver
|
||||
}
|
||||
|
||||
// readThermalMetrics reads thermal metrics from a redfish device
|
||||
func (r *RedfishReceiver) readThermalMetrics(
|
||||
clientConfig *RedfishReceiverClientConfig,
|
||||
chassis *redfish.Chassis) error {
|
||||
|
||||
// Get thermal information for each chassis
|
||||
thermal, err := chassis.Thermal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readMetrics: chassis.Thermal() failed: %v", err)
|
||||
}
|
||||
|
||||
// Skip empty thermal information
|
||||
if thermal == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
timestamp := time.Now()
|
||||
|
||||
for _, temperature := range thermal.Temperatures {
|
||||
|
||||
// Skip, when temperature metric is excluded
|
||||
if clientConfig.isExcluded["temperature"] {
|
||||
break
|
||||
}
|
||||
|
||||
// Skip all temperatures which are not in enabled state
|
||||
if temperature.Status.State != "" && temperature.Status.State != common.EnabledState {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"hostname": clientConfig.Hostname,
|
||||
"type": "node",
|
||||
// ChassisType shall indicate the physical form factor for the type of chassis
|
||||
"chassis_typ": string(chassis.ChassisType),
|
||||
// Chassis name
|
||||
"chassis_name": chassis.Name,
|
||||
// ID uniquely identifies the resource
|
||||
"temperature_id": temperature.ID,
|
||||
// MemberID shall uniquely identify the member within the collection. For
|
||||
// services supporting Redfish v1.6 or higher, this value shall be the
|
||||
// zero-based array index.
|
||||
"temperature_member_id": temperature.MemberID,
|
||||
// PhysicalContext shall be a description of the affected device or region
|
||||
// within the chassis to which this temperature measurement applies
|
||||
"temperature_physical_context": string(temperature.PhysicalContext),
|
||||
// Name
|
||||
"temperature_name": temperature.Name,
|
||||
}
|
||||
|
||||
// Delete empty tags
|
||||
for key, value := range tags {
|
||||
if value == "" {
|
||||
delete(tags, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Set meta data tags
|
||||
meta := map[string]string{
|
||||
"source": r.name,
|
||||
"group": "Temperature",
|
||||
"unit": "degC",
|
||||
}
|
||||
|
||||
// ReadingCelsius shall be the current value of the temperature sensor's reading.
|
||||
value := temperature.ReadingCelsius
|
||||
|
||||
y, err := lp.New("temperature", tags, meta,
|
||||
map[string]interface{}{
|
||||
"value": value,
|
||||
},
|
||||
timestamp)
|
||||
if err == nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
|
||||
for _, fan := range thermal.Fans {
|
||||
// Skip, when fan_speed metric is excluded
|
||||
if clientConfig.isExcluded["fan_speed"] {
|
||||
break
|
||||
}
|
||||
|
||||
// Skip all fans which are not in enabled state
|
||||
if fan.Status.State != common.EnabledState {
|
||||
continue
|
||||
}
|
||||
|
||||
tags := map[string]string{
|
||||
"hostname": clientConfig.Hostname,
|
||||
"type": "node",
|
||||
// ChassisType shall indicate the physical form factor for the type of chassis
|
||||
"chassis_typ": string(chassis.ChassisType),
|
||||
// Chassis name
|
||||
"chassis_name": chassis.Name,
|
||||
// ID uniquely identifies the resource
|
||||
"fan_id": fan.ID,
|
||||
// MemberID shall uniquely identify the member within the collection. For
|
||||
// services supporting Redfish v1.6 or higher, this value shall be the
|
||||
// zero-based array index.
|
||||
"fan_member_id": fan.MemberID,
|
||||
// PhysicalContext shall be a description of the affected device or region
|
||||
// within the chassis to which this fan is associated
|
||||
"fan_physical_context": string(fan.PhysicalContext),
|
||||
// Name
|
||||
"fan_name": fan.Name,
|
||||
}
|
||||
|
||||
// Delete empty tags
|
||||
for key, value := range tags {
|
||||
if value == "" {
|
||||
delete(tags, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Set meta data tags
|
||||
meta := map[string]string{
|
||||
"source": r.name,
|
||||
"group": "FanSpeed",
|
||||
"unit": string(fan.ReadingUnits),
|
||||
}
|
||||
|
||||
// Reading shall be the current value of the fan sensor's reading
|
||||
value := fan.Reading
|
||||
|
||||
y, err := lp.New("fan_speed", tags, meta,
|
||||
map[string]interface{}{
|
||||
"value": value,
|
||||
},
|
||||
timestamp)
|
||||
if err == nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readPowerMetrics reads power metrics from a redfish device
|
||||
func (r *RedfishReceiver) readPowerMetrics(
|
||||
clientConfig *RedfishReceiverClientConfig,
|
||||
chassis *redfish.Chassis) error {
|
||||
|
||||
// Get power information for each chassis
|
||||
power, err := chassis.Power()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readMetrics: chassis.Power() failed: %v", err)
|
||||
}
|
||||
|
||||
// Skip empty power information
|
||||
if power == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
timestamp := time.Now()
|
||||
|
||||
// Read min, max and average consumed watts for each power control
|
||||
for _, pc := range power.PowerControl {
|
||||
|
||||
// Skip all power controls which are not in enabled state
|
||||
if pc.Status.State != "" && pc.Status.State != common.EnabledState {
|
||||
continue
|
||||
}
|
||||
|
||||
// Map of collected metrics
|
||||
metrics := make(map[string]float32)
|
||||
|
||||
// PowerConsumedWatts shall represent the actual power being consumed (in
|
||||
// Watts) by the chassis
|
||||
if !clientConfig.isExcluded["consumed_watts"] {
|
||||
metrics["consumed_watts"] = pc.PowerConsumedWatts
|
||||
}
|
||||
// AverageConsumedWatts shall represent the
|
||||
// average power level that occurred averaged over the last IntervalInMin
|
||||
// minutes.
|
||||
if !clientConfig.isExcluded["average_consumed_watts"] {
|
||||
metrics["average_consumed_watts"] = pc.PowerMetrics.AverageConsumedWatts
|
||||
}
|
||||
// MinConsumedWatts shall represent the
|
||||
// minimum power level in watts that occurred within the last
|
||||
// IntervalInMin minutes.
|
||||
if !clientConfig.isExcluded["min_consumed_watts"] {
|
||||
metrics["min_consumed_watts"] = pc.PowerMetrics.MinConsumedWatts
|
||||
}
|
||||
// MaxConsumedWatts shall represent the
|
||||
// maximum power level in watts that occurred within the last
|
||||
// IntervalInMin minutes
|
||||
if !clientConfig.isExcluded["max_consumed_watts"] {
|
||||
metrics["max_consumed_watts"] = pc.PowerMetrics.MaxConsumedWatts
|
||||
}
|
||||
// IntervalInMin shall represent the time interval (or window), in minutes,
|
||||
// in which the PowerMetrics properties are measured over.
|
||||
// Should be an integer, but some Dell implementations return as a float
|
||||
intervalInMin :=
|
||||
strconv.FormatFloat(
|
||||
float64(pc.PowerMetrics.IntervalInMin), 'f', -1, 32)
|
||||
|
||||
// Set tags
|
||||
tags := map[string]string{
|
||||
"hostname": clientConfig.Hostname,
|
||||
"type": "node",
|
||||
// ChassisType shall indicate the physical form factor for the type of chassis
|
||||
"chassis_typ": string(chassis.ChassisType),
|
||||
// Chassis name
|
||||
"chassis_name": chassis.Name,
|
||||
// ID uniquely identifies the resource
|
||||
"power_control_id": pc.ID,
|
||||
// MemberID shall uniquely identify the member within the collection. For
|
||||
// services supporting Redfish v1.6 or higher, this value shall be the
|
||||
// zero-based array index.
|
||||
"power_control_member_id": pc.MemberID,
|
||||
// PhysicalContext shall be a description of the affected device(s) or region
|
||||
// within the chassis to which this power control applies.
|
||||
"power_control_physical_context": string(pc.PhysicalContext),
|
||||
// Name
|
||||
"power_control_name": pc.Name,
|
||||
}
|
||||
|
||||
// Delete empty tags
|
||||
for key, value := range tags {
|
||||
if value == "" {
|
||||
delete(tags, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Set meta data tags
|
||||
meta := map[string]string{
|
||||
"source": r.name,
|
||||
"group": "Energy",
|
||||
"interval_in_minutes": intervalInMin,
|
||||
"unit": "watts",
|
||||
}
|
||||
|
||||
// Delete empty meta data tags
|
||||
for key, value := range meta {
|
||||
if value == "" {
|
||||
delete(meta, key)
|
||||
}
|
||||
}
|
||||
|
||||
for name, value := range metrics {
|
||||
|
||||
y, err := lp.New(name, tags, meta,
|
||||
map[string]interface{}{
|
||||
"value": value,
|
||||
},
|
||||
timestamp)
|
||||
if err == nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readProcessorMetrics reads processor metrics from a redfish device
|
||||
// See: https://redfish.dmtf.org/schemas/v1/ProcessorMetrics.json
|
||||
func (r *RedfishReceiver) readProcessorMetrics(
|
||||
clientConfig *RedfishReceiverClientConfig,
|
||||
processor *redfish.Processor) error {
|
||||
|
||||
timestamp := time.Now()
|
||||
|
||||
// URL to processor metrics
|
||||
URL := processor.ODataID + "/ProcessorMetrics"
|
||||
|
||||
// Skip previously detected non existing URLs
|
||||
if clientConfig.skipProcessorMetricsURL[URL] {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := processor.Client.Get(URL)
|
||||
if err != nil {
|
||||
// Skip non existing URLs
|
||||
if statusCode := err.(*common.Error).HTTPReturnedStatusCode; statusCode == http.StatusNotFound {
|
||||
clientConfig.skipProcessorMetricsURL[URL] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("processor.Client.Get(%v) failed: %+w", URL, err)
|
||||
}
|
||||
|
||||
var processorMetrics struct {
|
||||
common.Entity
|
||||
ODataType string `json:"@odata.type"`
|
||||
ODataEtag string `json:"@odata.etag"`
|
||||
Description string `json:"Description"`
|
||||
// This property shall contain the power, in watts, that the processor has consumed.
|
||||
ConsumedPowerWatt float32 `json:"ConsumedPowerWatt"`
|
||||
// This property shall contain the temperature, in Celsius, of the processor.
|
||||
TemperatureCelsius float32 `json:"TemperatureCelsius"`
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read JSON for processor metrics: %+w", err)
|
||||
}
|
||||
err = json.Unmarshal(body, &processorMetrics)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"unable to unmarshal JSON='%s' for processor metrics: %+w",
|
||||
string(body),
|
||||
err,
|
||||
)
|
||||
}
|
||||
processorMetrics.SetClient(processor.Client)
|
||||
|
||||
// Set tags
|
||||
tags := map[string]string{
|
||||
"hostname": clientConfig.Hostname,
|
||||
"type": "socket",
|
||||
// ProcessorType shall contain the string which identifies the type of processor contained in this Socket
|
||||
"processor_typ": string(processor.ProcessorType),
|
||||
// Processor name
|
||||
"processor_name": processor.Name,
|
||||
// ID uniquely identifies the resource
|
||||
"processor_id": processor.ID,
|
||||
}
|
||||
|
||||
// Delete empty tags
|
||||
for key, value := range tags {
|
||||
if value == "" {
|
||||
delete(tags, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Set meta data tags
|
||||
metaPower := map[string]string{
|
||||
"source": r.name,
|
||||
"group": "Energy",
|
||||
"unit": "watts",
|
||||
}
|
||||
|
||||
namePower := "consumed_power"
|
||||
|
||||
if !clientConfig.isExcluded[namePower] &&
|
||||
// Some servers return "ConsumedPowerWatt":65535 instead of "ConsumedPowerWatt":null
|
||||
processorMetrics.ConsumedPowerWatt != 65535 {
|
||||
y, err := lp.New(namePower, tags, metaPower,
|
||||
map[string]interface{}{
|
||||
"value": processorMetrics.ConsumedPowerWatt,
|
||||
},
|
||||
timestamp)
|
||||
if err == nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
// Set meta data tags
|
||||
metaThermal := map[string]string{
|
||||
"source": r.name,
|
||||
"group": "Temperature",
|
||||
"unit": "degC",
|
||||
}
|
||||
|
||||
nameThermal := "temperature"
|
||||
|
||||
if !clientConfig.isExcluded[nameThermal] {
|
||||
y, err := lp.New(nameThermal, tags, metaThermal,
|
||||
map[string]interface{}{
|
||||
"value": processorMetrics.TemperatureCelsius,
|
||||
},
|
||||
timestamp)
|
||||
if err == nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readMetrics reads redfish thermal, power and processor metrics from the redfish device
|
||||
// configured in clientConfig
|
||||
func (r *RedfishReceiver) readMetrics(clientConfig *RedfishReceiverClientConfig) error {
|
||||
|
||||
// Connect to redfish service
|
||||
c, err := gofish.Connect(clientConfig.gofish)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"readMetrics: gofish.Connect({Username: %v, Endpoint: %v, BasicAuth: %v, HttpTimeout: %v, HttpInsecure: %v}) failed: %v",
|
||||
clientConfig.gofish.Username,
|
||||
clientConfig.gofish.Endpoint,
|
||||
clientConfig.gofish.BasicAuth,
|
||||
clientConfig.gofish.HTTPClient.Timeout,
|
||||
clientConfig.gofish.HTTPClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify,
|
||||
err)
|
||||
}
|
||||
defer c.Logout()
|
||||
|
||||
// Create a session, when required
|
||||
if _, err = c.GetSession(); err != nil {
|
||||
c, err = c.CloneWithSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readMetrics: Failed to create a session: %+w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all chassis managed by this service
|
||||
isChassisListRequired :=
|
||||
clientConfig.doThermalMetrics ||
|
||||
clientConfig.doPowerMetric
|
||||
var chassisList []*redfish.Chassis
|
||||
if isChassisListRequired {
|
||||
chassisList, err = c.Service.Chassis()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readMetrics: c.Service.Chassis() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all computer systems managed by this service
|
||||
isComputerSystemListRequired := clientConfig.doProcessorMetrics
|
||||
var computerSystemList []*redfish.ComputerSystem
|
||||
if isComputerSystemListRequired {
|
||||
computerSystemList, err = c.Service.Systems()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readMetrics: c.Service.Systems() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// read thermal metrics
|
||||
if clientConfig.doThermalMetrics {
|
||||
for _, chassis := range chassisList {
|
||||
err := r.readThermalMetrics(clientConfig, chassis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read power metrics
|
||||
if clientConfig.doPowerMetric {
|
||||
for _, chassis := range chassisList {
|
||||
err = r.readPowerMetrics(clientConfig, chassis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read processor metrics
|
||||
if clientConfig.doProcessorMetrics {
|
||||
// loop for all computer systems
|
||||
for _, system := range computerSystemList {
|
||||
|
||||
// loop for all processors
|
||||
processors, err := system.Processors()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readMetrics: system.Processors() failed: %v", err)
|
||||
}
|
||||
for _, processor := range processors {
|
||||
err := r.readProcessorMetrics(clientConfig, processor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doReadMetrics reads metrics from all configure redfish devices.
|
||||
// To compensate latencies of the Redfish devices a fanout is used.
|
||||
func (r *RedfishReceiver) doReadMetric() {
|
||||
|
||||
// Create wait group and input channel for workers
|
||||
var workerWaitGroup sync.WaitGroup
|
||||
workerInput := make(chan *RedfishReceiverClientConfig, r.config.fanout)
|
||||
|
||||
// Create worker go routines
|
||||
for i := 0; i < r.config.fanout; i++ {
|
||||
// Increment worker wait group counter
|
||||
workerWaitGroup.Add(1)
|
||||
go func() {
|
||||
// Decrement worker wait group counter
|
||||
defer workerWaitGroup.Done()
|
||||
|
||||
// Read power metrics for each client config
|
||||
for clientConfig := range workerInput {
|
||||
err := r.readMetrics(clientConfig)
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Distribute client configs to workers
|
||||
for i := range r.config.ClientConfigs {
|
||||
|
||||
// Check done channel status
|
||||
select {
|
||||
case workerInput <- &r.config.ClientConfigs[i]:
|
||||
case <-r.done:
|
||||
// process done event
|
||||
// Stop workers, clear channel and wait for all workers to finish
|
||||
close(workerInput)
|
||||
for range workerInput {
|
||||
}
|
||||
workerWaitGroup.Wait()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Stop workers and wait for all workers to finish
|
||||
close(workerInput)
|
||||
workerWaitGroup.Wait()
|
||||
}
|
||||
|
||||
// Start starts the redfish receiver
|
||||
func (r *RedfishReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
|
||||
// Start redfish receiver
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
|
||||
// Create ticker
|
||||
ticker := time.NewTicker(r.config.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
r.doReadMetric()
|
||||
|
||||
select {
|
||||
case tickerTime := <-ticker.C:
|
||||
// Check if we missed the ticker event
|
||||
if since := time.Since(tickerTime); since > 5*time.Second {
|
||||
cclog.ComponentInfo(r.name, "Missed ticker event for more then", since)
|
||||
}
|
||||
|
||||
// process ticker event -> continue
|
||||
continue
|
||||
case <-r.done:
|
||||
// process done event
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cclog.ComponentDebug(r.name, "STARTED")
|
||||
}
|
||||
|
||||
// Close closes the redfish receiver
|
||||
func (r *RedfishReceiver) Close() {
|
||||
cclog.ComponentDebug(r.name, "CLOSE")
|
||||
|
||||
// Send the signal and wait
|
||||
close(r.done)
|
||||
r.wg.Wait()
|
||||
|
||||
cclog.ComponentDebug(r.name, "DONE")
|
||||
}
|
||||
|
||||
// NewRedfishReceiver creates a new instance of the redfish receiver
|
||||
// Initialize the receiver by giving it a name and reading in the config JSON
|
||||
func NewRedfishReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(RedfishReceiver)
|
||||
|
||||
// Config options from config file
|
||||
configJSON := struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
// Maximum number of simultaneous redfish connections (default: 64)
|
||||
Fanout int `json:"fanout,omitempty"`
|
||||
// How often the redfish power metrics should be read and send to the sink (default: 30 s)
|
||||
IntervalString string `json:"interval,omitempty"`
|
||||
|
||||
// Control whether a client verifies the server's certificate
|
||||
// (default: true == do not verify server's certificate)
|
||||
HttpInsecure bool `json:"http_insecure,omitempty"`
|
||||
// Time limit for requests made by this HTTP client (default: 10 s)
|
||||
HttpTimeoutString string `json:"http_timeout,omitempty"`
|
||||
|
||||
// Default client username, password and endpoint
|
||||
Username *string `json:"username"` // User name to authenticate with
|
||||
Password *string `json:"password"` // Password to use for authentication
|
||||
Endpoint *string `json:"endpoint"` // URL of the redfish service
|
||||
|
||||
// Globally disable collection of power, processor or thermal metrics
|
||||
DisablePowerMetrics bool `json:"disable_power_metrics"`
|
||||
DisableProcessorMetrics bool `json:"disable_processor_metrics"`
|
||||
DisableThermalMetrics bool `json:"disable_thermal_metrics"`
|
||||
|
||||
// Globally excluded metrics
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
|
||||
ClientConfigs []struct {
|
||||
HostList string `json:"host_list"` // List of hosts with the same client configuration
|
||||
Username *string `json:"username"` // User name to authenticate with
|
||||
Password *string `json:"password"` // Password to use for authentication
|
||||
Endpoint *string `json:"endpoint"` // URL of the redfish service
|
||||
|
||||
// Per client disable collection of power,processor or thermal metrics
|
||||
DisablePowerMetrics bool `json:"disable_power_metrics"`
|
||||
DisableProcessorMetrics bool `json:"disable_processor_metrics"`
|
||||
DisableThermalMetrics bool `json:"disable_thermal_metrics"`
|
||||
|
||||
// Per client excluded metrics
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
} `json:"client_config"`
|
||||
}{
|
||||
// Set defaults values
|
||||
// Allow overwriting these defaults by reading config JSON
|
||||
Fanout: 64,
|
||||
IntervalString: "30s",
|
||||
HttpTimeoutString: "10s",
|
||||
HttpInsecure: true,
|
||||
}
|
||||
|
||||
// Set name
|
||||
r.name = fmt.Sprintf("RedfishReceiver(%s)", name)
|
||||
|
||||
// Create done channel
|
||||
r.done = make(chan bool)
|
||||
|
||||
// Read the redfish receiver specific JSON config
|
||||
if len(config) > 0 {
|
||||
d := json.NewDecoder(bytes.NewReader(config))
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(&configJSON); err != nil {
|
||||
cclog.ComponentError(r.name, "Error reading config:", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Convert interval string representation to duration
|
||||
var err error
|
||||
r.config.Interval, err = time.ParseDuration(configJSON.IntervalString)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Failed to parse duration string interval='%s': %w",
|
||||
configJSON.IntervalString,
|
||||
err,
|
||||
)
|
||||
cclog.Error(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// HTTP timeout duration
|
||||
r.config.HttpTimeout, err = time.ParseDuration(configJSON.HttpTimeoutString)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Failed to parse duration string http_timeout='%s': %w",
|
||||
configJSON.HttpTimeoutString,
|
||||
err,
|
||||
)
|
||||
cclog.Error(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create new http client
|
||||
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
customTransport.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: configJSON.HttpInsecure,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Timeout: r.config.HttpTimeout,
|
||||
Transport: customTransport,
|
||||
}
|
||||
|
||||
// Initialize client configurations
|
||||
r.config.ClientConfigs = make([]RedfishReceiverClientConfig, 0)
|
||||
|
||||
// Create client config from JSON config
|
||||
for i := range configJSON.ClientConfigs {
|
||||
|
||||
clientConfigJSON := &configJSON.ClientConfigs[i]
|
||||
|
||||
var endpoint_pattern string
|
||||
if clientConfigJSON.Endpoint != nil {
|
||||
endpoint_pattern = *clientConfigJSON.Endpoint
|
||||
} else if configJSON.Endpoint != nil {
|
||||
endpoint_pattern = *configJSON.Endpoint
|
||||
} else {
|
||||
err := fmt.Errorf("client config number %v requires endpoint", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var username string
|
||||
if clientConfigJSON.Username != nil {
|
||||
username = *clientConfigJSON.Username
|
||||
} else if configJSON.Username != nil {
|
||||
username = *configJSON.Username
|
||||
} else {
|
||||
err := fmt.Errorf("client config number %v requires username", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var password string
|
||||
if clientConfigJSON.Password != nil {
|
||||
password = *clientConfigJSON.Password
|
||||
} else if configJSON.Password != nil {
|
||||
password = *configJSON.Password
|
||||
} else {
|
||||
err := fmt.Errorf("client config number %v requires password", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Which metrics should be collected
|
||||
doPowerMetric :=
|
||||
!(configJSON.DisablePowerMetrics ||
|
||||
clientConfigJSON.DisablePowerMetrics)
|
||||
doProcessorMetrics :=
|
||||
!(configJSON.DisableProcessorMetrics ||
|
||||
clientConfigJSON.DisableProcessorMetrics)
|
||||
doThermalMetrics :=
|
||||
!(configJSON.DisableThermalMetrics ||
|
||||
clientConfigJSON.DisableThermalMetrics)
|
||||
|
||||
// Is metrics excluded globally or per client
|
||||
isExcluded := make(map[string]bool)
|
||||
for _, key := range clientConfigJSON.ExcludeMetrics {
|
||||
isExcluded[key] = true
|
||||
}
|
||||
for _, key := range configJSON.ExcludeMetrics {
|
||||
isExcluded[key] = true
|
||||
}
|
||||
|
||||
hostList, err := hostlist.Expand(clientConfigJSON.HostList)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("client config number %d failed to parse host list %s: %v",
|
||||
i, clientConfigJSON.HostList, err)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
for _, host := range hostList {
|
||||
|
||||
// Endpoint of the redfish service
|
||||
endpoint := strings.Replace(endpoint_pattern, "%h", host, -1)
|
||||
|
||||
r.config.ClientConfigs = append(
|
||||
r.config.ClientConfigs,
|
||||
RedfishReceiverClientConfig{
|
||||
Hostname: host,
|
||||
isExcluded: isExcluded,
|
||||
doPowerMetric: doPowerMetric,
|
||||
doProcessorMetrics: doProcessorMetrics,
|
||||
doThermalMetrics: doThermalMetrics,
|
||||
skipProcessorMetricsURL: make(map[string]bool),
|
||||
gofish: gofish.ClientConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Endpoint: endpoint,
|
||||
HTTPClient: httpClient,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Compute parallel fanout to use
|
||||
numClients := len(r.config.ClientConfigs)
|
||||
r.config.fanout = configJSON.Fanout
|
||||
if numClients < r.config.fanout {
|
||||
r.config.fanout = numClients
|
||||
}
|
||||
|
||||
if numClients == 0 {
|
||||
err := fmt.Errorf("at least one client config is required")
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check for duplicate client configurations
|
||||
isDuplicate := make(map[string]bool)
|
||||
for i := range r.config.ClientConfigs {
|
||||
host := r.config.ClientConfigs[i].Hostname
|
||||
if isDuplicate[host] {
|
||||
err := fmt.Errorf("Found duplicate client config for host %s", host)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
isDuplicate[host] = true
|
||||
}
|
||||
|
||||
// Give some basic info about redfish receiver status
|
||||
cclog.ComponentInfo(r.name, "Monitoring", numClients, "clients")
|
||||
cclog.ComponentInfo(r.name, "Monitoring interval:", r.config.Interval)
|
||||
cclog.ComponentInfo(r.name, "Monitoring parallel fanout:", r.config.fanout)
|
||||
|
||||
return r, nil
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
## Redfish receiver
|
||||
|
||||
The Redfish receiver uses the [Redfish (specification)](https://www.dmtf.org/standards/redfish) to query thermal and power metrics. Thermal metrics may include various fan speeds and temperatures. Power metrics may include the current power consumption of various hardware components. It may also include the minimum, maximum and average power consumption of these components in a given time interval. The receiver will poll each configured redfish device once in a given interval. Multiple devices can be accessed in parallel to increase throughput.
|
||||
|
||||
### Configuration structure
|
||||
|
||||
```json
|
||||
{
|
||||
"<redfish receiver name>": {
|
||||
"type": "redfish",
|
||||
"username": "<Username>",
|
||||
"password": "<Password>",
|
||||
"endpoint": "https://%h-bmc",
|
||||
"exclude_metrics": [ "min_consumed_watts" ],
|
||||
"client_config": [
|
||||
{
|
||||
"host_list": "n[1,2-4]"
|
||||
},
|
||||
{
|
||||
"host_list": "n5"
|
||||
"disable_power_metrics": true
|
||||
},
|
||||
{
|
||||
"host_list": "n6" ],
|
||||
"username": "<Username 2>",
|
||||
"password": "<Password 2>",
|
||||
"endpoint": "https://%h-BMC",
|
||||
"disable_thermal_metrics": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Global settings:
|
||||
|
||||
- `fanout`: Maximum number of simultaneous redfish connections (default: 64)
|
||||
- `interval`: How often the redfish power metrics should be read and send to the sink (default: 30 s)
|
||||
- `http_insecure`: Control whether a client verifies the server's certificate (default: true == do not verify server's certificate)
|
||||
- `http_timeout`: Time limit for requests made by this HTTP client (default: 10 s)
|
||||
|
||||
Global and per redfish device settings (per redfish device settings overwrite the global settings):
|
||||
|
||||
- `disable_power_metrics`: disable collection of power metrics
|
||||
- `disable_processor_metrics`: disable collection of processor metrics
|
||||
- `disable_thermal_metrics`: disable collection of thermal metrics
|
||||
- `exclude_metrics`: list of excluded metrics
|
||||
- `username`: User name to authenticate with
|
||||
- `password`: Password to use for authentication
|
||||
- `endpoint`: URL of the redfish service (placeholder `%h` gets replaced by the hostname)
|
||||
|
||||
Per redfish device settings:
|
||||
|
||||
- `host_list`: List of hosts with the same client configuration
|
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
)
|
||||
|
||||
// SampleReceiver configuration: receiver type, listen address, port
|
||||
@@ -36,26 +36,16 @@ func (r *SampleReceiver) Start() {
|
||||
|
||||
// or use own go routine but always make sure it exits
|
||||
// as soon as it gets the signal of the r.done channel
|
||||
//
|
||||
// r.done = make(chan bool)
|
||||
// r.wg.Add(1)
|
||||
// go func() {
|
||||
// defer r.wg.Done()
|
||||
//
|
||||
// // Create ticker
|
||||
// ticker := time.NewTicker(30 * time.Second)
|
||||
// defer ticker.Stop()
|
||||
//
|
||||
// for {
|
||||
// readMetric()
|
||||
// select {
|
||||
// case <-ticker.C:
|
||||
// // process ticker event -> continue
|
||||
// continue
|
||||
// case <-r.done:
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// for {
|
||||
// select {
|
||||
// case <-r.done:
|
||||
// r.wg.Done()
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// r.wg.Done()
|
||||
// }()
|
||||
}
|
||||
|
||||
|
@@ -1,294 +0,0 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
|
||||
"github.com/gosnmp/gosnmp"
|
||||
)
|
||||
|
||||
type SNMPReceiverTargetConfig struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Community string `json:"community,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
timeout time.Duration
|
||||
Version string `json:"version,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
TypeId string `json:"type-id,omitempty"`
|
||||
SubType string `json:"subtype,omitempty"`
|
||||
SubTypeId string `json:"subtype-id,omitempty"`
|
||||
}
|
||||
|
||||
type SNMPReceiverMetricConfig struct {
|
||||
Name string `json:"name"`
|
||||
OID string `json:"oid"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
}
|
||||
|
||||
// SNMPReceiver configuration: receiver type, listen address, port
|
||||
type SNMPReceiverConfig struct {
|
||||
Type string `json:"type"`
|
||||
Targets []SNMPReceiverTargetConfig `json:"targets"`
|
||||
Metrics []SNMPReceiverMetricConfig `json:"metrics"`
|
||||
ReadInterval string `json:"read_interval,omitempty"`
|
||||
}
|
||||
|
||||
type SNMPReceiver struct {
|
||||
receiver
|
||||
config SNMPReceiverConfig
|
||||
|
||||
// Storage for static information
|
||||
meta map[string]string
|
||||
tags map[string]string
|
||||
// Use in case of own go routine
|
||||
done chan bool
|
||||
wg sync.WaitGroup
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func validOid(oid string) bool {
|
||||
// Regex from https://github.com/BornToBeRoot/NETworkManager/blob/6805740762bf19b95051c7eaa73cf2b4727733c3/Source/NETworkManager.Utilities/RegexHelper.cs#L88
|
||||
// Match on leading dot added by Thomas Gruber <thomas.gruber@fau.de>
|
||||
match, err := regexp.MatchString(`^[\.]?[012]\.(?:[0-9]|[1-3][0-9])(\.\d+)*$`, oid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
func (r *SNMPReceiver) readTarget(target SNMPReceiverTargetConfig, output chan lp.CCMetric) {
|
||||
port := uint16(161)
|
||||
comm := "public"
|
||||
timeout := time.Duration(1) * time.Second
|
||||
version := gosnmp.Version2c
|
||||
timestamp := time.Now()
|
||||
if target.Port > 0 {
|
||||
port = uint16(target.Port)
|
||||
}
|
||||
if len(target.Community) > 0 {
|
||||
comm = target.Community
|
||||
}
|
||||
if target.timeout > 0 {
|
||||
timeout = target.timeout
|
||||
}
|
||||
if len(target.Version) > 0 {
|
||||
switch target.Version {
|
||||
case "1":
|
||||
version = gosnmp.Version1
|
||||
case "2c":
|
||||
version = gosnmp.Version2c
|
||||
case "3":
|
||||
version = gosnmp.Version3
|
||||
default:
|
||||
cclog.ComponentError(r.name, "Invalid SNMP version ", target.Version)
|
||||
return
|
||||
}
|
||||
}
|
||||
params := &gosnmp.GoSNMP{
|
||||
Target: target.Hostname,
|
||||
Port: port,
|
||||
Community: comm,
|
||||
Version: version,
|
||||
Timeout: timeout,
|
||||
}
|
||||
err := params.Connect()
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return
|
||||
}
|
||||
for _, metric := range r.config.Metrics {
|
||||
if !validOid(metric.OID) {
|
||||
cclog.ComponentDebug(r.name, "Skipping ", metric.Name, ", not valid OID: ", metric.OID)
|
||||
continue
|
||||
}
|
||||
oids := make([]string, 0)
|
||||
name := gosnmp.SnmpPDU{
|
||||
Value: metric.Name,
|
||||
Name: metric.Name,
|
||||
}
|
||||
nameidx := -1
|
||||
value := gosnmp.SnmpPDU{
|
||||
Value: nil,
|
||||
Name: metric.OID,
|
||||
}
|
||||
valueidx := -1
|
||||
unit := gosnmp.SnmpPDU{
|
||||
Value: metric.Unit,
|
||||
Name: metric.Unit,
|
||||
}
|
||||
unitidx := -1
|
||||
idx := 0
|
||||
if validOid(metric.Name) {
|
||||
oids = append(oids, metric.Name)
|
||||
nameidx = idx
|
||||
idx = idx + 1
|
||||
}
|
||||
if validOid(metric.OID) {
|
||||
oids = append(oids, metric.OID)
|
||||
valueidx = idx
|
||||
idx = idx + 1
|
||||
}
|
||||
if len(metric.Unit) > 0 && validOid(metric.Unit) {
|
||||
oids = append(oids, metric.Unit)
|
||||
unitidx = idx
|
||||
}
|
||||
//cclog.ComponentDebug(r.name, len(oids), oids)
|
||||
result, err := params.Get(oids)
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, "failed to get data for OIDs ", strings.Join(oids, ","), ": ", err.Error())
|
||||
continue
|
||||
}
|
||||
if nameidx >= 0 && len(result.Variables) > nameidx {
|
||||
name = result.Variables[nameidx]
|
||||
}
|
||||
if valueidx >= 0 && len(result.Variables) > valueidx {
|
||||
value = result.Variables[valueidx]
|
||||
}
|
||||
if unitidx >= 0 && len(result.Variables) > unitidx {
|
||||
unit = result.Variables[unitidx]
|
||||
}
|
||||
tags := r.tags
|
||||
if len(target.Type) > 0 {
|
||||
tags["type"] = target.Type
|
||||
}
|
||||
if len(target.TypeId) > 0 {
|
||||
tags["type-id"] = target.TypeId
|
||||
}
|
||||
if len(target.SubType) > 0 {
|
||||
tags["stype"] = target.SubType
|
||||
}
|
||||
if len(target.SubTypeId) > 0 {
|
||||
tags["stype-id"] = target.SubTypeId
|
||||
}
|
||||
if value.Value != nil {
|
||||
y, err := lp.New(name.Value.(string), tags, r.meta, map[string]interface{}{"value": value.Value}, timestamp)
|
||||
if err == nil {
|
||||
if len(unit.Name) > 0 && unit.Value != nil {
|
||||
y.AddMeta("unit", unit.Value.(string))
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
params.Conn.Close()
|
||||
}
|
||||
|
||||
// Implement functions required for Receiver interface
|
||||
// Start(), Close()
|
||||
// See: metricReceiver.go
|
||||
|
||||
func (r *SNMPReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
|
||||
r.done = make(chan bool)
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
|
||||
// Create ticker
|
||||
ticker := time.NewTicker(r.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// process ticker event -> continue
|
||||
if r.sink != nil {
|
||||
for _, t := range r.config.Targets {
|
||||
select {
|
||||
case <-r.done:
|
||||
return
|
||||
default:
|
||||
r.readTarget(t, r.sink)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
case <-r.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Close receiver: close network connection, close files, close libraries, ...
|
||||
func (r *SNMPReceiver) Close() {
|
||||
cclog.ComponentDebug(r.name, "CLOSE")
|
||||
|
||||
r.done <- true
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
// New function to create a new instance of the receiver
|
||||
// Initialize the receiver by giving it a name and reading in the config JSON
|
||||
func NewSNMPReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
var err error = nil
|
||||
r := new(SNMPReceiver)
|
||||
|
||||
// Set name of SNMPReceiver
|
||||
// The name should be chosen in such a way that different instances of SNMPReceiver can be distinguished
|
||||
r.name = fmt.Sprintf("SNMPReceiver(%s)", name)
|
||||
|
||||
// Set static information
|
||||
r.meta = map[string]string{"source": r.name, "group": "SNMP"}
|
||||
r.tags = map[string]string{"type": "node"}
|
||||
|
||||
// Set defaults in r.config
|
||||
r.interval = time.Duration(30) * time.Second
|
||||
|
||||
// Read the sample receiver specific JSON config
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &r.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, "Error reading config:", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all required fields in the configuration are set
|
||||
if len(r.config.Targets) == 0 {
|
||||
err = fmt.Errorf("no targets configured, exiting")
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(r.config.Metrics) == 0 {
|
||||
err = fmt.Errorf("no metrics configured, exiting")
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(r.config.ReadInterval) > 0 {
|
||||
d, err := time.ParseDuration(r.config.ReadInterval)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse read interval, exiting")
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
r.interval = d
|
||||
}
|
||||
newtargets := make([]SNMPReceiverTargetConfig, 0)
|
||||
for _, t := range r.config.Targets {
|
||||
t.timeout = time.Duration(1) * time.Second
|
||||
if len(t.Timeout) > 0 {
|
||||
d, err := time.ParseDuration(t.Timeout)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse interval for target %s", t.Hostname)
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
continue
|
||||
}
|
||||
t.timeout = d
|
||||
}
|
||||
newtargets = append(newtargets, t)
|
||||
}
|
||||
r.config.Targets = newtargets
|
||||
|
||||
return r, nil
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
# SNMP Receiver
|
||||
|
||||
```json
|
||||
"<name>": {
|
||||
"type": "snmp",
|
||||
"read_interval": "30s",
|
||||
"targets" : [{
|
||||
"hostname" : "host1.example.com",
|
||||
"port" : 161,
|
||||
"community": "public",
|
||||
"timeout" : 1,
|
||||
}],
|
||||
"metrics" : [
|
||||
{
|
||||
"name": "sensor1",
|
||||
"value": "1.3.6.1.2.1.1.4.0",
|
||||
"unit": "1.3.6.1.2.1.1.7.0",
|
||||
},
|
||||
{
|
||||
"name": "1.3.6.1.2.1.1.2.0",
|
||||
"value": "1.3.6.1.2.1.1.4.0",
|
||||
"unit": "mb/s",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `snmp` receiver uses [gosnmp](https://github.com/gosnmp/gosnmp) to read metrics from network-attached devices.
|
||||
|
||||
The configuration of SNMP is quite extensive due to it's flexibility.
|
||||
|
||||
## Configuration
|
||||
|
||||
- `type` has to be `snmp`
|
||||
- `read_interval` as duration like '1s' or '20s' (default '30s')
|
||||
|
||||
For the receiver, the configuration is split in two parts:
|
||||
### Target configuration
|
||||
|
||||
Each network-attached device that should be queried. A target consits of
|
||||
- `hostname`
|
||||
- `port` (default 161)
|
||||
- `community` (default `public`)
|
||||
- `timeout` as duration like '1s' or '20s' (default '1s')
|
||||
- `version` SNMP version `X` (`X` in `1`, `2c`, `3`) (default `2c`)
|
||||
- `type` to specify `type` tag for the target (default `node`)
|
||||
- `type-id` to specify `type-id` tag for the target
|
||||
- `stype` to specify `stype` tag (sub type) for the target
|
||||
- `stype-id` to specify `stype-id` tag for the target
|
||||
|
||||
### Metric configuration
|
||||
- `name` can be an OID or a user-given string
|
||||
- `value` has to be an OID
|
||||
- `unit` can be empty, an OID or a user-given string
|
||||
|
||||
If a OID is used for `name` or `unit`, the receiver will use the returned values to create the output metric. If there are any issues with the returned values, it uses the `OID`.
|
||||
|
||||
## Testing
|
||||
|
||||
For testing an SNMP endpoint and OIDs, you can use [`scripts/snmpReceiverTest`](../scripts/snmpReceiverTest)
|
@@ -6,7 +6,7 @@ CC_HOME=/tmp
|
||||
|
||||
LOG_DIR=/var/log
|
||||
|
||||
DATA_DIR=/var/lib/cc-metric-collector
|
||||
DATA_DIR=/var/lib/grafana
|
||||
|
||||
MAX_OPEN_FILES=10000
|
||||
|
||||
@@ -15,9 +15,3 @@ CONF_DIR=/etc/cc-metric-collector
|
||||
CONF_FILE=/etc/cc-metric-collector/cc-metric-collector.json
|
||||
|
||||
RESTART_ON_UPGRADE=true
|
||||
|
||||
# Golang runtime debugging. (see: https://pkg.go.dev/runtime)
|
||||
# GODEBUG=gctrace=1
|
||||
|
||||
# Golang garbage collection target percentage
|
||||
# GOGC=100
|
||||
|
@@ -1,16 +0,0 @@
|
||||
|
||||
#!/usr/bin/make -f
|
||||
# You must remove unused comment lines for the released package.
|
||||
#export DH_VERBOSE = 1
|
||||
#export DEB_BUILD_MAINT_OPTIONS = hardening=+all
|
||||
#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic
|
||||
#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_build:
|
||||
make
|
||||
|
||||
override_dh_auto_install:
|
||||
make PREFIX=/usr install
|
@@ -19,7 +19,7 @@
|
||||
PATH=/bin:/usr/bin:/sbin:/usr/sbin
|
||||
NAME=cc-metric-collector
|
||||
DESC="ClusterCockpit metric collector"
|
||||
DEFAULT=/etc/default/${NAME}
|
||||
DEFAULT=/etc/default/${NAME}.json
|
||||
|
||||
CC_USER=clustercockpit
|
||||
CC_GROUP=clustercockpit
|
||||
@@ -75,7 +75,7 @@ case "$1" in
|
||||
fi
|
||||
|
||||
# Start Daemon
|
||||
start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$CC_USER" -c "$CC_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $CC_OPTS
|
||||
start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$CC_USER" -c "$CC_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $DAEMON_OPTS
|
||||
return=$?
|
||||
if [ $return -eq 0 ]
|
||||
then
|
||||
|
@@ -3,6 +3,7 @@ Description=ClusterCockpit metric collector
|
||||
Documentation=https://github.com/ClusterCockpit/cc-metric-collector
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
After=postgresql.service mariadb.service mysql.service
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/cc-metric-collector
|
||||
@@ -13,7 +14,7 @@ Restart=on-failure
|
||||
WorkingDirectory=/tmp
|
||||
RuntimeDirectory=cc-metric-collector
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/bin/cc-metric-collector --config=${CONF_FILE}
|
||||
ExecStart=/usr/sbin/cc-metric-collector --config=${CONF_FILE}
|
||||
LimitNOFILE=10000
|
||||
TimeoutStopSec=20
|
||||
UMask=0027
|
||||
|
@@ -10,8 +10,6 @@ BuildRequires: go-toolset
|
||||
BuildRequires: systemd-rpm-macros
|
||||
# for header downloads
|
||||
BuildRequires: wget
|
||||
# Recommended when using the sysusers_create_package macro
|
||||
Requires(pre): /usr/bin/systemd-sysusers
|
||||
|
||||
Provides: %{name} = %{version}
|
||||
|
||||
@@ -29,7 +27,7 @@ make
|
||||
|
||||
|
||||
%install
|
||||
install -Dpm 0750 %{name} %{buildroot}%{_bindir}/%{name}
|
||||
install -Dpm 0750 %{name} %{buildroot}%{_sbindir}/%{name}
|
||||
install -Dpm 0600 config.json %{buildroot}%{_sysconfdir}/%{name}/%{name}.json
|
||||
install -Dpm 0600 collectors.json %{buildroot}%{_sysconfdir}/%{name}/collectors.json
|
||||
install -Dpm 0600 sinks.json %{buildroot}%{_sysconfdir}/%{name}/sinks.json
|
||||
@@ -44,7 +42,7 @@ install -Dpm 0644 scripts/%{name}.sysusers %{buildroot}%{_sysusersdir}/%{name}.c
|
||||
# go test should be here... :)
|
||||
|
||||
%pre
|
||||
%sysusers_create_package %{name} scripts/%{name}.sysusers
|
||||
%sysusers_create_package scripts/%{name}.sysusers
|
||||
|
||||
%post
|
||||
%systemd_post %{name}.service
|
||||
@@ -54,7 +52,7 @@ install -Dpm 0644 scripts/%{name}.sysusers %{buildroot}%{_sysusersdir}/%{name}.c
|
||||
|
||||
%files
|
||||
# Binary
|
||||
%attr(-,clustercockpit,clustercockpit) %{_bindir}/%{name}
|
||||
%attr(-,clustercockpit,clustercockpit) %{_sbindir}/%{name}
|
||||
# Config
|
||||
%dir %{_sysconfdir}/%{name}
|
||||
%attr(0600,clustercockpit,clustercockpit) %config(noreplace) %{_sysconfdir}/%{name}/%{name}.json
|
||||
|
@@ -39,13 +39,13 @@ def group_to_json(groupfile):
|
||||
llist = re.split("\s+", line)
|
||||
calc = llist[-1]
|
||||
metric = " ".join(llist[:-1])
|
||||
scope = "hwthread"
|
||||
scope = "cpu"
|
||||
if "BOX" in calc:
|
||||
scope = "socket"
|
||||
if "PWR" in calc:
|
||||
scope = "socket"
|
||||
|
||||
m = {"name" : metric, "calc": calc, "type" : scope, "publish" : True}
|
||||
m = {"name" : metric, "calc": calc, "scope" : scope, "publish" : True}
|
||||
metrics.append(m)
|
||||
return {"events" : events, "metrics" : metrics}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user