mirror of
https://github.com/ClusterCockpit/cc-metric-collector.git
synced 2025-07-19 11:21:41 +02:00
Compare commits
129 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
31a38bc17d | ||
|
dbdec1eab8 | ||
|
0d31ec481b | ||
|
e22c3287e9 | ||
|
8d85bd53f1 | ||
|
186a62a86b | ||
|
e098c33179 | ||
|
38d4e0a730 | ||
|
54d14519ca | ||
|
c35ac9dba8 | ||
|
c019f8e7ad | ||
|
fb6f6a4daa | ||
|
9d6d0dbd93 | ||
|
c2d4272fdf | ||
|
8c73095548 | ||
|
31c5c89a5a | ||
|
bf9c7e1830 | ||
|
48d34bf564 | ||
|
a1d85fa886 | ||
|
96ee16398e | ||
|
e7b8088c41 | ||
|
017cd58247 | ||
|
36dd440864 | ||
|
7b098e0b1b | ||
|
229a57b16a | ||
|
70a9530aba | ||
|
2f0b6057ca | ||
|
69f7c19659 | ||
|
ecdb4c1bcf | ||
|
4d5b1adbc8 | ||
|
28348bd108 | ||
|
a3b9d8a90b | ||
|
7e43e9171e | ||
|
5d25a7bf12 | ||
|
83b4343310 | ||
|
f1d3cabdc6 | ||
|
4763733d8d | ||
|
2a014b6fba | ||
|
16e898ecca | ||
|
50479f9325 | ||
|
e0e91844bc | ||
|
296225f3a8 | ||
|
43bcce6fb5 | ||
|
622e94ae0e | ||
|
c506114480 | ||
|
657543dded | ||
|
4851382ad7 | ||
|
b66fdd1436 | ||
|
3f76947f54 | ||
|
c182d295f4 | ||
|
beebcd7145 | ||
|
57629a2e0a | ||
|
082eea525a | ||
|
aa1afd745e | ||
|
2b8266d1d2 | ||
|
992b19d354 | ||
|
0b08ca9ae0 | ||
|
f6dae7c013 | ||
|
1de3dda7be | ||
|
73f22c1041 | ||
|
c9b8fcdaa7 | ||
|
17f37583fc | ||
|
fb9ea992ea | ||
|
21edca5f88 | ||
|
d835724d93 | ||
|
3cf2f69a07 | ||
|
e7f7e68095 | ||
|
c5082bbffe | ||
|
c0e600269a | ||
|
f2486abeab | ||
|
21864e0ac4 | ||
|
3157386b3e | ||
|
8c668fcc6f | ||
|
1961edc659 | ||
|
547bc0461f | ||
|
7f62975a68 | ||
|
5e64c01c9d | ||
|
ff08eaeb43 | ||
|
64c41be34c | ||
|
f4af520b2a | ||
|
f1d2828e1d | ||
|
07af660e50 | ||
|
31f10b3163 | ||
|
9ece27eec6 | ||
|
af4c468ab5 | ||
|
b3030a8d44 | ||
|
db04c8fbae | ||
|
fdbdb79527 | ||
|
948c34d74d | ||
|
60de21c41e | ||
|
276c00442a | ||
|
c61b8d2877 | ||
|
6023abd028 | ||
|
0753c81156 | ||
|
092e7f6a71 | ||
|
f7e8b52667 | ||
|
02baef8c71 | ||
|
33d954f767 | ||
|
a5325a6535 | ||
|
d40163cf8f | ||
|
33fec95eac | ||
|
2c08e53be4 | ||
|
a2f9b23e85 | ||
|
4c1263312b | ||
|
d98076c792 | ||
|
a203370aaa | ||
|
f099a311a0 | ||
|
940623585c | ||
|
fe3a8d59b0 | ||
|
bac1f18b1d | ||
|
87ecb12c6f | ||
|
c8bca59de4 | ||
|
16c03d2aa2 | ||
|
2f044f4b58 | ||
|
f911ff802c | ||
|
2f36375470 | ||
|
6843902909 | ||
|
73981527d3 | ||
|
d542f32baa | ||
|
6b6566b0aa | ||
|
3598aed090 | ||
|
24e12ccc57 | ||
|
18a226183c | ||
|
9cfbe10247 | ||
|
66275ecf74 | ||
|
b4cc6d54ea | ||
|
45714fe337 | ||
|
a97c705f4c | ||
|
ae64eddcc8 |
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": 5,
|
||||
"duration": 1
|
||||
"interval": "5s",
|
||||
"duration": "1s"
|
||||
}
|
||||
|
4
.github/ci-sinks.json
vendored
4
.github/ci-sinks.json
vendored
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"testoutput" : {
|
||||
"type" : "stdout",
|
||||
"meta_as_tags" : true
|
||||
"meta_as_tags" : [
|
||||
"unit"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
187
.github/workflows/Release.yml
vendored
Normal file
187
.github/workflows/Release.yml
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
|
||||
# Workflow name
|
||||
name: Release
|
||||
|
||||
# Run on tag push
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
|
||||
#
|
||||
# Build on AlmaLinux 8.5 using go-toolset
|
||||
#
|
||||
AlmaLinux-RPM-build:
|
||||
runs-on: ubuntu-latest
|
||||
# See: https://hub.docker.com/_/almalinux
|
||||
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:
|
||||
rpm : ${{steps.rpmrename.outputs.RPM}}
|
||||
srpm : ${{steps.rpmrename.outputs.SRPM}}
|
||||
steps:
|
||||
|
||||
# Use dnf to install development packages
|
||||
- name: Install development packages
|
||||
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@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Use dnf to install build dependencies
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf --assumeyes install 'dnf-command(builddep)'
|
||||
dnf --assumeyes install which
|
||||
dnf --assumeyes builddep scripts/cc-metric-collector.spec
|
||||
|
||||
- name: RPM build MetricCollector
|
||||
id: rpmbuild
|
||||
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 '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/alma85/)
|
||||
id: rpmrename
|
||||
run: |
|
||||
OLD_RPM="${{steps.rpmbuild.outputs.RPM}}"
|
||||
OLD_SRPM="${{steps.rpmbuild.outputs.SRPM}}"
|
||||
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}"
|
||||
echo "::set-output name=RPM::${NEW_RPM}"
|
||||
|
||||
# See: https://github.com/actions/upload-artifact
|
||||
- name: Save RPM as artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
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.5
|
||||
path: ${{ steps.rpmrename.outputs.SRPM }}
|
||||
|
||||
#
|
||||
# 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.5-226.1645809065
|
||||
# The job outputs link to the outputs of the 'rpmbuild' step
|
||||
outputs:
|
||||
rpm : ${{steps.rpmbuild.outputs.RPM}}
|
||||
srpm : ${{steps.rpmbuild.outputs.SRPM}}
|
||||
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
|
||||
|
||||
# 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@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Use dnf to install build dependencies
|
||||
- name: Install build dependencies
|
||||
run: dnf --assumeyes --disableplugin=subscription-manager builddep scripts/cc-metric-collector.spec
|
||||
|
||||
- name: RPM build MetricCollector
|
||||
id: rpmbuild
|
||||
run: make RPM
|
||||
|
||||
# See: https://github.com/actions/upload-artifact
|
||||
- name: Save RPM as artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector RPM for UBI 8
|
||||
path: ${{ steps.rpmbuild.outputs.RPM }}
|
||||
- name: Save SRPM as artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector SRPM for UBI 8
|
||||
path: ${{ steps.rpmbuild.outputs.SRPM }}
|
||||
|
||||
#
|
||||
# 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]
|
||||
|
||||
steps:
|
||||
# See: https://github.com/actions/download-artifact
|
||||
- name: Download AlmaLinux 8.5 RPM
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
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.5
|
||||
|
||||
- name: Download UBI 8 RPM
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector RPM for UBI 8
|
||||
- name: Download UBI 8 SRPM
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: cc-metric-collector SRPM for UBI 8
|
||||
|
||||
# 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.
|
||||
# The gh-release action afterwards does not accept file lists but all
|
||||
# files have to be listed at 'files'. The step creates one output per
|
||||
# RPM package (2 per distro)
|
||||
- name: Set RPM variables
|
||||
id: files
|
||||
run: |
|
||||
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}}")
|
||||
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 "::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}"
|
||||
|
||||
# See: https://github.com/softprops/action-gh-release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: cc-metric-collector-${{github.ref_name}}
|
||||
files: |
|
||||
${{ steps.files.outputs.ALMA_85_RPM }}
|
||||
${{ steps.files.outputs.ALMA_85_SRPM }}
|
||||
${{ steps.files.outputs.UBI_8_RPM }}
|
||||
${{ steps.files.outputs.UBI_8_SRPM }}
|
58
.github/workflows/rpmbuild.yml
vendored
58
.github/workflows/rpmbuild.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Run RPM Build
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build-alma-8_5:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: TomTheBear/rpmbuild@alma8.5
|
||||
id: rpm
|
||||
name: Build RPM package on AlmaLinux 8.5
|
||||
with:
|
||||
spec_file: "./scripts/cc-metric-collector.spec"
|
||||
- name: Save RPM as artifact
|
||||
uses: actions/upload-artifact@v1.0.0
|
||||
with:
|
||||
name: cc-metric-collector RPM AlmaLinux 8.5
|
||||
path: ${{ steps.rpm.outputs.rpm_path }}
|
||||
- name: Save SRPM as artifact
|
||||
uses: actions/upload-artifact@v1.0.0
|
||||
with:
|
||||
name: cc-metric-collector SRPM AlmaLinux 8.5
|
||||
path: ${{ steps.rpm.outputs.source_rpm_path }}
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: cc-metric-collector-${{github.ref_name}}
|
||||
files: |
|
||||
${{ steps.rpm.outputs.source_rpm_path }}
|
||||
${{ steps.rpm.outputs.rpm_path }}
|
||||
build-rhel-ubi8:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: TomTheBear/rpmbuild@rh-ubi8
|
||||
id: rpm
|
||||
name: Build RPM package on Red Hat Universal Base Image 8
|
||||
with:
|
||||
spec_file: "./scripts/cc-metric-collector.spec"
|
||||
- name: Save RPM as artifact
|
||||
uses: actions/upload-artifact@v1.0.0
|
||||
with:
|
||||
name: cc-metric-collector RPM Red Hat Universal Base Image 8
|
||||
path: ${{ steps.rpm.outputs.rpm_path }}
|
||||
- name: Save SRPM as artifact
|
||||
uses: actions/upload-artifact@v1.0.0
|
||||
with:
|
||||
name: cc-metric-collector SRPM Red Hat Universal Base Image 8
|
||||
path: ${{ steps.rpm.outputs.source_rpm_path }}
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
${{ steps.rpm.outputs.source_rpm_path }}
|
||||
${{ steps.rpm.outputs.rpm_path }}
|
68
.github/workflows/runonce.yml
vendored
68
.github/workflows/runonce.yml
vendored
@@ -1,46 +1,86 @@
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
|
||||
# Workflow name
|
||||
name: Run Test
|
||||
|
||||
# Run on event push
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
#
|
||||
# Job build-1-18
|
||||
# Build on latest Ubuntu using golang version 1.18
|
||||
#
|
||||
build-1-18:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
# Checkout git repository and submodules
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# See: https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18.2'
|
||||
|
||||
- name: Build MetricCollector
|
||||
run: make
|
||||
|
||||
- name: Run MetricCollector once
|
||||
run: ./cc-metric-collector --once --config .github/ci-config.json
|
||||
|
||||
#
|
||||
# Job build-1-17
|
||||
# Build on latest Ubuntu using golang version 1.17
|
||||
#
|
||||
build-1-17:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
# Checkout git repository and submodules
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# See: https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v2.1.5
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.17.6'
|
||||
|
||||
- name: Setup Ganglia
|
||||
run: sudo apt install ganglia-monitor libganglia1
|
||||
go-version: '1.17.7'
|
||||
|
||||
- name: Build MetricCollector
|
||||
run: make
|
||||
|
||||
- name: Run MetricCollector
|
||||
- name: Run MetricCollector once
|
||||
run: ./cc-metric-collector --once --config .github/ci-config.json
|
||||
|
||||
#
|
||||
# Job build-1-16
|
||||
# Build on latest Ubuntu using golang version 1.16
|
||||
#
|
||||
build-1-16:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# See: https://github.com/marketplace/actions/checkout
|
||||
# Checkout git repository and submodules
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
# See: https://github.com/marketplace/actions/setup-go-environment
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v2.1.5
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.16.7' # The version AlmaLinux 8.5 uses
|
||||
|
||||
- name: Setup Ganglia
|
||||
run: sudo apt install ganglia-monitor libganglia1
|
||||
go-version: '1.16.7' # The version AlmaLinux 8.5 uses
|
||||
|
||||
- name: Build MetricCollector
|
||||
run: make
|
||||
|
||||
- name: Run MetricCollectorlibganglia1
|
||||
- name: Run MetricCollector once
|
||||
run: ./cc-metric-collector --once --config .github/ci-config.json
|
||||
|
108
Makefile
108
Makefile
@@ -1,5 +1,5 @@
|
||||
APP = cc-metric-collector
|
||||
GOSRC_APP := metric-collector.go
|
||||
GOSRC_APP := cc-metric-collector.go
|
||||
GOSRC_COLLECTORS := $(wildcard collectors/*.go)
|
||||
GOSRC_SINKS := $(wildcard sinks/*.go)
|
||||
GOSRC_RECEIVERS := $(wildcard receivers/*.go)
|
||||
@@ -15,39 +15,121 @@ COMPONENT_DIRS := collectors \
|
||||
internal/ccTopology \
|
||||
internal/multiChanTicker
|
||||
|
||||
BINDIR = bin
|
||||
GOBIN = $(shell which go)
|
||||
|
||||
|
||||
.PHONY: all
|
||||
all: $(APP)
|
||||
|
||||
$(APP): $(GOSRC)
|
||||
if [ "$(shell $(GOBIN) version | cut -d' ' -f 3 | cut -d '.' -f1-2)" = "go1.16" ]; then \
|
||||
echo "1.16"; \
|
||||
cp go.mod.1.16 go.mod; \
|
||||
else \
|
||||
echo "1.17+"; \
|
||||
cp go.mod.1.17+ go.mod; \
|
||||
fi
|
||||
make -C collectors
|
||||
make -C sinks
|
||||
go get
|
||||
go build -o $(APP) $(GOSRC_APP)
|
||||
$(GOBIN) get
|
||||
$(GOBIN) build -o $(APP) $(GOSRC_APP)
|
||||
|
||||
install: $(APP)
|
||||
@WORKSPACE=$(PREFIX)
|
||||
@if [ -z "$${WORKSPACE}" ]; then exit 1; fi
|
||||
@mkdir --parents --verbose $${WORKSPACE}/usr/$(BINDIR)
|
||||
@install -Dpm 755 $(APP) $${WORKSPACE}/usr/$(BINDIR)/$(APP)
|
||||
@mkdir --parents --verbose $${WORKSPACE}/etc/cc-metric-collector $${WORKSPACE}/etc/default $${WORKSPACE}/etc/systemd/system $${WORKSPACE}/etc/init.d
|
||||
@install -Dpm 600 config.json $${WORKSPACE}/etc/cc-metric-collector/cc-metric-collector.json
|
||||
@sed -i -e s+"\"./"+"\"/etc/cc-metric-collector/"+g $${WORKSPACE}/etc/cc-metric-collector/cc-metric-collector.json
|
||||
@install -Dpm 600 sinks.json $${WORKSPACE}/etc/cc-metric-collector/sinks.json
|
||||
@install -Dpm 600 collectors.json $${WORKSPACE}/etc/cc-metric-collector/collectors.json
|
||||
@install -Dpm 600 router.json $${WORKSPACE}/etc/cc-metric-collector/router.json
|
||||
@install -Dpm 600 receivers.json $${WORKSPACE}/etc/cc-metric-collector/receivers.json
|
||||
@install -Dpm 600 scripts/cc-metric-collector.config $${WORKSPACE}/etc/default/cc-metric-collector
|
||||
@install -Dpm 644 scripts/cc-metric-collector.service $${WORKSPACE}/etc/systemd/system/cc-metric-collector.service
|
||||
@install -Dpm 644 scripts/cc-metric-collector.init $${WORKSPACE}/etc/init.d/cc-metric-collector
|
||||
|
||||
|
||||
.PHONY: clean
|
||||
.ONESHELL:
|
||||
clean:
|
||||
@for COMP in $(COMPONENT_DIRS); do if [ -e $$COMP/Makefile ]; then make -C $$COMP clean; fi; done
|
||||
@for COMP in $(COMPONENT_DIRS); do if [ -e $$COMP/Makefile ]; then make -C $$COMP clean; fi; done
|
||||
rm -f $(APP)
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
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
|
||||
$(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
|
||||
|
||||
|
||||
# Examine Go source code and reports suspicious constructs
|
||||
.PHONY: vet
|
||||
vet:
|
||||
go vet ./...
|
||||
$(GOBIN) 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:
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
$$(go env GOPATH)/bin/staticcheck ./...
|
||||
$(GOBIN) install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
$$($(GOBIN) env GOPATH)/bin/staticcheck ./...
|
||||
|
||||
.ONESHELL:
|
||||
.PHONY: RPM
|
||||
RPM: scripts/cc-metric-collector.spec
|
||||
@WORKSPACE="$${PWD}"
|
||||
@SPECFILE="$${WORKSPACE}/scripts/cc-metric-collector.spec"
|
||||
# Setup RPM build tree
|
||||
@eval $$(rpm --eval "ARCH='%{_arch}' RPMDIR='%{_rpmdir}' SOURCEDIR='%{_sourcedir}' SPECDIR='%{_specdir}' SRPMDIR='%{_srcrpmdir}' BUILDDIR='%{_builddir}'")
|
||||
@mkdir --parents --verbose "$${RPMDIR}" "$${SOURCEDIR}" "$${SPECDIR}" "$${SRPMDIR}" "$${BUILDDIR}"
|
||||
# Create source tarball
|
||||
@COMMITISH="HEAD"
|
||||
@VERS=$$(git describe --tags $${COMMITISH})
|
||||
@VERS=$${VERS#v}
|
||||
@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"
|
||||
@SRCFILE="$${SOURCEDIR}/$${PREFIX}.$${FORMAT}"
|
||||
@git archive --verbose --format "$${FORMAT}" --prefix="$${PREFIX}/" --output="$${SRCFILE}" $${COMMITISH}
|
||||
# Build RPM and SRPM
|
||||
@rpmbuild -ba --define="VERS $${VERS}" --rmsource --clean "$${SPECFILE}"
|
||||
# Report RPMs and SRPMs when in GitHub Workflow
|
||||
@if [[ "$${GITHUB_ACTIONS}" == true ]]; then
|
||||
@ RPMFILE="$${RPMDIR}/$${ARCH}/$${NVRA}.rpm"
|
||||
@ SRPMFILE="$${SRPMDIR}/$${NVR}.src.rpm"
|
||||
@ echo "RPM: $${RPMFILE}"
|
||||
@ echo "SRPM: $${SRPMFILE}"
|
||||
@ echo "::set-output name=SRPM::$${SRPMFILE}"
|
||||
@ echo "::set-output name=RPM::$${RPMFILE}"
|
||||
@fi
|
||||
|
||||
.PHONY: DEB
|
||||
DEB: scripts/cc-metric-collector.deb.control $(APP)
|
||||
@BASEDIR=$${PWD}
|
||||
@WORKSPACE=$${PWD}/.dpkgbuild
|
||||
@DEBIANDIR=$${WORKSPACE}/debian
|
||||
@DEBIANBINDIR=$${WORKSPACE}/DEBIAN
|
||||
@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})
|
||||
@VERS=$${VERS#v}
|
||||
@VERS=$$(echo $$VERS | sed -e s+'-'+'_'+g)
|
||||
@ARCH=$$(uname -m)
|
||||
@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 > $${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"
|
||||
@rm -r "$${WORKSPACE}"
|
||||
|
34
README.md
34
README.md
@@ -56,7 +56,41 @@ 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:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph a ["Cluster A"]
|
||||
nodeA[NodeA with CC collector]
|
||||
nodeB[NodeB with CC collector]
|
||||
nodeC[NodeC with CC collector]
|
||||
end
|
||||
a --> db[(Database)]
|
||||
db <--> ccweb("Webfrontend")
|
||||
```
|
||||
|
||||
``` mermaid
|
||||
flowchart TD
|
||||
subgraph a [ClusterA]
|
||||
direction LR
|
||||
nodeA[NodeA with CC collector]
|
||||
nodeB[NodeB with CC collector]
|
||||
nodeC[NodeC with CC collector]
|
||||
end
|
||||
subgraph b [ClusterB]
|
||||
direction LR
|
||||
nodeD[NodeD with CC collector]
|
||||
nodeE[NodeE with CC collector]
|
||||
nodeF[NodeF with CC collector]
|
||||
end
|
||||
a --> ccrecv{"CC collector as receiver"}
|
||||
b --> ccrecv
|
||||
ccrecv --> db[("Database1")]
|
||||
ccrecv -.-> db2[("Database2")]
|
||||
db <-.-> ccweb("Webfrontend")
|
||||
```
|
||||
|
||||
# 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.
|
||||
|
@@ -22,8 +22,8 @@ import (
|
||||
)
|
||||
|
||||
type CentralConfigFile struct {
|
||||
Interval int `json:"interval"`
|
||||
Duration int `json:"duration"`
|
||||
Interval string `json:"interval"`
|
||||
Duration string `json:"duration"`
|
||||
CollectorConfigFile string `json:"collectors"`
|
||||
RouterConfigFile string `json:"router"`
|
||||
SinkConfigFile string `json:"sinks"`
|
||||
@@ -173,16 +173,36 @@ func mainFunc() int {
|
||||
cclog.Error("Error reading configuration file ", rcfg.CliArgs["configfile"], ": ", err.Error())
|
||||
return 1
|
||||
}
|
||||
if rcfg.ConfigFile.Interval <= 0 || time.Duration(rcfg.ConfigFile.Interval)*time.Second <= 0 {
|
||||
cclog.Error("Configuration value 'interval' must be greater than zero")
|
||||
|
||||
// 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")
|
||||
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")
|
||||
@@ -271,7 +291,7 @@ func mainFunc() int {
|
||||
|
||||
// Wait until one tick has passed. This is a workaround
|
||||
if rcfg.CliArgs["once"] == "true" {
|
||||
x := 1.2 * float64(rcfg.ConfigFile.Interval)
|
||||
x := 1.2 * float64(rcfg.Interval.Seconds())
|
||||
time.Sleep(time.Duration(int(x)) * time.Second)
|
||||
shutdownSignal <- os.Interrupt
|
||||
}
|
@@ -12,6 +12,12 @@
|
||||
"proc_total"
|
||||
]
|
||||
},
|
||||
"netstat": {
|
||||
"include_devices": [
|
||||
"enp5s0"
|
||||
],
|
||||
"send_derived_values": true
|
||||
},
|
||||
"numastats": {},
|
||||
"nvidia": {},
|
||||
"tempstat": {
|
||||
|
@@ -1,82 +1,31 @@
|
||||
# Use central installation
|
||||
CENTRAL_INSTALL = false
|
||||
# How to access hardware performance counters through LIKWID.
|
||||
# Recommended is 'direct' mode
|
||||
ACCESSMODE = direct
|
||||
|
||||
#######################################################################
|
||||
# if CENTRAL_INSTALL == true
|
||||
#######################################################################
|
||||
# Path to central installation (if CENTRAL_INSTALL=true)
|
||||
LIKWID_BASE=/apps/likwid/5.2.1
|
||||
# LIKWID version (should be same major version as central installation, 5.2.x)
|
||||
# LIKWID version
|
||||
LIKWID_VERSION = 5.2.1
|
||||
LIKWID_INSTALLED_FOLDER=$(shell dirname $(shell which likwid-topology 2>/dev/null) 2>/dev/null)
|
||||
|
||||
#######################################################################
|
||||
# if CENTRAL_INSTALL == false and ACCESSMODE == accessdaemon
|
||||
#######################################################################
|
||||
# Where to install the accessdaemon
|
||||
DAEMON_INSTALLDIR = /usr/local
|
||||
# Which user to use for the accessdaemon
|
||||
DAEMON_USER = root
|
||||
# Which group to use for the accessdaemon
|
||||
DAEMON_GROUP = root
|
||||
LIKWID_FOLDER="$(shell pwd)/likwid"
|
||||
|
||||
all: $(LIKWID_FOLDER)/likwid.h
|
||||
|
||||
.ONESHELL:
|
||||
.PHONY: $(LIKWID_FOLDER)/likwid.h
|
||||
$(LIKWID_FOLDER)/likwid.h:
|
||||
if [ "$(LIKWID_INSTALLED_FOLDER)" != "" ]; then \
|
||||
BASE="$(LIKWID_INSTALLED_FOLDER)/../include"; \
|
||||
mkdir -p $(LIKWID_FOLDER); \
|
||||
cp $$BASE/*.h $(LIKWID_FOLDER); \
|
||||
else \
|
||||
BUILD_FOLDER="$${PWD}/likwidbuild"; \
|
||||
if [ -d $(LIKWID_FOLDER) ]; then rm -r $(LIKWID_FOLDER); fi; \
|
||||
mkdir --parents --verbose $(LIKWID_FOLDER) $${BUILD_FOLDER}; \
|
||||
wget -P "$${BUILD_FOLDER}" http://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 $(LIKWID_FOLDER)/; \
|
||||
install -Dpm 0644 $${BUILD_FOLDER}/likwid-$(LIKWID_VERSION)/src/includes/bstrlib.h $(LIKWID_FOLDER)/; \
|
||||
rm -r $${BUILD_FOLDER}; \
|
||||
fi
|
||||
|
||||
|
||||
|
||||
#################################################
|
||||
# No need to change anything below this line
|
||||
#################################################
|
||||
INSTALL_FOLDER = ./likwid
|
||||
BUILD_FOLDER = ./likwid/build
|
||||
|
||||
ifneq ($(strip $(CENTRAL_INSTALL)),true)
|
||||
LIKWID_BASE := $(shell pwd)/$(INSTALL_FOLDER)
|
||||
DAEMON_BASE := $(LIKWID_BASE)
|
||||
GROUPS_BASE := $(LIKWID_BASE)/groups
|
||||
all: $(INSTALL_FOLDER)/liblikwid.a cleanup
|
||||
else
|
||||
DAEMON_BASE= $(LIKWID_BASE)/sbin
|
||||
all: $(INSTALL_FOLDER)/liblikwid.a cleanup
|
||||
endif
|
||||
|
||||
|
||||
|
||||
$(BUILD_FOLDER)/likwid-$(LIKWID_VERSION).tar.gz: $(BUILD_FOLDER)
|
||||
wget -P $(BUILD_FOLDER) ftp://ftp.rrze.uni-erlangen.de/mirrors/likwid/likwid-$(LIKWID_VERSION).tar.gz
|
||||
|
||||
$(BUILD_FOLDER):
|
||||
mkdir -p $(BUILD_FOLDER)
|
||||
|
||||
$(INSTALL_FOLDER):
|
||||
mkdir -p $(INSTALL_FOLDER)
|
||||
|
||||
$(BUILD_FOLDER)/likwid-$(LIKWID_VERSION): $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION).tar.gz
|
||||
tar -C $(BUILD_FOLDER) -xf $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION).tar.gz
|
||||
|
||||
$(INSTALL_FOLDER)/liblikwid.a: $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION) $(INSTALL_FOLDER)
|
||||
sed -i -e s+"PREFIX ?= .*"+"PREFIX = $(LIKWID_BASE)"+g \
|
||||
-e s+"SHARED_LIBRARY = .*"+"SHARED_LIBRARY = false"+g \
|
||||
-e s+"ACCESSMODE = .*"+"ACCESSMODE = $(ACCESSMODE)"+g \
|
||||
-e s+"INSTALLED_ACCESSDAEMON = .*"+"INSTALLED_ACCESSDAEMON = $(DAEMON_INSTALLDIR)/likwid-accessD"+g \
|
||||
$(BUILD_FOLDER)/likwid-$(LIKWID_VERSION)/config.mk
|
||||
cd $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION) && make
|
||||
cp $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION)/liblikwid.a $(INSTALL_FOLDER)
|
||||
cp $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION)/ext/hwloc/liblikwid-hwloc.a $(INSTALL_FOLDER)
|
||||
cp $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION)/src/includes/likwid*.h $(INSTALL_FOLDER)
|
||||
cp $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION)/src/includes/bstrlib.h $(INSTALL_FOLDER)
|
||||
|
||||
$(DAEMON_INSTALLDIR)/likwid-accessD: $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION)/likwid-accessD
|
||||
sudo -u $(DAEMON_USER) -g $(DAEMON_GROUP) install -m 4775 $(BUILD_FOLDER)/likwid-$(LIKWID_VERSION)/likwid-accessD $(DAEMON_INSTALLDIR)/likwid-accessD
|
||||
|
||||
prepare_collector: likwidMetric.go
|
||||
cp likwidMetric.go likwidMetric.go.orig
|
||||
sed -i -e s+"const GROUPPATH =.*"+"const GROUPPATH = \`$(GROUPS_BASE)\`"+g likwidMetric.go
|
||||
|
||||
cleanup:
|
||||
rm -rf $(BUILD_FOLDER)
|
||||
|
||||
clean: cleanup
|
||||
clean:
|
||||
rm -rf likwid
|
||||
|
||||
.PHONY: clean
|
||||
|
@@ -37,6 +37,9 @@ In contrast to the configuration files for sinks and receivers, the collectors c
|
||||
* [`cpufreq_cpuinfo`](./cpufreqCpuinfoMetric.md)
|
||||
* [`numastat`](./numastatMetric.md)
|
||||
* [`gpfs`](./gpfsMetric.md)
|
||||
* [`beegfs_meta`](./beegfsmetaMetric.md)
|
||||
* [`beegfs_storage`](./beegfsstorageMetric.md)
|
||||
* [`rocm_smi`](./rocmsmiMetric.md)
|
||||
|
||||
## Todos
|
||||
|
||||
|
230
collectors/beegfsmetaMetric.go
Normal file
230
collectors/beegfsmetaMetric.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
const DEFAULT_BEEGFS_CMD = "beegfs-ctl"
|
||||
|
||||
// Struct for the collector-specific JSON config
|
||||
type BeegfsMetaCollectorConfig struct {
|
||||
Beegfs string `json:"beegfs_path"`
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
ExcludeFilesystem []string `json:"exclude_filesystem"`
|
||||
}
|
||||
|
||||
type BeegfsMetaCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config BeegfsMetaCollectorConfig
|
||||
skipFS map[string]struct{}
|
||||
}
|
||||
|
||||
func (m *BeegfsMetaCollector) Init(config json.RawMessage) error {
|
||||
// Check if already initialized
|
||||
if m.init {
|
||||
return nil
|
||||
}
|
||||
// Metrics
|
||||
var nodeMdstat_array = [39]string{
|
||||
"sum", "ack", "close", "entInf",
|
||||
"fndOwn", "mkdir", "create", "rddir",
|
||||
"refrEn", "mdsInf", "rmdir", "rmLnk",
|
||||
"mvDirIns", "mvFiIns", "open", "ren",
|
||||
"sChDrct", "sAttr", "sDirPat", "stat",
|
||||
"statfs", "trunc", "symlnk", "unlnk",
|
||||
"lookLI", "statLI", "revalLI", "openLI",
|
||||
"createLI", "hardlnk", "flckAp", "flckEn",
|
||||
"flckRg", "dirparent", "listXA", "getXA",
|
||||
"rmXA", "setXA", "mirror"}
|
||||
|
||||
m.name = "BeegfsMetaCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
// Set default beegfs-ctl binary
|
||||
|
||||
m.config.Beegfs = DEFAULT_BEEGFS_CMD
|
||||
|
||||
// Read JSON configuration
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//create map with possible variables
|
||||
m.matches = make(map[string]string)
|
||||
for _, value := range nodeMdstat_array {
|
||||
_, skip := stringArrayContains(m.config.ExcludeMetrics, value)
|
||||
if skip {
|
||||
m.matches["other"] = "0"
|
||||
} else {
|
||||
m.matches["beegfs_cmeta_"+value] = "0"
|
||||
}
|
||||
}
|
||||
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
"group": "BeegfsMeta",
|
||||
}
|
||||
m.tags = map[string]string{
|
||||
"type": "node",
|
||||
"filesystem": "",
|
||||
}
|
||||
m.skipFS = make(map[string]struct{})
|
||||
for _, fs := range m.config.ExcludeFilesystem {
|
||||
m.skipFS[fs] = struct{}{}
|
||||
}
|
||||
|
||||
// Beegfs file system statistics can only be queried by user root
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("BeegfsMetaCollector.Init(): Failed to get current user: %v", err)
|
||||
}
|
||||
if user.Uid != "0" {
|
||||
return fmt.Errorf("BeegfsMetaCollector.Init(): BeeGFS file system statistics can only be queried by user root")
|
||||
}
|
||||
|
||||
// Check if beegfs-ctl is in executable search path
|
||||
_, err = exec.LookPath(m.config.Beegfs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("BeegfsMetaCollector.Init(): Failed to find beegfs-ctl binary '%s': %v", m.config.Beegfs, err)
|
||||
}
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BeegfsMetaCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
//get mounpoint
|
||||
buffer, _ := ioutil.ReadFile(string("/proc/mounts"))
|
||||
mounts := strings.Split(string(buffer), "\n")
|
||||
var mountpoints []string
|
||||
for _, line := range mounts {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
f := strings.Fields(line)
|
||||
if strings.Contains(f[0], "beegfs_ondemand") {
|
||||
// Skip excluded filesystems
|
||||
if _, skip := m.skipFS[f[1]]; skip {
|
||||
continue
|
||||
}
|
||||
mountpoints = append(mountpoints, f[1])
|
||||
}
|
||||
}
|
||||
|
||||
if len(mountpoints) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, mountpoint := range mountpoints {
|
||||
m.tags["filesystem"] = mountpoint
|
||||
|
||||
// bwwgfs-ctl:
|
||||
// --clientstats: Show client IO statistics.
|
||||
// --nodetype=meta: The node type to query (meta, storage).
|
||||
// --interval:
|
||||
// --mount=/mnt/beeond/: Which mount point
|
||||
//cmd := exec.Command(m.config.Beegfs, "/root/mc/test.txt")
|
||||
mountoption := "--mount=" + mountpoint
|
||||
cmd := exec.Command(m.config.Beegfs, "--clientstats",
|
||||
"--nodetype=meta", mountoption, "--allstats")
|
||||
cmd.Stdin = strings.NewReader("\n")
|
||||
cmdStdout := new(bytes.Buffer)
|
||||
cmdStderr := new(bytes.Buffer)
|
||||
cmd.Stdout = cmdStdout
|
||||
cmd.Stderr = cmdStderr
|
||||
err := cmd.Run()
|
||||
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, _ := ioutil.ReadAll(cmdStderr)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsMetaCollector.Read(): command stderr: \"%s\"\n", string(data))
|
||||
data, _ = ioutil.ReadAll(cmdStdout)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsMetaCollector.Read(): command stdout: \"%s\"\n", string(data))
|
||||
return
|
||||
}
|
||||
// Read I/O statistics
|
||||
scanner := bufio.NewScanner(cmdStdout)
|
||||
|
||||
sumLine := regexp.MustCompile(`^Sum:\s+\d+\s+\[[a-zA-Z]+\]+`)
|
||||
//Line := regexp.MustCompile(`^(.*)\s+(\d)+\s+\[([a-zA-Z]+)\]+`)
|
||||
statsLine := regexp.MustCompile(`^(.*?)\s+?(\d.*?)$`)
|
||||
singleSpacePattern := regexp.MustCompile(`\s+`)
|
||||
removePattern := regexp.MustCompile(`[\[|\]]`)
|
||||
|
||||
for scanner.Scan() {
|
||||
readLine := scanner.Text()
|
||||
//fmt.Println(readLine)
|
||||
// Jump few lines, we only want the I/O stats from nodes
|
||||
if !sumLine.MatchString(readLine) {
|
||||
continue
|
||||
}
|
||||
|
||||
match := statsLine.FindStringSubmatch(readLine)
|
||||
// nodeName = "Sum:" or would be nodes
|
||||
// nodeName := match[1]
|
||||
//Remove multiple whitespaces
|
||||
dummy := removePattern.ReplaceAllString(match[2], " ")
|
||||
metaStats := strings.TrimSpace(singleSpacePattern.ReplaceAllString(dummy, " "))
|
||||
split := strings.Split(metaStats, " ")
|
||||
|
||||
// fill map with values
|
||||
// split[i+1] = mdname
|
||||
// split[i] = amount of md operations
|
||||
for i := 0; i <= len(split)-1; i += 2 {
|
||||
if _, ok := m.matches[split[i+1]]; ok {
|
||||
m.matches["beegfs_cmeta_"+split[i+1]] = split[i]
|
||||
} else {
|
||||
f1, err := strconv.ParseFloat(m.matches["other"], 32)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("Metric (other): Failed to convert str written '%s' to float: %v", m.matches["other"], err))
|
||||
continue
|
||||
}
|
||||
f2, err := strconv.ParseFloat(split[i], 32)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("Metric (other): Failed to convert str written '%s' to float: %v", m.matches["other"], err))
|
||||
continue
|
||||
}
|
||||
//mdStat["other"] = fmt.Sprintf("%f", f1+f2)
|
||||
m.matches["beegfs_cstorage_other"] = fmt.Sprintf("%f", f1+f2)
|
||||
}
|
||||
}
|
||||
|
||||
for key, data := range m.matches {
|
||||
value, _ := strconv.ParseFloat(data, 32)
|
||||
y, err := lp.New(key, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BeegfsMetaCollector) Close() {
|
||||
m.init = false
|
||||
}
|
75
collectors/beegfsmetaMetric.md
Normal file
75
collectors/beegfsmetaMetric.md
Normal file
@@ -0,0 +1,75 @@
|
||||
## `BeeGFS on Demand` collector
|
||||
This Collector is to collect BeeGFS on Demand (BeeOND) metadata clientstats.
|
||||
|
||||
```json
|
||||
"beegfs_meta": {
|
||||
"beegfs_path": "/usr/bin/beegfs-ctl",
|
||||
"exclude_filesystem": [
|
||||
"/mnt/ignore_me"
|
||||
],
|
||||
"exclude_metrics": [
|
||||
"ack",
|
||||
"entInf",
|
||||
"fndOwn"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `BeeGFS On Demand (BeeOND)` collector uses the `beegfs-ctl` command to read performance metrics for
|
||||
BeeGFS filesystems.
|
||||
|
||||
The reported filesystems can be filtered with the `exclude_filesystem` option
|
||||
in the configuration.
|
||||
|
||||
The path to the `beegfs-ctl` command can be configured with the `beegfs_path` option
|
||||
in the configuration.
|
||||
|
||||
When using the `exclude_metrics` option, the excluded metrics are summed as `other`.
|
||||
|
||||
Important: The metrics listed below, are similar to the naming of BeeGFS. The Collector prefixes these with `beegfs_cstorage`(beegfs client storage).
|
||||
|
||||
For example beegfs metric `open`-> `beegfs_cstorage_open`
|
||||
|
||||
Available Metrics:
|
||||
|
||||
* sum
|
||||
* ack
|
||||
* close
|
||||
* entInf
|
||||
* fndOwn
|
||||
* mkdir
|
||||
* create
|
||||
* rddir
|
||||
* refrEnt
|
||||
* mdsInf
|
||||
* rmdir
|
||||
* rmLnk
|
||||
* mvDirIns
|
||||
* mvFiIns
|
||||
* open
|
||||
* ren
|
||||
* sChDrct
|
||||
* sAttr
|
||||
* sDirPat
|
||||
* stat
|
||||
* statfs
|
||||
* trunc
|
||||
* symlnk
|
||||
* unlnk
|
||||
* lookLI
|
||||
* statLI
|
||||
* revalLI
|
||||
* openLI
|
||||
* createLI
|
||||
* hardlnk
|
||||
* flckAp
|
||||
* flckEn
|
||||
* flckRg
|
||||
* dirparent
|
||||
* listXA
|
||||
* getXA
|
||||
* rmXA
|
||||
* setXA
|
||||
* mirror
|
||||
|
||||
The collector adds a `filesystem` tag to all metrics
|
222
collectors/beegfsstorageMetric.go
Normal file
222
collectors/beegfsstorageMetric.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
// Struct for the collector-specific JSON config
|
||||
type BeegfsStorageCollectorConfig struct {
|
||||
Beegfs string `json:"beegfs_path"`
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
ExcludeFilesystem []string `json:"exclude_filesystem"`
|
||||
}
|
||||
|
||||
type BeegfsStorageCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config BeegfsStorageCollectorConfig
|
||||
skipFS map[string]struct{}
|
||||
}
|
||||
|
||||
func (m *BeegfsStorageCollector) Init(config json.RawMessage) error {
|
||||
// Check if already initialized
|
||||
if m.init {
|
||||
return nil
|
||||
}
|
||||
// Metrics
|
||||
var storageStat_array = [18]string{
|
||||
"sum", "ack", "sChDrct", "getFSize",
|
||||
"sAttr", "statfs", "trunc", "close",
|
||||
"fsync", "ops-rd", "MiB-rd/s", "ops-wr",
|
||||
"MiB-wr/s", "gendbg", "hrtbeat", "remNode",
|
||||
"storInf", "unlnk"}
|
||||
|
||||
m.name = "BeegfsStorageCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
// Set default beegfs-ctl binary
|
||||
|
||||
m.config.Beegfs = DEFAULT_BEEGFS_CMD
|
||||
|
||||
// Read JSON configuration
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
println(m.config.Beegfs)
|
||||
//create map with possible variables
|
||||
m.matches = make(map[string]string)
|
||||
for _, value := range storageStat_array {
|
||||
_, skip := stringArrayContains(m.config.ExcludeMetrics, value)
|
||||
if skip {
|
||||
m.matches["other"] = "0"
|
||||
} else {
|
||||
m.matches["beegfs_cstorage_"+value] = "0"
|
||||
}
|
||||
}
|
||||
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
"group": "BeegfsStorage",
|
||||
}
|
||||
m.tags = map[string]string{
|
||||
"type": "node",
|
||||
"filesystem": "",
|
||||
}
|
||||
m.skipFS = make(map[string]struct{})
|
||||
for _, fs := range m.config.ExcludeFilesystem {
|
||||
m.skipFS[fs] = struct{}{}
|
||||
}
|
||||
|
||||
// Beegfs file system statistics can only be queried by user root
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("BeegfsStorageCollector.Init(): Failed to get current user: %v", err)
|
||||
}
|
||||
if user.Uid != "0" {
|
||||
return fmt.Errorf("BeegfsStorageCollector.Init(): BeeGFS file system statistics can only be queried by user root")
|
||||
}
|
||||
|
||||
// Check if beegfs-ctl is in executable search path
|
||||
_, err = exec.LookPath(m.config.Beegfs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("BeegfsStorageCollector.Init(): Failed to find beegfs-ctl binary '%s': %v", m.config.Beegfs, err)
|
||||
}
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BeegfsStorageCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
//get mounpoint
|
||||
buffer, _ := ioutil.ReadFile(string("/proc/mounts"))
|
||||
mounts := strings.Split(string(buffer), "\n")
|
||||
var mountpoints []string
|
||||
for _, line := range mounts {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
f := strings.Fields(line)
|
||||
if strings.Contains(f[0], "beegfs_ondemand") {
|
||||
// Skip excluded filesystems
|
||||
if _, skip := m.skipFS[f[1]]; skip {
|
||||
continue
|
||||
}
|
||||
mountpoints = append(mountpoints, f[1])
|
||||
}
|
||||
}
|
||||
if len(mountpoints) == 0 {
|
||||
return
|
||||
}
|
||||
// collects stats for each BeeGFS on Demand FS
|
||||
for _, mountpoint := range mountpoints {
|
||||
m.tags["filesystem"] = mountpoint
|
||||
|
||||
// bwwgfs-ctl:
|
||||
// --clientstats: Show client IO statistics.
|
||||
// --nodetype=meta: The node type to query (meta, storage).
|
||||
// --interval:
|
||||
// --mount=/mnt/beeond/: Which mount point
|
||||
//cmd := exec.Command(m.config.Beegfs, "/root/mc/test.txt")
|
||||
mountoption := "--mount=" + mountpoint
|
||||
cmd := exec.Command(m.config.Beegfs, "--clientstats",
|
||||
"--nodetype=storage", mountoption, "--allstats")
|
||||
cmd.Stdin = strings.NewReader("\n")
|
||||
cmdStdout := new(bytes.Buffer)
|
||||
cmdStderr := new(bytes.Buffer)
|
||||
cmd.Stdout = cmdStdout
|
||||
cmd.Stderr = cmdStderr
|
||||
err := cmd.Run()
|
||||
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, _ := ioutil.ReadAll(cmdStderr)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsStorageCollector.Read(): command stderr: \"%s\"\n", string(data))
|
||||
data, _ = ioutil.ReadAll(cmdStdout)
|
||||
fmt.Fprintf(os.Stderr, "BeegfsStorageCollector.Read(): command stdout: \"%s\"\n", string(data))
|
||||
return
|
||||
}
|
||||
// Read I/O statistics
|
||||
scanner := bufio.NewScanner(cmdStdout)
|
||||
|
||||
sumLine := regexp.MustCompile(`^Sum:\s+\d+\s+\[[a-zA-Z]+\]+`)
|
||||
//Line := regexp.MustCompile(`^(.*)\s+(\d)+\s+\[([a-zA-Z]+)\]+`)
|
||||
statsLine := regexp.MustCompile(`^(.*?)\s+?(\d.*?)$`)
|
||||
singleSpacePattern := regexp.MustCompile(`\s+`)
|
||||
removePattern := regexp.MustCompile(`[\[|\]]`)
|
||||
|
||||
for scanner.Scan() {
|
||||
readLine := scanner.Text()
|
||||
//fmt.Println(readLine)
|
||||
// Jump few lines, we only want the I/O stats from nodes
|
||||
if !sumLine.MatchString(readLine) {
|
||||
continue
|
||||
}
|
||||
|
||||
match := statsLine.FindStringSubmatch(readLine)
|
||||
// nodeName = "Sum:" or would be nodes
|
||||
// nodeName := match[1]
|
||||
//Remove multiple whitespaces
|
||||
dummy := removePattern.ReplaceAllString(match[2], " ")
|
||||
metaStats := strings.TrimSpace(singleSpacePattern.ReplaceAllString(dummy, " "))
|
||||
split := strings.Split(metaStats, " ")
|
||||
|
||||
// fill map with values
|
||||
// split[i+1] = mdname
|
||||
// split[i] = amount of operations
|
||||
for i := 0; i <= len(split)-1; i += 2 {
|
||||
if _, ok := m.matches[split[i+1]]; ok {
|
||||
m.matches["beegfs_cstorage_"+split[i+1]] = split[i]
|
||||
//m.matches[split[i+1]] = split[i]
|
||||
} else {
|
||||
f1, err := strconv.ParseFloat(m.matches["other"], 32)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("Metric (other): Failed to convert str written '%s' to float: %v", m.matches["other"], err))
|
||||
continue
|
||||
}
|
||||
f2, err := strconv.ParseFloat(split[i], 32)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("Metric (other): Failed to convert str written '%s' to float: %v", m.matches["other"], err))
|
||||
continue
|
||||
}
|
||||
m.matches["beegfs_cstorage_other"] = fmt.Sprintf("%f", f1+f2)
|
||||
}
|
||||
}
|
||||
|
||||
for key, data := range m.matches {
|
||||
value, _ := strconv.ParseFloat(data, 32)
|
||||
y, err := lp.New(key, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BeegfsStorageCollector) Close() {
|
||||
m.init = false
|
||||
}
|
55
collectors/beegfsstorageMetric.md
Normal file
55
collectors/beegfsstorageMetric.md
Normal file
@@ -0,0 +1,55 @@
|
||||
## `BeeGFS on Demand` collector
|
||||
This Collector is to collect BeeGFS on Demand (BeeOND) storage stats.
|
||||
|
||||
```json
|
||||
"beegfs_storage": {
|
||||
"beegfs_path": "/usr/bin/beegfs-ctl",
|
||||
"exclude_filesystem": [
|
||||
"/mnt/ignore_me"
|
||||
],
|
||||
"exclude_metrics": [
|
||||
"ack",
|
||||
"storInf",
|
||||
"unlnk"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `BeeGFS On Demand (BeeOND)` collector uses the `beegfs-ctl` command to read performance metrics for BeeGFS filesystems.
|
||||
|
||||
The reported filesystems can be filtered with the `exclude_filesystem` option
|
||||
in the configuration.
|
||||
|
||||
The path to the `beegfs-ctl` command can be configured with the `beegfs_path` option
|
||||
in the configuration.
|
||||
|
||||
When using the `exclude_metrics` option, the excluded metrics are summed as `other`.
|
||||
|
||||
Important: The metrics listed below, are similar to the naming of BeeGFS. The Collector prefixes these with `beegfs_cstorage_`(beegfs client meta).
|
||||
For example beegfs metric `open`-> `beegfs_cstorage_`
|
||||
|
||||
Note: BeeGFS FS offers many Metadata Information. Probably it makes sense to exlcude most of them. Nevertheless, these excluded metrics will be summed as `beegfs_cstorage_other`.
|
||||
|
||||
Available Metrics:
|
||||
|
||||
* "sum"
|
||||
* "ack"
|
||||
* "sChDrct"
|
||||
* "getFSize"
|
||||
* "sAttr"
|
||||
* "statfs"
|
||||
* "trunc"
|
||||
* "close"
|
||||
* "fsync"
|
||||
* "ops-rd"
|
||||
* "MiB-rd/s"
|
||||
* "ops-wr"
|
||||
* "MiB-wr/s"
|
||||
* "endbg"
|
||||
* "hrtbeat"
|
||||
* "remNode"
|
||||
* "storInf"
|
||||
* "unlnk"
|
||||
|
||||
|
||||
The collector adds a `filesystem` tag to all metrics
|
@@ -14,38 +14,43 @@ import (
|
||||
// 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),
|
||||
"ibstat_perfquery": new(InfinibandPerfQueryCollector),
|
||||
"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),
|
||||
"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),
|
||||
"rocm_smi": new(RocmSmiCollector),
|
||||
}
|
||||
|
||||
// Metric collector manager data structure
|
||||
type collectorManager struct {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// Metric collector manager access functions
|
||||
@@ -65,6 +70,7 @@ 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
|
||||
@@ -99,7 +105,11 @@ func (cm *collectorManager) Init(ticker mct.MultiChanTicker, duration time.Durat
|
||||
continue
|
||||
}
|
||||
cclog.ComponentDebug("CollectorManager", "ADD COLLECTOR", collector.Name())
|
||||
cm.collectors = append(cm.collectors, collector)
|
||||
if collector.Parallel() {
|
||||
cm.collectors = append(cm.collectors, collector)
|
||||
} else {
|
||||
cm.serial = append(cm.serial, collector)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -115,6 +125,10 @@ 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()
|
||||
}
|
||||
@@ -129,7 +143,26 @@ 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:
|
||||
|
@@ -48,6 +48,7 @@ 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",
|
||||
@@ -57,7 +58,7 @@ func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
const cpuInfoFile = "/proc/cpuinfo"
|
||||
file, err := os.Open(cpuInfoFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open file '%s': %v", cpuInfoFile, err)
|
||||
return fmt.Errorf("failed to open file '%s': %v", cpuInfoFile, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@@ -106,14 +107,14 @@ func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
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)
|
||||
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)
|
||||
return fmt.Errorf("unable to convert physicalPackageID '%s' to int64: %v", physicalPackageID, err)
|
||||
}
|
||||
|
||||
// increase maximun socket / package ID, when required
|
||||
@@ -150,7 +151,7 @@ func (m *CPUFreqCpuInfoCollector) Init(config json.RawMessage) error {
|
||||
t.numNonHT = numNonHT
|
||||
t.numNonHT_int = numNonHT_int
|
||||
t.tagSet = map[string]string{
|
||||
"type": "cpu",
|
||||
"type": "hwthread",
|
||||
"type-id": t.processor,
|
||||
"package_id": t.physicalPackageID,
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"cpufreq_cpuinfo": {}
|
||||
```
|
||||
|
||||
The `cpufreq_cpuinfo` collector reads the clock frequency from `/proc/cpuinfo` and outputs a handful **cpu** metrics.
|
||||
The `cpufreq_cpuinfo` collector reads the clock frequency from `/proc/cpuinfo` and outputs a handful **hwthread** metrics.
|
||||
|
||||
Metrics:
|
||||
* `cpufreq`
|
||||
|
@@ -53,6 +53,7 @@ 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 {
|
||||
@@ -70,10 +71,10 @@ func (m *CPUFreqCollector) Init(config json.RawMessage) error {
|
||||
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)
|
||||
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)
|
||||
return fmt.Errorf("unable to find any files with pattern '%s'", globPattern)
|
||||
}
|
||||
|
||||
// Initialize CPU topology
|
||||
@@ -82,38 +83,38 @@ func (m *CPUFreqCollector) Init(config json.RawMessage) error {
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
return fmt.Errorf("unable to convert coreID '%s' to int64: %v", coreID, err)
|
||||
}
|
||||
|
||||
// Check access to current frequency file
|
||||
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)
|
||||
return fmt.Errorf("unable to access file '%s': %v", scalingCurFreqFile, err)
|
||||
}
|
||||
|
||||
t := &m.topology[processor_int]
|
||||
@@ -161,7 +162,7 @@ func (m *CPUFreqCollector) Init(config json.RawMessage) error {
|
||||
t.numNonHT = numNonHT
|
||||
t.numNonHT_int = numNonHT_int
|
||||
t.tagSet = map[string]string{
|
||||
"type": "cpu",
|
||||
"type": "hwthread",
|
||||
"type-id": t.processor,
|
||||
"package_id": t.physicalPackageID,
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
The `cpufreq` collector reads the clock frequency from `/sys/devices/system/cpu/cpu*/cpufreq` and outputs a handful **cpu** metrics.
|
||||
The `cpufreq` collector reads the clock frequency from `/sys/devices/system/cpu/cpu*/cpufreq` and outputs a handful **hwthread** metrics.
|
||||
|
||||
Metrics:
|
||||
* `cpufreq`
|
@@ -21,16 +21,16 @@ type CpustatCollectorConfig struct {
|
||||
|
||||
type CpustatCollector struct {
|
||||
metricCollector
|
||||
config CpustatCollectorConfig
|
||||
matches map[string]int
|
||||
cputags map[string]map[string]string
|
||||
nodetags map[string]string
|
||||
num_cpus_metric lp.CCMetric
|
||||
config CpustatCollectorConfig
|
||||
matches map[string]int
|
||||
cputags map[string]map[string]string
|
||||
nodetags map[string]string
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -83,7 +83,7 @@ func (m *CpustatCollector) Init(config json.RawMessage) error {
|
||||
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": "cpu", "type-id": fmt.Sprintf("%d", cpu)}
|
||||
m.cputags[linefields[0]] = map[string]string{"type": "hwthread", "type-id": fmt.Sprintf("%d", cpu)}
|
||||
num_cpus++
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ type CustomCmdCollector struct {
|
||||
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)
|
||||
@@ -61,7 +62,7 @@ func (m *CustomCmdCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
}
|
||||
if len(m.files) == 0 && len(m.commands) == 0 {
|
||||
return errors.New("No metrics to collect")
|
||||
return errors.New("no metrics to collect")
|
||||
}
|
||||
m.handler = influx.NewMetricHandler()
|
||||
m.parser = influx.NewParser(m.handler)
|
||||
|
@@ -3,7 +3,6 @@ package collectors
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -30,6 +29,7 @@ type DiskstatCollector struct {
|
||||
|
||||
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 {
|
||||
@@ -78,11 +78,17 @@ func (m *DiskstatCollector) Read(interval time.Duration, output chan lp.CCMetric
|
||||
continue
|
||||
}
|
||||
path := strings.Replace(linefields[1], `\040`, " ", -1)
|
||||
stat := syscall.Statfs_t{}
|
||||
stat := syscall.Statfs_t{
|
||||
Blocks: 0,
|
||||
Bsize: 0,
|
||||
Bfree: 0,
|
||||
}
|
||||
err := syscall.Statfs(path, &stat)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
continue
|
||||
}
|
||||
if stat.Blocks == 0 || stat.Bsize == 0 {
|
||||
continue
|
||||
}
|
||||
tags := map[string]string{"type": "node", "device": linefields[0]}
|
||||
total := (stat.Blocks * uint64(stat.Bsize)) / uint64(1000000000)
|
||||
@@ -97,12 +103,14 @@ func (m *DiskstatCollector) Read(interval time.Duration, output chan lp.CCMetric
|
||||
y.AddMeta("unit", "GBytes")
|
||||
output <- y
|
||||
}
|
||||
perc := (100 * (total - free)) / total
|
||||
if perc > part_max_used {
|
||||
part_max_used = perc
|
||||
if total > 0 {
|
||||
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": part_max_used}, time.Now())
|
||||
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")
|
||||
output <- y
|
||||
|
@@ -17,14 +17,24 @@ import (
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
const DEFAULT_GPFS_CMD = "mmpmon"
|
||||
|
||||
type GpfsCollectorLastState struct {
|
||||
bytesRead int64
|
||||
bytesWritten int64
|
||||
}
|
||||
|
||||
type GpfsCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
config struct {
|
||||
Mmpmon string `json:"mmpmon_path,omitempty"`
|
||||
ExcludeFilesystem []string `json:"exclude_filesystem,omitempty"`
|
||||
SendBandwidths bool `json:"send_bandwidths"`
|
||||
}
|
||||
skipFS map[string]struct{}
|
||||
skipFS map[string]struct{}
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
lastState map[string]GpfsCollectorLastState
|
||||
}
|
||||
|
||||
func (m *GpfsCollector) Init(config json.RawMessage) error {
|
||||
@@ -36,9 +46,10 @@ 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 = "/usr/lpp/mmfs/bin/mmpmon"
|
||||
m.config.Mmpmon = DEFAULT_GPFS_CMD
|
||||
|
||||
// Read JSON configuration
|
||||
if len(config) > 0 {
|
||||
@@ -60,21 +71,23 @@ 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()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get current user: %v", err)
|
||||
return fmt.Errorf("failed to get current user: %v", err)
|
||||
}
|
||||
if user.Uid != "0" {
|
||||
return fmt.Errorf("GPFS file system statistics can only be queried by user root")
|
||||
}
|
||||
|
||||
// Check if mmpmon is in executable search path
|
||||
_, err = exec.LookPath(m.config.Mmpmon)
|
||||
p, err := exec.LookPath(m.config.Mmpmon)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to find mmpmon binary '%s': %v", m.config.Mmpmon, err)
|
||||
return fmt.Errorf("failed to find mmpmon binary '%s': %v", m.config.Mmpmon, err)
|
||||
}
|
||||
m.config.Mmpmon = p
|
||||
|
||||
m.init = true
|
||||
return nil
|
||||
@@ -86,6 +99,13 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
return
|
||||
}
|
||||
|
||||
// Current time stamp
|
||||
now := time.Now()
|
||||
// time difference to last time stamp
|
||||
timeDiff := now.Sub(m.lastTimestamp).Seconds()
|
||||
// Save current timestamp
|
||||
m.lastTimestamp = now
|
||||
|
||||
// mmpmon:
|
||||
// -p: generate output that can be parsed
|
||||
// -s: suppress the prompt on input
|
||||
@@ -144,8 +164,19 @@ 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return code
|
||||
rc, err := strconv.Atoi(key_value["_rc_"])
|
||||
if err != nil {
|
||||
@@ -188,6 +219,14 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
if y, err := lp.New("gpfs_bytes_read", m.tags, m.meta, map[string]interface{}{"value": bytesRead}, timestamp); err == nil {
|
||||
output <- y
|
||||
}
|
||||
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 {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bytes written
|
||||
bytesWritten, err := strconv.ParseInt(key_value["_bw_"], 10, 64)
|
||||
@@ -200,6 +239,21 @@ func (m *GpfsCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
if y, err := lp.New("gpfs_bytes_written", m.tags, m.meta, map[string]interface{}{"value": bytesWritten}, timestamp); err == nil {
|
||||
output <- y
|
||||
}
|
||||
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 {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.config.SendBandwidths {
|
||||
m.lastState[filesystem] = GpfsCollectorLastState{
|
||||
bytesRead: bytesRead,
|
||||
bytesWritten: bytesWritten,
|
||||
}
|
||||
}
|
||||
|
||||
// number of opens
|
||||
numOpens, err := strconv.ParseInt(key_value["_oc_"], 10, 64)
|
||||
|
@@ -5,7 +5,8 @@
|
||||
"mmpmon_path": "/path/to/mmpmon",
|
||||
"exclude_filesystem": [
|
||||
"fs1"
|
||||
]
|
||||
],
|
||||
"send_bandwidths" : true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -16,15 +17,18 @@ The reported filesystems can be filtered with the `exclude_filesystem` option
|
||||
in the configuration.
|
||||
|
||||
The path to the `mmpmon` command can be configured with the `mmpmon_path` option
|
||||
in the configuration.
|
||||
in the configuration. If nothing is set, the collector searches in `$PATH` for `mmpmon`.
|
||||
|
||||
|
||||
Metrics:
|
||||
* `bytes_read`
|
||||
* `gpfs_bytes_read`
|
||||
* `gpfs_bytes_written`
|
||||
* `gpfs_num_opens`
|
||||
* `gpfs_num_closes`
|
||||
* `gpfs_num_reads`
|
||||
* `gpfs_num_readdirs`
|
||||
* `gpfs_num_inode_updates`
|
||||
* `gpfs_bw_read` (if `send_bandwidths == true`)
|
||||
* `gpfs_bw_write` (if `send_bandwidths == true`)
|
||||
|
||||
The collector adds a `filesystem` tag to all metrics
|
||||
|
@@ -16,22 +16,31 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const IB_BASEPATH = `/sys/class/infiniband/`
|
||||
const IB_BASEPATH = "/sys/class/infiniband/"
|
||||
|
||||
type InfinibandCollectorMetric struct {
|
||||
path string
|
||||
unit string
|
||||
}
|
||||
|
||||
type InfinibandCollectorInfo struct {
|
||||
LID string // IB local Identifier (LID)
|
||||
device string // IB device
|
||||
port string // IB device port
|
||||
portCounterFiles map[string]string // mapping counter name -> sysfs file
|
||||
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 {
|
||||
metricCollector
|
||||
config struct {
|
||||
ExcludeDevices []string `json:"exclude_devices,omitempty"` // IB device to exclude e.g. mlx5_0
|
||||
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
|
||||
SendDerivedValues bool `json:"send_derived_values"` // Send derived values e.g. rates
|
||||
}
|
||||
info []*InfinibandCollectorInfo
|
||||
info []*InfinibandCollectorInfo
|
||||
lastTimestamp time.Time // Store time stamp of last tick to derive bandwidths
|
||||
}
|
||||
|
||||
// Init initializes the Infiniband collector by walking through files below IB_BASEPATH
|
||||
@@ -45,10 +54,16 @@ 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",
|
||||
}
|
||||
|
||||
// Set default configuration,
|
||||
m.config.SendAbsoluteValues = true
|
||||
m.config.SendDerivedValues = false
|
||||
// Read configuration file, allow overwriting default config
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
@@ -60,10 +75,10 @@ func (m *InfinibandCollector) Init(config json.RawMessage) error {
|
||||
globPattern := filepath.Join(IB_BASEPATH, "*", "ports", "*")
|
||||
ibDirs, err := filepath.Glob(globPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to glob files with pattern %s: %v", globPattern, err)
|
||||
return fmt.Errorf("unable to glob files with pattern %s: %v", globPattern, err)
|
||||
}
|
||||
if ibDirs == nil {
|
||||
return fmt.Errorf("Unable to find any directories with pattern %s", globPattern)
|
||||
return fmt.Errorf("unable to find any directories with pattern %s", globPattern)
|
||||
}
|
||||
|
||||
for _, path := range ibDirs {
|
||||
@@ -97,19 +112,25 @@ func (m *InfinibandCollector) Init(config json.RawMessage) error {
|
||||
|
||||
// Check access to counter files
|
||||
countersDir := filepath.Join(path, "counters")
|
||||
portCounterFiles := map[string]string{
|
||||
"ib_recv": filepath.Join(countersDir, "port_rcv_data"),
|
||||
"ib_xmit": filepath.Join(countersDir, "port_xmit_data"),
|
||||
"ib_recv_pkts": filepath.Join(countersDir, "port_rcv_packets"),
|
||||
"ib_xmit_pkts": filepath.Join(countersDir, "port_xmit_packets"),
|
||||
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 _, counterFile := range portCounterFiles {
|
||||
err := unix.Access(counterFile, unix.R_OK)
|
||||
for _, counter := range portCounterFiles {
|
||||
err := unix.Access(counter.path, unix.R_OK)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to access %s: %v", counterFile, err)
|
||||
return fmt.Errorf("unable to access %s: %v", counter.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize last state
|
||||
lastState := make(map[string]int64)
|
||||
for counter := range portCounterFiles {
|
||||
lastState[counter] = -1
|
||||
}
|
||||
|
||||
m.info = append(m.info,
|
||||
&InfinibandCollectorInfo{
|
||||
LID: LID,
|
||||
@@ -122,11 +143,12 @@ 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")
|
||||
return fmt.Errorf("found no IB devices")
|
||||
}
|
||||
|
||||
m.init = true
|
||||
@@ -141,17 +163,27 @@ func (m *InfinibandCollector) Read(interval time.Duration, output chan lp.CCMetr
|
||||
return
|
||||
}
|
||||
|
||||
// Current time stamp
|
||||
now := time.Now()
|
||||
// time difference to last time stamp
|
||||
timeDiff := now.Sub(m.lastTimestamp).Seconds()
|
||||
// Save current timestamp
|
||||
m.lastTimestamp = now
|
||||
|
||||
for _, info := range m.info {
|
||||
for counterName, counterFile := range info.portCounterFiles {
|
||||
line, err := ioutil.ReadFile(counterFile)
|
||||
for counterName, counterDef := range info.portCounterFiles {
|
||||
|
||||
// Read counter file
|
||||
line, err := ioutil.ReadFile(counterDef.path)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
m.name,
|
||||
fmt.Sprintf("Read(): Failed to read from file '%s': %v", counterFile, err))
|
||||
fmt.Sprintf("Read(): Failed to read from file '%s': %v", counterDef.path, err))
|
||||
continue
|
||||
}
|
||||
data := strings.TrimSpace(string(line))
|
||||
|
||||
// convert counter to int64
|
||||
v, err := strconv.ParseInt(data, 10, 64)
|
||||
if err != nil {
|
||||
cclog.ComponentError(
|
||||
@@ -159,8 +191,26 @@ func (m *InfinibandCollector) Read(interval time.Duration, output chan lp.CCMetr
|
||||
fmt.Sprintf("Read(): Failed to convert Infininiband metrice %s='%s' to int64: %v", counterName, data, err))
|
||||
continue
|
||||
}
|
||||
if y, err := lp.New(counterName, info.tagSet, m.meta, map[string]interface{}{"value": v}, now); err == nil {
|
||||
output <- y
|
||||
|
||||
// Send absolut values
|
||||
if m.config.SendAbsoluteValues {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Send derived values
|
||||
if m.config.SendDerivedValues {
|
||||
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
|
||||
}
|
||||
}
|
||||
// Save current state
|
||||
info.lastState[counterName] = v
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,9 @@
|
||||
"ibstat": {
|
||||
"exclude_devices": [
|
||||
"mlx4"
|
||||
]
|
||||
],
|
||||
"send_abs_values": true,
|
||||
"send_derived_values": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -22,5 +24,9 @@ Metrics:
|
||||
* `ib_xmit`
|
||||
* `ib_recv_pkts`
|
||||
* `ib_xmit_pkts`
|
||||
* `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`)
|
||||
* `ib_xmit_pkts_bw` (if `send_derived_values == true`)
|
||||
|
||||
The collector adds a `device` tag to all metrics
|
||||
|
@@ -1,232 +0,0 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
|
||||
// "os"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const PERFQUERY = `/usr/sbin/perfquery`
|
||||
|
||||
type InfinibandPerfQueryCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
lids map[string]map[string]string
|
||||
config struct {
|
||||
ExcludeDevices []string `json:"exclude_devices,omitempty"`
|
||||
PerfQueryPath string `json:"perfquery_path"`
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InfinibandPerfQueryCollector) Init(config json.RawMessage) error {
|
||||
var err error
|
||||
m.name = "InfinibandCollectorPerfQuery"
|
||||
m.setup()
|
||||
m.meta = map[string]string{"source": m.name, "group": "Network"}
|
||||
m.tags = map[string]string{"type": "node"}
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(m.config.PerfQueryPath) == 0 {
|
||||
path, err := exec.LookPath("perfquery")
|
||||
if err == nil {
|
||||
m.config.PerfQueryPath = path
|
||||
}
|
||||
}
|
||||
m.lids = make(map[string]map[string]string)
|
||||
p := fmt.Sprintf("%s/*/ports/*/lid", string(IB_BASEPATH))
|
||||
files, err := filepath.Glob(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
lid, err := ioutil.ReadFile(f)
|
||||
if err == nil {
|
||||
plist := strings.Split(strings.Replace(f, string(IB_BASEPATH), "", -1), "/")
|
||||
skip := false
|
||||
for _, d := range m.config.ExcludeDevices {
|
||||
if d == plist[0] {
|
||||
skip = true
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
m.lids[plist[0]] = make(map[string]string)
|
||||
m.lids[plist[0]][plist[2]] = string(lid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ports := range m.lids {
|
||||
for port, lid := range ports {
|
||||
args := fmt.Sprintf("-r %s %s 0xf000", lid, port)
|
||||
command := exec.Command(m.config.PerfQueryPath, args)
|
||||
command.Wait()
|
||||
_, err := command.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to execute %s: %v", m.config.PerfQueryPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.lids) == 0 {
|
||||
return errors.New("No usable IB devices")
|
||||
}
|
||||
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *InfinibandPerfQueryCollector) doPerfQuery(cmd string, dev string, lid string, port string, tags map[string]string, output chan lp.CCMetric) error {
|
||||
|
||||
args := fmt.Sprintf("-r %s %s 0xf000", lid, port)
|
||||
command := exec.Command(cmd, args)
|
||||
command.Wait()
|
||||
stdout, err := command.Output()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return err
|
||||
}
|
||||
ll := strings.Split(string(stdout), "\n")
|
||||
|
||||
for _, line := range ll {
|
||||
if strings.HasPrefix(line, "PortRcvData") || strings.HasPrefix(line, "RcvData") {
|
||||
lv := strings.Fields(line)
|
||||
v, err := strconv.ParseFloat(lv[1], 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_recv", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "PortXmitData") || strings.HasPrefix(line, "XmtData") {
|
||||
lv := strings.Fields(line)
|
||||
v, err := strconv.ParseFloat(lv[1], 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_xmit", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "PortRcvPkts") || strings.HasPrefix(line, "RcvPkts") {
|
||||
lv := strings.Fields(line)
|
||||
v, err := strconv.ParseFloat(lv[1], 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_recv_pkts", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "PortXmitPkts") || strings.HasPrefix(line, "XmtPkts") {
|
||||
lv := strings.Fields(line)
|
||||
v, err := strconv.ParseFloat(lv[1], 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_xmit_pkts", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "PortRcvPkts") || strings.HasPrefix(line, "RcvPkts") {
|
||||
lv := strings.Fields(line)
|
||||
v, err := strconv.ParseFloat(lv[1], 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_recv_pkts", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "PortXmitPkts") || strings.HasPrefix(line, "XmtPkts") {
|
||||
lv := strings.Fields(line)
|
||||
v, err := strconv.ParseFloat(lv[1], 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_xmit_pkts", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *InfinibandPerfQueryCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
|
||||
if m.init {
|
||||
for dev, ports := range m.lids {
|
||||
for port, lid := range ports {
|
||||
tags := map[string]string{
|
||||
"type": "node",
|
||||
"device": dev,
|
||||
"port": port,
|
||||
"lid": lid}
|
||||
path := fmt.Sprintf("%s/%s/ports/%s/counters/", string(IB_BASEPATH), dev, port)
|
||||
buffer, err := ioutil.ReadFile(fmt.Sprintf("%s/port_rcv_data", path))
|
||||
if err == nil {
|
||||
data := strings.Replace(string(buffer), "\n", "", -1)
|
||||
v, err := strconv.ParseFloat(data, 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_recv", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer, err = ioutil.ReadFile(fmt.Sprintf("%s/port_xmit_data", path))
|
||||
if err == nil {
|
||||
data := strings.Replace(string(buffer), "\n", "", -1)
|
||||
v, err := strconv.ParseFloat(data, 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_xmit", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer, err = ioutil.ReadFile(fmt.Sprintf("%s/port_rcv_packets", path))
|
||||
if err == nil {
|
||||
data := strings.Replace(string(buffer), "\n", "", -1)
|
||||
v, err := strconv.ParseFloat(data, 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_recv_pkts", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer, err = ioutil.ReadFile(fmt.Sprintf("%s/port_xmit_packets", path))
|
||||
if err == nil {
|
||||
data := strings.Replace(string(buffer), "\n", "", -1)
|
||||
v, err := strconv.ParseFloat(data, 64)
|
||||
if err == nil {
|
||||
y, err := lp.New("ib_xmit_pkts", tags, m.meta, map[string]interface{}{"value": float64(v)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InfinibandPerfQueryCollector) Close() {
|
||||
m.init = false
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
|
||||
## `ibstat_perfquery` collector
|
||||
|
||||
```json
|
||||
"ibstat_perfquery": {
|
||||
"perfquery_path": "/path/to/perfquery",
|
||||
"exclude_devices": [
|
||||
"mlx4"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `ibstat_perfquery` collector includes all Infiniband devices that can be
|
||||
found below `/sys/class/infiniband/` and where any of the ports provides a
|
||||
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 calls the `perfquery` command. The path to the
|
||||
`perfquery` command can be configured with the `perfquery_path` option in the configuration
|
||||
|
||||
Metrics:
|
||||
* `ib_recv`
|
||||
* `ib_xmit`
|
||||
* `ib_recv_pkts`
|
||||
* `ib_xmit_pkts`
|
||||
|
||||
The collector adds a `device` tag to all metrics
|
@@ -37,6 +37,7 @@ type IOstatCollector struct {
|
||||
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 {
|
||||
|
@@ -34,6 +34,7 @@ type IpmiCollector struct {
|
||||
func (m *IpmiCollector) Init(config json.RawMessage) error {
|
||||
m.name = "IpmiCollector"
|
||||
m.setup()
|
||||
m.parallel = true
|
||||
m.meta = map[string]string{"source": m.name, "group": "IPMI"}
|
||||
m.config.IpmitoolPath = string(IPMITOOL_PATH)
|
||||
m.config.IpmisensorsPath = string(IPMISENSORS_PATH)
|
||||
@@ -54,7 +55,7 @@ func (m *IpmiCollector) Init(config json.RawMessage) error {
|
||||
m.ipmisensors = p
|
||||
}
|
||||
if len(m.ipmitool) == 0 && len(m.ipmisensors) == 0 {
|
||||
return errors.New("No IPMI reader found")
|
||||
return errors.New("no IPMI reader found")
|
||||
}
|
||||
m.init = true
|
||||
return nil
|
||||
|
@@ -2,7 +2,7 @@ package collectors
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I./likwid
|
||||
#cgo LDFLAGS: -L./likwid -llikwid -llikwid-hwloc -lm -Wl,--unresolved-symbols=ignore-in-object-files
|
||||
#cgo LDFLAGS: -Wl,--unresolved-symbols=ignore-in-object-files
|
||||
#include <stdlib.h>
|
||||
#include <likwid.h>
|
||||
*/
|
||||
@@ -15,9 +15,12 @@ import (
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -28,66 +31,33 @@ import (
|
||||
"github.com/NVIDIA/go-nvml/pkg/dl"
|
||||
)
|
||||
|
||||
type MetricScope string
|
||||
|
||||
const (
|
||||
METRIC_SCOPE_HWTHREAD = iota
|
||||
METRIC_SCOPE_CORE
|
||||
METRIC_SCOPE_LLC
|
||||
METRIC_SCOPE_NUMA
|
||||
METRIC_SCOPE_DIE
|
||||
METRIC_SCOPE_SOCKET
|
||||
METRIC_SCOPE_NODE
|
||||
)
|
||||
|
||||
func (ms MetricScope) String() string {
|
||||
return string(ms)
|
||||
}
|
||||
|
||||
func (ms MetricScope) Likwid() string {
|
||||
LikwidDomains := map[string]string{
|
||||
"cpu": "",
|
||||
"core": "",
|
||||
"llc": "C",
|
||||
"numadomain": "M",
|
||||
"die": "D",
|
||||
"socket": "S",
|
||||
"node": "N",
|
||||
}
|
||||
return LikwidDomains[string(ms)]
|
||||
}
|
||||
|
||||
func (ms MetricScope) Granularity() int {
|
||||
for i, g := range GetAllMetricScopes() {
|
||||
if ms == g {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetAllMetricScopes() []MetricScope {
|
||||
return []MetricScope{"cpu" /*, "core", "llc", "numadomain", "die",*/, "socket", "node"}
|
||||
}
|
||||
|
||||
const (
|
||||
LIKWID_LIB_NAME = "liblikwid.so"
|
||||
LIKWID_LIB_DL_FLAGS = dl.RTLD_LAZY | dl.RTLD_GLOBAL
|
||||
LIKWID_LIB_NAME = "liblikwid.so"
|
||||
LIKWID_LIB_DL_FLAGS = dl.RTLD_LAZY | dl.RTLD_GLOBAL
|
||||
LIKWID_DEF_ACCESSMODE = "direct"
|
||||
)
|
||||
|
||||
type LikwidCollectorMetricConfig struct {
|
||||
Name string `json:"name"` // Name of the metric
|
||||
Calc string `json:"calc"` // Calculation for the metric using
|
||||
//Aggr string `json:"aggregation"` // if scope unequal to LIKWID metric scope, the values are combined (sum, min, max, mean or avg, median)
|
||||
Scope MetricScope `json:"scope"` // scope for calculation. subscopes are aggregated using the 'aggregation' function
|
||||
Publish bool `json:"publish"`
|
||||
granulatity MetricScope
|
||||
Name string `json:"name"` // Name of the metric
|
||||
Calc string `json:"calc"` // Calculation for the metric using
|
||||
Type string `json:"type"` // Metric type (aka node, socket, cpu, ...)
|
||||
Publish bool `json:"publish"`
|
||||
Unit string `json:"unit"` // Unit of metric if any
|
||||
}
|
||||
|
||||
type LikwidCollectorEventsetConfig struct {
|
||||
Events map[string]string `json:"events"`
|
||||
granulatity map[string]MetricScope
|
||||
Metrics []LikwidCollectorMetricConfig `json:"metrics"`
|
||||
Events map[string]string `json:"events"`
|
||||
Metrics []LikwidCollectorMetricConfig `json:"metrics"`
|
||||
}
|
||||
|
||||
type LikwidEventsetConfig struct {
|
||||
internal int
|
||||
gid C.int
|
||||
eorder []*C.char
|
||||
estr *C.char
|
||||
go_estr string
|
||||
results map[int]map[string]interface{}
|
||||
metrics map[int]map[string]float64
|
||||
}
|
||||
|
||||
type LikwidCollectorConfig struct {
|
||||
@@ -95,28 +65,31 @@ type LikwidCollectorConfig struct {
|
||||
Metrics []LikwidCollectorMetricConfig `json:"globalmetrics,omitempty"`
|
||||
ForceOverwrite bool `json:"force_overwrite,omitempty"`
|
||||
InvalidToZero bool `json:"invalid_to_zero,omitempty"`
|
||||
AccessMode string `json:"access_mode,omitempty"`
|
||||
DaemonPath string `json:"accessdaemon_path,omitempty"`
|
||||
LibraryPath string `json:"liblikwid_path,omitempty"`
|
||||
}
|
||||
|
||||
type LikwidCollector struct {
|
||||
metricCollector
|
||||
cpulist []C.int
|
||||
cpu2tid map[int]int
|
||||
sock2tid map[int]int
|
||||
scopeRespTids map[MetricScope]map[int]int
|
||||
metrics map[C.int]map[string]int
|
||||
groups []C.int
|
||||
config LikwidCollectorConfig
|
||||
results map[int]map[int]map[string]interface{}
|
||||
mresults map[int]map[int]map[string]float64
|
||||
gmresults map[int]map[string]float64
|
||||
basefreq float64
|
||||
running bool
|
||||
cpulist []C.int
|
||||
cpu2tid map[int]int
|
||||
sock2tid map[int]int
|
||||
metrics map[C.int]map[string]int
|
||||
groups []C.int
|
||||
config LikwidCollectorConfig
|
||||
gmresults map[int]map[string]float64
|
||||
basefreq float64
|
||||
running bool
|
||||
initialized bool
|
||||
likwidGroups map[C.int]LikwidEventsetConfig
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type LikwidMetric struct {
|
||||
name string
|
||||
search string
|
||||
scope MetricScope
|
||||
scope string
|
||||
group_idx int
|
||||
}
|
||||
|
||||
@@ -128,340 +101,276 @@ func eventsToEventStr(events map[string]string) string {
|
||||
return strings.Join(elist, ",")
|
||||
}
|
||||
|
||||
func getGranularity(counter, event string) MetricScope {
|
||||
if strings.HasPrefix(counter, "PMC") || strings.HasPrefix(counter, "FIXC") {
|
||||
return "cpu"
|
||||
} else if strings.Contains(counter, "BOX") || strings.Contains(counter, "DEV") {
|
||||
return "socket"
|
||||
} else if strings.HasPrefix(counter, "PWR") {
|
||||
if event == "RAPL_CORE_ENERGY" {
|
||||
return "cpu"
|
||||
} else {
|
||||
return "socket"
|
||||
func genLikwidEventSet(input LikwidCollectorEventsetConfig) LikwidEventsetConfig {
|
||||
tmplist := make([]string, 0)
|
||||
clist := make([]string, 0)
|
||||
for k := range input.Events {
|
||||
clist = append(clist, k)
|
||||
}
|
||||
sort.Strings(clist)
|
||||
elist := make([]*C.char, 0)
|
||||
for _, k := range clist {
|
||||
v := input.Events[k]
|
||||
tmplist = append(tmplist, fmt.Sprintf("%s:%s", v, k))
|
||||
c_counter := C.CString(k)
|
||||
elist = append(elist, c_counter)
|
||||
}
|
||||
estr := strings.Join(tmplist, ",")
|
||||
res := make(map[int]map[string]interface{})
|
||||
met := make(map[int]map[string]float64)
|
||||
for _, i := range topo.CpuList() {
|
||||
res[i] = make(map[string]interface{})
|
||||
for k := range input.Events {
|
||||
res[i][k] = 0.0
|
||||
}
|
||||
met[i] = make(map[string]float64)
|
||||
for _, v := range input.Metrics {
|
||||
res[i][v.Name] = 0.0
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
return LikwidEventsetConfig{
|
||||
gid: -1,
|
||||
eorder: elist,
|
||||
estr: C.CString(estr),
|
||||
go_estr: estr,
|
||||
results: res,
|
||||
metrics: met,
|
||||
}
|
||||
}
|
||||
|
||||
func testLikwidMetricFormula(formula string, params []string) bool {
|
||||
myparams := make(map[string]interface{})
|
||||
for _, p := range params {
|
||||
myparams[p] = float64(1.0)
|
||||
}
|
||||
_, err := agg.EvalFloat64Condition(formula, myparams)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func getBaseFreq() float64 {
|
||||
files := []string{
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/bios_limit",
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/base_frequency",
|
||||
}
|
||||
var freq float64 = math.NaN()
|
||||
C.power_init(0)
|
||||
info := C.get_powerInfo()
|
||||
if float64(info.baseFrequency) != 0 {
|
||||
freq = float64(info.baseFrequency) * 1e3
|
||||
} else {
|
||||
buffer, err := ioutil.ReadFile("/sys/devices/system/cpu/cpu0/cpufreq/bios_limit")
|
||||
for _, f := range files {
|
||||
buffer, err := ioutil.ReadFile(f)
|
||||
if err == nil {
|
||||
data := strings.Replace(string(buffer), "\n", "", -1)
|
||||
x, err := strconv.ParseInt(data, 0, 64)
|
||||
if err == nil {
|
||||
freq = float64(x) * 1e3
|
||||
freq = float64(x) * 1e6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if math.IsNaN(freq) {
|
||||
C.power_init(0)
|
||||
info := C.get_powerInfo()
|
||||
if float64(info.baseFrequency) != 0 {
|
||||
freq = float64(info.baseFrequency) * 1e6
|
||||
}
|
||||
C.power_finalize()
|
||||
}
|
||||
return freq
|
||||
}
|
||||
|
||||
func (m *LikwidCollector) initGranularity() {
|
||||
splitRegex := regexp.MustCompile("[+-/*()]")
|
||||
for _, evset := range m.config.Eventsets {
|
||||
evset.granulatity = make(map[string]MetricScope)
|
||||
for counter, event := range evset.Events {
|
||||
gran := getGranularity(counter, event)
|
||||
if gran.Granularity() >= 0 {
|
||||
evset.granulatity[counter] = gran
|
||||
}
|
||||
}
|
||||
for i, metric := range evset.Metrics {
|
||||
s := splitRegex.Split(metric.Calc, -1)
|
||||
gran := MetricScope("cpu")
|
||||
evset.Metrics[i].granulatity = gran
|
||||
for _, x := range s {
|
||||
if _, ok := evset.Events[x]; ok {
|
||||
if evset.granulatity[x].Granularity() > gran.Granularity() {
|
||||
gran = evset.granulatity[x]
|
||||
}
|
||||
}
|
||||
}
|
||||
evset.Metrics[i].granulatity = gran
|
||||
}
|
||||
}
|
||||
for i, metric := range m.config.Metrics {
|
||||
s := splitRegex.Split(metric.Calc, -1)
|
||||
gran := MetricScope("cpu")
|
||||
m.config.Metrics[i].granulatity = gran
|
||||
for _, x := range s {
|
||||
for _, evset := range m.config.Eventsets {
|
||||
for _, m := range evset.Metrics {
|
||||
if m.Name == x && m.granulatity.Granularity() > gran.Granularity() {
|
||||
gran = m.granulatity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.config.Metrics[i].granulatity = gran
|
||||
}
|
||||
}
|
||||
|
||||
type TopoResolveFunc func(cpuid int) int
|
||||
|
||||
func (m *LikwidCollector) getResponsiblities() map[MetricScope]map[int]int {
|
||||
get_cpus := func(scope MetricScope) map[int]int {
|
||||
var slist []int
|
||||
var cpu C.int
|
||||
var input func(index int) string
|
||||
switch scope {
|
||||
case "node":
|
||||
slist = []int{0}
|
||||
input = func(index int) string { return "N:0" }
|
||||
case "socket":
|
||||
input = func(index int) string { return fmt.Sprintf("%s%d:0", scope.Likwid(), index) }
|
||||
slist = topo.SocketList()
|
||||
// case "numadomain":
|
||||
// input = func(index int) string { return fmt.Sprintf("%s%d:0", scope.Likwid(), index) }
|
||||
// slist = topo.NumaNodeList()
|
||||
// cclog.Debug(scope, " ", input(0), " ", slist)
|
||||
// case "die":
|
||||
// input = func(index int) string { return fmt.Sprintf("%s%d:0", scope.Likwid(), index) }
|
||||
// slist = topo.DieList()
|
||||
// case "llc":
|
||||
// input = fmt.Sprintf("%s%d:0", scope.Likwid(), s)
|
||||
// slist = topo.LLCacheList()
|
||||
case "cpu":
|
||||
input = func(index int) string { return fmt.Sprintf("%d", index) }
|
||||
slist = topo.CpuList()
|
||||
case "hwthread":
|
||||
input = func(index int) string { return fmt.Sprintf("%d", index) }
|
||||
slist = topo.CpuList()
|
||||
}
|
||||
outmap := make(map[int]int)
|
||||
for _, s := range slist {
|
||||
t := C.CString(input(s))
|
||||
clen := C.cpustr_to_cpulist(t, &cpu, 1)
|
||||
if int(clen) == 1 {
|
||||
outmap[s] = m.cpu2tid[int(cpu)]
|
||||
} else {
|
||||
cclog.Error(fmt.Sprintf("Cannot determine responsible CPU for %s", input(s)))
|
||||
outmap[s] = -1
|
||||
}
|
||||
C.free(unsafe.Pointer(t))
|
||||
}
|
||||
return outmap
|
||||
}
|
||||
|
||||
scopes := GetAllMetricScopes()
|
||||
complete := make(map[MetricScope]map[int]int)
|
||||
for _, s := range scopes {
|
||||
complete[s] = get_cpus(s)
|
||||
}
|
||||
return complete
|
||||
}
|
||||
|
||||
func (m *LikwidCollector) Init(config json.RawMessage) error {
|
||||
var ret C.int
|
||||
m.name = "LikwidCollector"
|
||||
m.parallel = false
|
||||
m.initialized = false
|
||||
m.running = false
|
||||
m.config.AccessMode = LIKWID_DEF_ACCESSMODE
|
||||
m.config.LibraryPath = LIKWID_LIB_NAME
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
lib := dl.New(LIKWID_LIB_NAME, LIKWID_LIB_DL_FLAGS)
|
||||
lib := dl.New(m.config.LibraryPath, LIKWID_LIB_DL_FLAGS)
|
||||
if lib == nil {
|
||||
return fmt.Errorf("error instantiating DynamicLibrary for %s", LIKWID_LIB_NAME)
|
||||
return fmt.Errorf("error instantiating DynamicLibrary for %s", m.config.LibraryPath)
|
||||
}
|
||||
err := lib.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening %s: %v", m.config.LibraryPath, err)
|
||||
}
|
||||
|
||||
if m.config.ForceOverwrite {
|
||||
cclog.ComponentDebug(m.name, "Set LIKWID_FORCE=1")
|
||||
os.Setenv("LIKWID_FORCE", "1")
|
||||
}
|
||||
m.setup()
|
||||
|
||||
m.meta = map[string]string{"source": m.name, "group": "PerfCounter"}
|
||||
m.meta = map[string]string{"group": "PerfCounter"}
|
||||
cclog.ComponentDebug(m.name, "Get cpulist and init maps and lists")
|
||||
cpulist := topo.CpuList()
|
||||
cpulist := topo.HwthreadList()
|
||||
m.cpulist = make([]C.int, len(cpulist))
|
||||
m.cpu2tid = make(map[int]int)
|
||||
for i, c := range cpulist {
|
||||
m.cpulist[i] = C.int(c)
|
||||
m.cpu2tid[c] = i
|
||||
|
||||
}
|
||||
m.results = make(map[int]map[int]map[string]interface{})
|
||||
m.mresults = make(map[int]map[int]map[string]float64)
|
||||
|
||||
m.likwidGroups = make(map[C.int]LikwidEventsetConfig)
|
||||
|
||||
// m.results = make(map[int]map[int]map[string]interface{})
|
||||
// m.mresults = make(map[int]map[int]map[string]float64)
|
||||
m.gmresults = make(map[int]map[string]float64)
|
||||
cclog.ComponentDebug(m.name, "initialize LIKWID topology")
|
||||
ret = C.topology_init()
|
||||
if ret != 0 {
|
||||
err := errors.New("failed to initialize LIKWID topology")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Determine which counter works at which level. PMC*: cpu, *BOX*: socket, ...
|
||||
m.initGranularity()
|
||||
// Generate map for MetricScope -> scope_id (like socket id) -> responsible id (offset in cpulist)
|
||||
m.scopeRespTids = m.getResponsiblities()
|
||||
|
||||
cclog.ComponentDebug(m.name, "initialize LIKWID perfmon module")
|
||||
ret = C.perfmon_init(C.int(len(m.cpulist)), &m.cpulist[0])
|
||||
if ret != 0 {
|
||||
C.topology_finalize()
|
||||
err := errors.New("failed to initialize LIKWID topology")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
for _, tid := range m.cpu2tid {
|
||||
m.gmresults[tid] = make(map[string]float64)
|
||||
}
|
||||
|
||||
// This is for the global metrics computation test
|
||||
globalParams := make(map[string]interface{})
|
||||
globalParams["time"] = float64(1.0)
|
||||
globalParams["inverseClock"] = float64(1.0)
|
||||
// While adding the events, we test the metrics whether they can be computed at all
|
||||
for i, evset := range m.config.Eventsets {
|
||||
estr := eventsToEventStr(evset.Events)
|
||||
// Generate parameter list for the metric computing test
|
||||
params := make(map[string]interface{})
|
||||
params["time"] = float64(1.0)
|
||||
params["inverseClock"] = float64(1.0)
|
||||
for counter := range evset.Events {
|
||||
params[counter] = float64(1.0)
|
||||
}
|
||||
for _, metric := range evset.Metrics {
|
||||
// Try to evaluate the metric
|
||||
_, err := agg.EvalFloat64Condition(metric.Calc, params)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Calculation for metric", metric.Name, "failed:", err.Error())
|
||||
continue
|
||||
totalMetrics := 0
|
||||
// Generate parameter list for the metric computing test
|
||||
params := make([]string, 0)
|
||||
params = append(params, "time", "inverseClock")
|
||||
// Generate parameter list for the global metric computing test
|
||||
globalParams := make([]string, 0)
|
||||
globalParams = append(globalParams, "time", "inverseClock")
|
||||
// We test the eventset metrics whether they can be computed at all
|
||||
for _, evset := range m.config.Eventsets {
|
||||
if len(evset.Events) > 0 {
|
||||
params = params[:2]
|
||||
for counter := range evset.Events {
|
||||
params = append(params, counter)
|
||||
}
|
||||
// If the metric is not in the parameter list for the global metrics, add it
|
||||
if _, ok := globalParams[metric.Name]; !ok {
|
||||
globalParams[metric.Name] = float64(1.0)
|
||||
}
|
||||
}
|
||||
// Now we add the list of events to likwid
|
||||
cstr := C.CString(estr)
|
||||
gid := C.perfmon_addEventSet(cstr)
|
||||
if gid >= 0 {
|
||||
m.groups = append(m.groups, gid)
|
||||
}
|
||||
C.free(unsafe.Pointer(cstr))
|
||||
m.results[i] = make(map[int]map[string]interface{})
|
||||
m.mresults[i] = make(map[int]map[string]float64)
|
||||
for tid := range m.cpulist {
|
||||
m.results[i][tid] = make(map[string]interface{})
|
||||
m.mresults[i][tid] = make(map[string]float64)
|
||||
if i == 0 {
|
||||
m.gmresults[tid] = make(map[string]float64)
|
||||
for _, metric := range evset.Metrics {
|
||||
// Try to evaluate the metric
|
||||
if testLikwidMetricFormula(metric.Calc, params) {
|
||||
// Add the computable metric to the parameter list for the global metrics
|
||||
globalParams = append(globalParams, metric.Name)
|
||||
totalMetrics++
|
||||
} else {
|
||||
metric.Calc = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cclog.ComponentError(m.name, "Invalid Likwid eventset config, no events given")
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, metric := range m.config.Metrics {
|
||||
// Try to evaluate the global metric
|
||||
_, err := agg.EvalFloat64Condition(metric.Calc, globalParams)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Calculation for metric", metric.Name, "failed:", err.Error())
|
||||
continue
|
||||
if !testLikwidMetricFormula(metric.Calc, globalParams) {
|
||||
cclog.ComponentError(m.name, "Calculation for metric", metric.Name, "failed")
|
||||
metric.Calc = ""
|
||||
} else {
|
||||
totalMetrics++
|
||||
}
|
||||
}
|
||||
|
||||
// If no event set could be added, shut down LikwidCollector
|
||||
if len(m.groups) == 0 {
|
||||
C.perfmon_finalize()
|
||||
C.topology_finalize()
|
||||
err := errors.New("no LIKWID performance group initialized")
|
||||
if totalMetrics == 0 {
|
||||
err := errors.New("no LIKWID eventset or metric usable")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
m.basefreq = getBaseFreq()
|
||||
cclog.ComponentDebug(m.name, "BaseFreq", m.basefreq)
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// take a measurement for 'interval' seconds of event set index 'group'
|
||||
func (m *LikwidCollector) takeMeasurement(group int, interval time.Duration) error {
|
||||
func (m *LikwidCollector) takeMeasurement(evset LikwidEventsetConfig, interval time.Duration) (bool, error) {
|
||||
var ret C.int
|
||||
gid := m.groups[group]
|
||||
ret = C.perfmon_setupCounters(gid)
|
||||
if ret != 0 {
|
||||
gctr := C.GoString(C.perfmon_getGroupName(gid))
|
||||
err := fmt.Errorf("failed to setup performance group %d (%s)", gid, gctr)
|
||||
return err
|
||||
m.lock.Lock()
|
||||
if m.initialized {
|
||||
ret = C.perfmon_setupCounters(evset.gid)
|
||||
if ret != 0 {
|
||||
var err error = nil
|
||||
var skip bool = false
|
||||
if ret == -37 {
|
||||
skip = true
|
||||
} else {
|
||||
err = fmt.Errorf("failed to setup performance group %d", evset.gid)
|
||||
}
|
||||
m.lock.Unlock()
|
||||
return skip, err
|
||||
}
|
||||
ret = C.perfmon_startCounters()
|
||||
if ret != 0 {
|
||||
var err error = nil
|
||||
var skip bool = false
|
||||
if ret == -37 {
|
||||
skip = true
|
||||
} else {
|
||||
err = fmt.Errorf("failed to setup performance group %d", evset.gid)
|
||||
}
|
||||
m.lock.Unlock()
|
||||
return skip, err
|
||||
}
|
||||
m.running = true
|
||||
time.Sleep(interval)
|
||||
m.running = false
|
||||
ret = C.perfmon_stopCounters()
|
||||
if ret != 0 {
|
||||
var err error = nil
|
||||
var skip bool = false
|
||||
if ret == -37 {
|
||||
skip = true
|
||||
} else {
|
||||
err = fmt.Errorf("failed to setup performance group %d", evset.gid)
|
||||
}
|
||||
m.lock.Unlock()
|
||||
return skip, err
|
||||
}
|
||||
}
|
||||
ret = C.perfmon_startCounters()
|
||||
if ret != 0 {
|
||||
gctr := C.GoString(C.perfmon_getGroupName(gid))
|
||||
err := fmt.Errorf("failed to start performance group %d (%s)", gid, gctr)
|
||||
return err
|
||||
}
|
||||
m.running = true
|
||||
time.Sleep(interval)
|
||||
m.running = false
|
||||
ret = C.perfmon_stopCounters()
|
||||
if ret != 0 {
|
||||
gctr := C.GoString(C.perfmon_getGroupName(gid))
|
||||
err := fmt.Errorf("failed to stop performance group %d (%s)", gid, gctr)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
m.lock.Unlock()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get all measurement results for an event set, derive the metric values out of the measurement results and send it
|
||||
func (m *LikwidCollector) calcEventsetMetrics(group int, interval time.Duration, output chan lp.CCMetric) error {
|
||||
var eidx C.int
|
||||
evset := m.config.Eventsets[group]
|
||||
gid := m.groups[group]
|
||||
func (m *LikwidCollector) calcEventsetMetrics(evset LikwidEventsetConfig, interval time.Duration, output chan lp.CCMetric) error {
|
||||
invClock := float64(1.0 / m.basefreq)
|
||||
|
||||
// Go over events and get the results
|
||||
for eidx = 0; int(eidx) < len(evset.Events); eidx++ {
|
||||
ctr := C.perfmon_getCounterName(gid, eidx)
|
||||
ev := C.perfmon_getEventName(gid, eidx)
|
||||
gctr := C.GoString(ctr)
|
||||
gev := C.GoString(ev)
|
||||
// MetricScope for the counter (and if needed the event)
|
||||
scope := getGranularity(gctr, gev)
|
||||
// Get the map scope-id -> tids
|
||||
// This way we read less counters like only the responsible hardware thread for a socket
|
||||
scopemap := m.scopeRespTids[scope]
|
||||
for _, tid := range scopemap {
|
||||
if tid >= 0 {
|
||||
m.results[group][tid]["time"] = interval.Seconds()
|
||||
m.results[group][tid]["inverseClock"] = invClock
|
||||
res := C.perfmon_getLastResult(gid, eidx, C.int(tid))
|
||||
m.results[group][tid][gctr] = float64(res)
|
||||
for eidx, counter := range evset.eorder {
|
||||
gctr := C.GoString(counter)
|
||||
for _, tid := range m.cpu2tid {
|
||||
res := C.perfmon_getLastResult(evset.gid, C.int(eidx), C.int(tid))
|
||||
fres := float64(res)
|
||||
if m.config.InvalidToZero && (math.IsNaN(fres) || math.IsInf(fres, 0)) {
|
||||
fres = 0.0
|
||||
}
|
||||
evset.results[tid][gctr] = fres
|
||||
evset.results[tid]["time"] = interval.Seconds()
|
||||
evset.results[tid]["inverseClock"] = invClock
|
||||
}
|
||||
}
|
||||
|
||||
// Go over the event set metrics, derive the value out of the event:counter values and send it
|
||||
for _, metric := range evset.Metrics {
|
||||
for _, metric := range m.config.Eventsets[evset.internal].Metrics {
|
||||
// The metric scope is determined in the Init() function
|
||||
// Get the map scope-id -> tids
|
||||
scopemap := m.scopeRespTids[metric.Scope]
|
||||
scopemap := m.cpu2tid
|
||||
if metric.Type == "socket" {
|
||||
scopemap = m.sock2tid
|
||||
}
|
||||
for domain, tid := range scopemap {
|
||||
if tid >= 0 {
|
||||
value, err := agg.EvalFloat64Condition(metric.Calc, m.results[group][tid])
|
||||
if tid >= 0 && len(metric.Calc) > 0 {
|
||||
value, err := agg.EvalFloat64Condition(metric.Calc, evset.results[tid])
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Calculation for metric", metric.Name, "failed:", err.Error())
|
||||
continue
|
||||
}
|
||||
m.mresults[group][tid][metric.Name] = value
|
||||
if m.config.InvalidToZero && math.IsNaN(value) {
|
||||
value = 0.0
|
||||
}
|
||||
if m.config.InvalidToZero && math.IsInf(value, 0) {
|
||||
if m.config.InvalidToZero && (math.IsNaN(value) || math.IsInf(value, 0)) {
|
||||
value = 0.0
|
||||
}
|
||||
evset.metrics[tid][metric.Name] = value
|
||||
// Now we have the result, send it with the proper tags
|
||||
if !math.IsNaN(value) {
|
||||
if metric.Publish {
|
||||
tags := map[string]string{"type": metric.Scope.String()}
|
||||
if metric.Scope != "node" {
|
||||
tags["type-id"] = fmt.Sprintf("%d", domain)
|
||||
}
|
||||
fields := map[string]interface{}{"value": value}
|
||||
y, err := lp.New(metric.Name, tags, m.meta, fields, time.Now())
|
||||
y, err := lp.New(metric.Name, map[string]string{"type": metric.Type}, m.meta, fields, time.Now())
|
||||
if err == nil {
|
||||
if metric.Type != "node" {
|
||||
y.AddTag("type-id", fmt.Sprintf("%d", domain))
|
||||
}
|
||||
if len(metric.Unit) > 0 {
|
||||
y.AddMeta("unit", metric.Unit)
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
@@ -476,13 +385,16 @@ func (m *LikwidCollector) calcEventsetMetrics(group int, interval time.Duration,
|
||||
// Go over the global metrics, derive the value out of the event sets' metric values and send it
|
||||
func (m *LikwidCollector) calcGlobalMetrics(interval time.Duration, output chan lp.CCMetric) error {
|
||||
for _, metric := range m.config.Metrics {
|
||||
scopemap := m.scopeRespTids[metric.Scope]
|
||||
scopemap := m.cpu2tid
|
||||
if metric.Type == "socket" {
|
||||
scopemap = m.sock2tid
|
||||
}
|
||||
for domain, tid := range scopemap {
|
||||
if tid >= 0 {
|
||||
// Here we generate parameter list
|
||||
params := make(map[string]interface{})
|
||||
for j := range m.groups {
|
||||
for mname, mres := range m.mresults[j][tid] {
|
||||
for _, evset := range m.likwidGroups {
|
||||
for mname, mres := range evset.metrics[tid] {
|
||||
params[mname] = mres
|
||||
}
|
||||
}
|
||||
@@ -490,25 +402,25 @@ func (m *LikwidCollector) calcGlobalMetrics(interval time.Duration, output chan
|
||||
value, err := agg.EvalFloat64Condition(metric.Calc, params)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Calculation for metric", metric.Name, "failed:", err.Error())
|
||||
continue
|
||||
value = 0.0
|
||||
}
|
||||
if m.config.InvalidToZero && (math.IsNaN(value) || math.IsInf(value, 0)) {
|
||||
value = 0.0
|
||||
}
|
||||
m.gmresults[tid][metric.Name] = value
|
||||
if m.config.InvalidToZero && math.IsNaN(value) {
|
||||
value = 0.0
|
||||
}
|
||||
if m.config.InvalidToZero && math.IsInf(value, 0) {
|
||||
value = 0.0
|
||||
}
|
||||
// Now we have the result, send it with the proper tags
|
||||
if !math.IsNaN(value) {
|
||||
if metric.Publish {
|
||||
tags := map[string]string{"type": metric.Scope.String()}
|
||||
if metric.Scope != "node" {
|
||||
tags["type-id"] = fmt.Sprintf("%d", domain)
|
||||
}
|
||||
tags := map[string]string{"type": metric.Type}
|
||||
fields := map[string]interface{}{"value": value}
|
||||
y, err := lp.New(metric.Name, tags, m.meta, fields, time.Now())
|
||||
if err == nil {
|
||||
if metric.Type != "node" {
|
||||
y.AddTag("type-id", fmt.Sprintf("%d", domain))
|
||||
}
|
||||
if len(metric.Unit) > 0 {
|
||||
y.AddMeta("unit", metric.Unit)
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
@@ -519,38 +431,163 @@ func (m *LikwidCollector) calcGlobalMetrics(interval time.Duration, output chan
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *LikwidCollector) LateInit() error {
|
||||
var ret C.int
|
||||
if m.initialized {
|
||||
return nil
|
||||
}
|
||||
switch m.config.AccessMode {
|
||||
case "direct":
|
||||
C.HPMmode(0)
|
||||
case "accessdaemon":
|
||||
if len(m.config.DaemonPath) > 0 {
|
||||
p := os.Getenv("PATH")
|
||||
os.Setenv("PATH", m.config.DaemonPath+":"+p)
|
||||
}
|
||||
C.HPMmode(1)
|
||||
}
|
||||
cclog.ComponentDebug(m.name, "initialize LIKWID topology")
|
||||
ret = C.topology_init()
|
||||
if ret != 0 {
|
||||
err := errors.New("failed to initialize LIKWID topology")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
m.sock2tid = make(map[int]int)
|
||||
tmp := make([]C.int, 1)
|
||||
for _, sid := range topo.SocketList() {
|
||||
cstr := C.CString(fmt.Sprintf("S%d:0", sid))
|
||||
ret = C.cpustr_to_cpulist(cstr, &tmp[0], 1)
|
||||
if ret > 0 {
|
||||
m.sock2tid[sid] = m.cpu2tid[int(tmp[0])]
|
||||
}
|
||||
C.free(unsafe.Pointer(cstr))
|
||||
}
|
||||
|
||||
m.basefreq = getBaseFreq()
|
||||
cclog.ComponentDebug(m.name, "BaseFreq", m.basefreq)
|
||||
|
||||
cclog.ComponentDebug(m.name, "initialize LIKWID perfmon module")
|
||||
ret = C.perfmon_init(C.int(len(m.cpulist)), &m.cpulist[0])
|
||||
if ret != 0 {
|
||||
var err error = nil
|
||||
C.topology_finalize()
|
||||
if ret != -22 {
|
||||
err = errors.New("failed to initialize LIKWID perfmon")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
} else {
|
||||
err = errors.New("access to LIKWID perfmon locked")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// While adding the events, we test the metrics whether they can be computed at all
|
||||
for i, evset := range m.config.Eventsets {
|
||||
var gid C.int
|
||||
if len(evset.Events) > 0 {
|
||||
skip := false
|
||||
likwidGroup := genLikwidEventSet(evset)
|
||||
for _, g := range m.likwidGroups {
|
||||
if likwidGroup.go_estr == g.go_estr {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
// Now we add the list of events to likwid
|
||||
gid = C.perfmon_addEventSet(likwidGroup.estr)
|
||||
if gid >= 0 {
|
||||
likwidGroup.gid = gid
|
||||
likwidGroup.internal = i
|
||||
m.likwidGroups[gid] = likwidGroup
|
||||
}
|
||||
} else {
|
||||
cclog.ComponentError(m.name, "Invalid Likwid eventset config, no events given")
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If no event set could be added, shut down LikwidCollector
|
||||
if len(m.likwidGroups) == 0 {
|
||||
C.perfmon_finalize()
|
||||
C.topology_finalize()
|
||||
err := errors.New("no LIKWID performance group initialized")
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
}
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, syscall.SIGCHLD)
|
||||
signal.Notify(sigchan, os.Interrupt)
|
||||
go func() {
|
||||
<-sigchan
|
||||
|
||||
signal.Stop(sigchan)
|
||||
m.initialized = false
|
||||
}()
|
||||
m.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// main read function taking multiple measurement rounds, each 'interval' seconds long
|
||||
func (m *LikwidCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
var skip bool = false
|
||||
var err error
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range m.groups {
|
||||
// measure event set 'i' for 'interval' seconds
|
||||
err := m.takeMeasurement(i, interval)
|
||||
if !m.initialized {
|
||||
m.lock.Lock()
|
||||
err = m.LateInit()
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
m.lock.Unlock()
|
||||
return
|
||||
}
|
||||
// read measurements and derive event set metrics
|
||||
m.calcEventsetMetrics(i, interval, output)
|
||||
m.initialized = true
|
||||
m.lock.Unlock()
|
||||
}
|
||||
|
||||
if m.initialized && !skip {
|
||||
for _, evset := range m.likwidGroups {
|
||||
if !skip {
|
||||
// measure event set 'i' for 'interval' seconds
|
||||
skip, err = m.takeMeasurement(evset, interval)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !skip {
|
||||
// read measurements and derive event set metrics
|
||||
m.calcEventsetMetrics(evset, interval, output)
|
||||
}
|
||||
}
|
||||
if !skip {
|
||||
// use the event set metrics to derive the global metrics
|
||||
m.calcGlobalMetrics(interval, output)
|
||||
}
|
||||
}
|
||||
// use the event set metrics to derive the global metrics
|
||||
m.calcGlobalMetrics(interval, output)
|
||||
}
|
||||
|
||||
func (m *LikwidCollector) Close() {
|
||||
if m.init {
|
||||
cclog.ComponentDebug(m.name, "Closing ...")
|
||||
m.init = false
|
||||
if m.running {
|
||||
cclog.ComponentDebug(m.name, "Stopping counters")
|
||||
C.perfmon_stopCounters()
|
||||
cclog.ComponentDebug(m.name, "Closing ...")
|
||||
m.lock.Lock()
|
||||
if m.initialized {
|
||||
cclog.ComponentDebug(m.name, "Finalize LIKWID perfmon module")
|
||||
C.perfmon_finalize()
|
||||
m.initialized = false
|
||||
}
|
||||
cclog.ComponentDebug(m.name, "Finalize LIKWID perfmon module")
|
||||
C.perfmon_finalize()
|
||||
m.lock.Unlock()
|
||||
cclog.ComponentDebug(m.name, "Finalize LIKWID topology module")
|
||||
C.topology_finalize()
|
||||
|
||||
cclog.ComponentDebug(m.name, "Closing done")
|
||||
}
|
||||
}
|
||||
|
@@ -3,37 +3,132 @@
|
||||
|
||||
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.
|
||||
|
||||
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. A 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`). 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. **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.
|
||||
```json
|
||||
"likwid": {
|
||||
"force_overwrite" : false,
|
||||
"invalid_to_zero" : false,
|
||||
"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 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. 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.
|
||||
|
||||
Additional options:
|
||||
- `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`.
|
||||
- `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`
|
||||
|
||||
### 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 collector provides the specification of a 'scope' 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 LikwidCollector provides the specification of a `type` for each metric.
|
||||
|
||||
- `cpu` : One metric per CPU hardware thread with the tags `"type" : "cpu"` and `"type-id" : "$cpu_id"`
|
||||
- `hwthread` : One metric per CPU hardware thread with the tags `"type" : "hwthread"` and `"type-id" : "$hwthread_id"`
|
||||
- `socket` : One metric per CPU socket/package with the tags `"type" : "socket"` and `"type-id" : "$socket_id"`
|
||||
|
||||
**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.
|
||||
**Note:** You cannot specify `socket` scope for a metric that is measured at `hwthread` 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 scope `cpu`
|
||||
- All counters `FIXCx`, `PMCy` and `TMAz` have the scope `hwthread`
|
||||
- All counters names containing `BOX` have the scope `socket`
|
||||
- All `PWRx` counters have scope `socket`, except `"PWR1" : "RAPL_CORE_ENERGY"` has `cpu` scope
|
||||
- All `PWRx` counters have scope `socket`, except `"PWR1" : "RAPL_CORE_ENERGY"` has `hwthread` 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
|
||||
[...]
|
||||
short name: ICX
|
||||
[...]
|
||||
$ likwid-perfctr -a
|
||||
[...]
|
||||
MEM_DP
|
||||
MEM
|
||||
FLOPS_SP
|
||||
CLOCK
|
||||
[...]
|
||||
$ scripts/likwid_perfgroup_to_cc_config.py ICX MEM_DP
|
||||
{
|
||||
"events": {
|
||||
"FIXC0": "INSTR_RETIRED_ANY",
|
||||
"FIXC1": "CPU_CLK_UNHALTED_CORE",
|
||||
"..." : "..."
|
||||
},
|
||||
"metrics" : [
|
||||
{
|
||||
"calc": "time",
|
||||
"name": "Runtime (RDTSC) [s]",
|
||||
"publish": true,
|
||||
"unit": "seconds"
|
||||
"scope": "hwthread"
|
||||
},
|
||||
{
|
||||
"..." : "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can copy this JSON and add it to the `eventsets` list. If you specify multiple event sets, you can add globally derived metrics in the extra `global_metrics` section with the metric names as variables.
|
||||
|
||||
### Mixed usage between daemon and users
|
||||
|
||||
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, ...)
|
||||
```
|
||||
$ chown $JOBUSER /var/run/likwid.lock
|
||||
```
|
||||
|
||||
After (SLURM epilog, ...)
|
||||
```
|
||||
$ 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).
|
||||
|
||||
|
||||
### Example configuration
|
||||
|
||||
#### AMD Zen3
|
||||
|
||||
```json
|
||||
"likwid": {
|
||||
"force_overwrite" : false,
|
||||
"nan_to_zero" : false,
|
||||
"invalid_to_zero" : false,
|
||||
"eventsets": [
|
||||
{
|
||||
"events": {
|
||||
@@ -52,25 +147,28 @@ As a guideline:
|
||||
{
|
||||
"name": "ipc",
|
||||
"calc": "PMC0/PMC1",
|
||||
"scope": "cpu",
|
||||
"type": "hwthread",
|
||||
"publish": true
|
||||
},
|
||||
{
|
||||
"name": "flops_any",
|
||||
"calc": "0.000001*PMC2/time",
|
||||
"scope": "cpu",
|
||||
"unit": "MFlops/s",
|
||||
"type": "hwthread",
|
||||
"publish": true
|
||||
},
|
||||
{
|
||||
"name": "clock_mhz",
|
||||
"name": "clock",
|
||||
"calc": "0.000001*(FIXC1/FIXC2)/inverseClock",
|
||||
"scope": "cpu",
|
||||
"type": "hwthread",
|
||||
"unit": "MHz",
|
||||
"publish": true
|
||||
},
|
||||
{
|
||||
"name": "mem1",
|
||||
"calc": "0.000001*(DFC0+DFC1+DFC2+DFC3)*64.0/time",
|
||||
"scope": "socket",
|
||||
"unit": "Mbyte/s",
|
||||
"type": "socket",
|
||||
"publish": false
|
||||
}
|
||||
]
|
||||
@@ -88,19 +186,22 @@ As a guideline:
|
||||
{
|
||||
"name": "pwr_core",
|
||||
"calc": "PWR0/time",
|
||||
"scope": "socket",
|
||||
"unit": "Watt"
|
||||
"type": "socket",
|
||||
"publish": true
|
||||
},
|
||||
{
|
||||
"name": "pwr_pkg",
|
||||
"calc": "PWR1/time",
|
||||
"scope": "socket",
|
||||
"type": "socket",
|
||||
"unit": "Watt"
|
||||
"publish": true
|
||||
},
|
||||
{
|
||||
"name": "mem2",
|
||||
"calc": "0.000001*(DFC0+DFC1+DFC2+DFC3)*64.0/time",
|
||||
"scope": "socket",
|
||||
"unit": "Mbyte/s",
|
||||
"type": "socket",
|
||||
"publish": false
|
||||
}
|
||||
]
|
||||
@@ -110,7 +211,8 @@ As a guideline:
|
||||
{
|
||||
"name": "mem_bw",
|
||||
"calc": "mem1+mem2",
|
||||
"scope": "socket",
|
||||
"type": "socket",
|
||||
"unit": "Mbyte/s",
|
||||
"publish": true
|
||||
}
|
||||
]
|
||||
@@ -119,7 +221,7 @@ As a guideline:
|
||||
|
||||
### How to get the eventsets and metrics from LIKWID
|
||||
|
||||
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 `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 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:
|
||||
```
|
||||
@@ -140,9 +242,10 @@ METRICS -> "metrics": [
|
||||
IPC PMC0/PMC1 -> {
|
||||
-> "name" : "IPC",
|
||||
-> "calc" : "PMC0/PMC1",
|
||||
-> "scope": "cpu",
|
||||
-> "scope": "hwthread",
|
||||
-> "publish": true
|
||||
-> }
|
||||
-> ]
|
||||
```
|
||||
|
||||
The script `scripts/likwid_perfgroup_to_cc_config.py` might help you.
|
||||
|
@@ -36,6 +36,7 @@ type LoadavgCollector struct {
|
||||
|
||||
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)
|
||||
|
@@ -19,23 +19,41 @@ const LCTL_CMD = `lctl`
|
||||
const LCTL_OPTION = `get_param`
|
||||
|
||||
type LustreCollectorConfig struct {
|
||||
LCtlCommand string `json:"lctl_command"`
|
||||
ExcludeMetrics []string `json:"exclude_metrics"`
|
||||
SendAllMetrics bool `json:"send_all_metrics"`
|
||||
LCtlCommand string `json:"lctl_command,omitempty"`
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
Sudo bool `json:"use_sudo,omitempty"`
|
||||
SendAbsoluteValues bool `json:"send_abs_values,omitempty"`
|
||||
SendDerivedValues bool `json:"send_derived_values,omitempty"`
|
||||
SendDiffValues bool `json:"send_diff_values,omitempty"`
|
||||
}
|
||||
|
||||
type LustreMetricDefinition struct {
|
||||
name string
|
||||
lineprefix string
|
||||
lineoffset int
|
||||
unit string
|
||||
calc string
|
||||
}
|
||||
|
||||
type LustreCollector struct {
|
||||
metricCollector
|
||||
tags map[string]string
|
||||
matches map[string]map[string]int
|
||||
stats map[string]map[string]int64
|
||||
config LustreCollectorConfig
|
||||
lctl string
|
||||
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
|
||||
}
|
||||
|
||||
func (m *LustreCollector) getDeviceDataCommand(device string) []string {
|
||||
var command *exec.Cmd
|
||||
statsfile := fmt.Sprintf("llite.%s.stats", device)
|
||||
command := exec.Command(m.lctl, LCTL_OPTION, statsfile)
|
||||
if m.config.Sudo {
|
||||
command = exec.Command(m.sudoCmd, m.lctl, LCTL_OPTION, statsfile)
|
||||
} else {
|
||||
command = exec.Command(m.lctl, LCTL_OPTION, statsfile)
|
||||
}
|
||||
command.Wait()
|
||||
stdout, _ := command.Output()
|
||||
return strings.Split(string(stdout), "\n")
|
||||
@@ -68,6 +86,16 @@ func (m *LustreCollector) getDevices() []string {
|
||||
return devices
|
||||
}
|
||||
|
||||
func getMetricData(lines []string, prefix string, offset int) (int64, error) {
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
lf := strings.Fields(line)
|
||||
return strconv.ParseInt(lf[offset], 0, 64)
|
||||
}
|
||||
}
|
||||
return 0, errors.New("no such line in data")
|
||||
}
|
||||
|
||||
// //Version reading the stats data of a device from sysfs
|
||||
// func (m *LustreCollector) getDeviceDataSysfs(device string) []string {
|
||||
// llitedir := filepath.Join(LUSTRE_SYSFS, "llite")
|
||||
@@ -80,9 +108,187 @@ func (m *LustreCollector) getDevices() []string {
|
||||
// return strings.Split(string(buffer), "\n")
|
||||
// }
|
||||
|
||||
var LustreAbsMetrics = []LustreMetricDefinition{
|
||||
{
|
||||
name: "lustre_read_requests",
|
||||
lineprefix: "read_bytes",
|
||||
lineoffset: 1,
|
||||
unit: "requests",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_write_requests",
|
||||
lineprefix: "write_bytes",
|
||||
lineoffset: 1,
|
||||
unit: "requests",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_read_bytes",
|
||||
lineprefix: "read_bytes",
|
||||
lineoffset: 6,
|
||||
unit: "bytes",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_write_bytes",
|
||||
lineprefix: "write_bytes",
|
||||
lineoffset: 6,
|
||||
unit: "bytes",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_open",
|
||||
lineprefix: "open",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_close",
|
||||
lineprefix: "close",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_setattr",
|
||||
lineprefix: "setattr",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_getattr",
|
||||
lineprefix: "getattr",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_statfs",
|
||||
lineprefix: "statfs",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "none",
|
||||
},
|
||||
{
|
||||
name: "lustre_inode_permission",
|
||||
lineprefix: "inode_permission",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "none",
|
||||
},
|
||||
}
|
||||
|
||||
var LustreDiffMetrics = []LustreMetricDefinition{
|
||||
{
|
||||
name: "lustre_read_requests_diff",
|
||||
lineprefix: "read_bytes",
|
||||
lineoffset: 1,
|
||||
unit: "requests",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_write_requests_diff",
|
||||
lineprefix: "write_bytes",
|
||||
lineoffset: 1,
|
||||
unit: "requests",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_read_bytes_diff",
|
||||
lineprefix: "read_bytes",
|
||||
lineoffset: 6,
|
||||
unit: "bytes",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_write_bytes_diff",
|
||||
lineprefix: "write_bytes",
|
||||
lineoffset: 6,
|
||||
unit: "bytes",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_open_diff",
|
||||
lineprefix: "open",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_close_diff",
|
||||
lineprefix: "close",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_setattr_diff",
|
||||
lineprefix: "setattr",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_getattr_diff",
|
||||
lineprefix: "getattr",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_statfs_diff",
|
||||
lineprefix: "statfs",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "difference",
|
||||
},
|
||||
{
|
||||
name: "lustre_inode_permission_diff",
|
||||
lineprefix: "inode_permission",
|
||||
lineoffset: 1,
|
||||
unit: "",
|
||||
calc: "difference",
|
||||
},
|
||||
}
|
||||
|
||||
var LustreDeriveMetrics = []LustreMetricDefinition{
|
||||
{
|
||||
name: "lustre_read_requests_rate",
|
||||
lineprefix: "read_bytes",
|
||||
lineoffset: 1,
|
||||
unit: "requests/sec",
|
||||
calc: "derivative",
|
||||
},
|
||||
{
|
||||
name: "lustre_write_requests_rate",
|
||||
lineprefix: "write_bytes",
|
||||
lineoffset: 1,
|
||||
unit: "requests/sec",
|
||||
calc: "derivative",
|
||||
},
|
||||
{
|
||||
name: "lustre_read_bw",
|
||||
lineprefix: "read_bytes",
|
||||
lineoffset: 6,
|
||||
unit: "bytes/sec",
|
||||
calc: "derivative",
|
||||
},
|
||||
{
|
||||
name: "lustre_write_bw",
|
||||
lineprefix: "write_bytes",
|
||||
lineoffset: 6,
|
||||
unit: "bytes/sec",
|
||||
calc: "derivative",
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -92,42 +298,28 @@ func (m *LustreCollector) Init(config json.RawMessage) error {
|
||||
m.setup()
|
||||
m.tags = map[string]string{"type": "node"}
|
||||
m.meta = map[string]string{"source": m.name, "group": "Lustre"}
|
||||
defmatches := map[string]map[string]int{
|
||||
"read_bytes": {"lustre_read_bytes": 6, "lustre_read_requests": 1},
|
||||
"write_bytes": {"lustre_write_bytes": 6, "lustre_write_requests": 1},
|
||||
"open": {"lustre_open": 1},
|
||||
"close": {"lustre_close": 1},
|
||||
"setattr": {"lustre_setattr": 1},
|
||||
"getattr": {"lustre_getattr": 1},
|
||||
"statfs": {"lustre_statfs": 1},
|
||||
"inode_permission": {"lustre_inode_permission": 1}}
|
||||
|
||||
// Lustre file system statistics can only be queried by user root
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Failed to get current user:", err.Error())
|
||||
return err
|
||||
}
|
||||
if user.Uid != "0" {
|
||||
cclog.ComponentError(m.name, "Lustre file system statistics can only be queried by user root:", err.Error())
|
||||
return err
|
||||
// or with password-less sudo
|
||||
if !m.config.Sudo {
|
||||
user, err := user.Current()
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Failed to get current user:", err.Error())
|
||||
return err
|
||||
}
|
||||
if user.Uid != "0" {
|
||||
cclog.ComponentError(m.name, "Lustre file system statistics can only be queried by user root")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
p, err := exec.LookPath("sudo")
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Cannot find 'sudo'")
|
||||
return err
|
||||
}
|
||||
m.sudoCmd = p
|
||||
}
|
||||
|
||||
m.matches = make(map[string]map[string]int)
|
||||
for lineprefix, names := range defmatches {
|
||||
for metricname, offset := range names {
|
||||
_, skip := stringArrayContains(m.config.ExcludeMetrics, metricname)
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
if _, prefixExist := m.matches[lineprefix]; !prefixExist {
|
||||
m.matches[lineprefix] = make(map[string]int)
|
||||
}
|
||||
if _, metricExist := m.matches[lineprefix][metricname]; !metricExist {
|
||||
m.matches[lineprefix][metricname] = offset
|
||||
}
|
||||
}
|
||||
}
|
||||
p, err := exec.LookPath(m.config.LCtlCommand)
|
||||
if err != nil {
|
||||
p, err = exec.LookPath(LCTL_CMD)
|
||||
@@ -137,19 +329,50 @@ func (m *LustreCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
m.lctl = p
|
||||
|
||||
m.definitions = []LustreMetricDefinition{}
|
||||
if m.config.SendAbsoluteValues {
|
||||
for _, def := range LustreAbsMetrics {
|
||||
if _, skip := stringArrayContains(m.config.ExcludeMetrics, def.name); !skip {
|
||||
m.definitions = append(m.definitions, def)
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.config.SendDiffValues {
|
||||
for _, def := range LustreDiffMetrics {
|
||||
if _, skip := stringArrayContains(m.config.ExcludeMetrics, def.name); !skip {
|
||||
m.definitions = append(m.definitions, def)
|
||||
}
|
||||
}
|
||||
}
|
||||
if m.config.SendDerivedValues {
|
||||
for _, def := range LustreDeriveMetrics {
|
||||
if _, skip := stringArrayContains(m.config.ExcludeMetrics, def.name); !skip {
|
||||
m.definitions = append(m.definitions, def)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(m.definitions) == 0 {
|
||||
return errors.New("no metrics to collect")
|
||||
}
|
||||
|
||||
devices := m.getDevices()
|
||||
if len(devices) == 0 {
|
||||
return errors.New("no metrics to collect")
|
||||
return errors.New("no Lustre devices found")
|
||||
}
|
||||
m.stats = make(map[string]map[string]int64)
|
||||
for _, d := range devices {
|
||||
m.stats[d] = make(map[string]int64)
|
||||
for _, names := range m.matches {
|
||||
for metricname := range names {
|
||||
m.stats[d][metricname] = 0
|
||||
data := m.getDeviceDataCommand(d)
|
||||
for _, def := range m.definitions {
|
||||
x, err := getMetricData(data, def.lineprefix, def.lineoffset)
|
||||
if err == nil {
|
||||
m.stats[d][def.name] = x
|
||||
} else {
|
||||
m.stats[d][def.name] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
m.lastTimestamp = time.Now()
|
||||
m.init = true
|
||||
return nil
|
||||
}
|
||||
@@ -158,54 +381,49 @@ func (m *LustreCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
tdiff := now.Sub(m.lastTimestamp)
|
||||
for device, devData := range m.stats {
|
||||
stats := m.getDeviceDataCommand(device)
|
||||
processed := []string{}
|
||||
|
||||
for _, line := range stats {
|
||||
lf := strings.Fields(line)
|
||||
if len(lf) > 1 {
|
||||
if fields, ok := m.matches[lf[0]]; ok {
|
||||
for name, idx := range fields {
|
||||
x, err := strconv.ParseInt(lf[idx], 0, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
value := x - devData[name]
|
||||
devData[name] = x
|
||||
if value < 0 {
|
||||
value = 0
|
||||
}
|
||||
y, err := lp.New(name, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
if err == nil {
|
||||
y.AddTag("device", device)
|
||||
if strings.Contains(name, "byte") {
|
||||
y.AddMeta("unit", "Byte")
|
||||
}
|
||||
output <- y
|
||||
if m.config.SendAllMetrics {
|
||||
processed = append(processed, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data := m.getDeviceDataCommand(device)
|
||||
for _, def := range m.definitions {
|
||||
var use_x int64
|
||||
var err error
|
||||
var y lp.CCMetric
|
||||
x, err := getMetricData(data, def.lineprefix, def.lineoffset)
|
||||
if err == nil {
|
||||
use_x = x
|
||||
} else {
|
||||
use_x = devData[def.name]
|
||||
}
|
||||
}
|
||||
if m.config.SendAllMetrics {
|
||||
for name := range devData {
|
||||
if _, done := stringArrayContains(processed, name); !done {
|
||||
y, err := lp.New(name, m.tags, m.meta, map[string]interface{}{"value": 0}, time.Now())
|
||||
if err == nil {
|
||||
y.AddTag("device", device)
|
||||
if strings.Contains(name, "byte") {
|
||||
y.AddMeta("unit", "Byte")
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
var value interface{}
|
||||
switch def.calc {
|
||||
case "none":
|
||||
value = use_x
|
||||
y, err = lp.New(def.name, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
case "difference":
|
||||
value = use_x - devData[def.name]
|
||||
if value.(int64) < 0 {
|
||||
value = 0
|
||||
}
|
||||
y, err = lp.New(def.name, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
case "derivative":
|
||||
value = float64(use_x-devData[def.name]) / tdiff.Seconds()
|
||||
if value.(float64) < 0 {
|
||||
value = 0
|
||||
}
|
||||
y, err = lp.New(def.name, m.tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
}
|
||||
if err == nil {
|
||||
y.AddTag("device", device)
|
||||
if len(def.unit) > 0 {
|
||||
y.AddMeta("unit", def.unit)
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
devData[def.name] = use_x
|
||||
}
|
||||
}
|
||||
m.lastTimestamp = now
|
||||
}
|
||||
|
||||
func (m *LustreCollector) Close() {
|
||||
|
@@ -3,27 +3,44 @@
|
||||
|
||||
```json
|
||||
"lustrestat": {
|
||||
"procfiles" : [
|
||||
"/proc/fs/lustre/llite/lnec-XXXXXX/stats"
|
||||
],
|
||||
"lctl_command": "/path/to/lctl",
|
||||
"exclude_metrics": [
|
||||
"setattr",
|
||||
"getattr"
|
||||
]
|
||||
],
|
||||
"send_abs_values" : true,
|
||||
"send_derived_values" : true,
|
||||
"send_diff_values": true,
|
||||
"use_sudo": false
|
||||
}
|
||||
```
|
||||
|
||||
The `lustrestat` collector reads from the procfs stat files for Lustre like `/proc/fs/lustre/llite/lnec-XXXXXX/stats`.
|
||||
The `lustrestat` collector uses the `lctl` application with the `get_param` option to get all `llite` metrics (Lustre client). The `llite` metrics are only available for root users. If password-less sudo is configured, you can enable `sudo` in the configuration.
|
||||
|
||||
Metrics:
|
||||
* `read_bytes`
|
||||
* `read_requests`
|
||||
* `write_bytes`
|
||||
* `write_requests`
|
||||
* `open`
|
||||
* `close`
|
||||
* `getattr`
|
||||
* `setattr`
|
||||
* `statfs`
|
||||
* `inode_permission`
|
||||
* `lustre_read_bytes` (unit `bytes`)
|
||||
* `lustre_read_requests` (unit `requests`)
|
||||
* `lustre_write_bytes` (unit `bytes`)
|
||||
* `lustre_write_requests` (unit `requests`)
|
||||
* `lustre_open`
|
||||
* `lustre_close`
|
||||
* `lustre_getattr`
|
||||
* `lustre_setattr`
|
||||
* `lustre_statfs`
|
||||
* `lustre_inode_permission`
|
||||
* `lustre_read_bw` (if `send_derived_values == true`, unit `bytes/sec`)
|
||||
* `lustre_write_bw` (if `send_derived_values == true`, unit `bytes/sec`)
|
||||
* `lustre_read_requests_rate` (if `send_derived_values == true`, unit `requests/sec`)
|
||||
* `lustre_write_requests_rate` (if `send_derived_values == true`, unit `requests/sec`)
|
||||
* `lustre_read_bytes_diff` (if `send_diff_values == true`, unit `bytes`)
|
||||
* `lustre_read_requests_diff` (if `send_diff_values == true`, unit `requests`)
|
||||
* `lustre_write_bytes_diff` (if `send_diff_values == true`, unit `bytes`)
|
||||
* `lustre_write_requests_diff` (if `send_diff_values == true`, unit `requests`)
|
||||
* `lustre_open_diff` (if `send_diff_values == true`)
|
||||
* `lustre_close_diff` (if `send_diff_values == true`)
|
||||
* `lustre_getattr_diff` (if `send_diff_values == true`)
|
||||
* `lustre_setattr_diff` (if `send_diff_values == true`)
|
||||
* `lustre_statfs_diff` (if `send_diff_values == true`)
|
||||
* `lustre_inode_permission_diff` (if `send_diff_values == true`)
|
||||
|
||||
This collector adds an `device` tag.
|
@@ -1,46 +1,101 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
const MEMSTATFILE = `/proc/meminfo`
|
||||
const MEMSTATFILE = "/proc/meminfo"
|
||||
const NUMA_MEMSTAT_BASE = "/sys/devices/system/node"
|
||||
|
||||
type MemstatCollectorConfig struct {
|
||||
ExcludeMetrics []string `json:"exclude_metrics"`
|
||||
NodeStats bool `json:"node_stats,omitempty"`
|
||||
NumaStats bool `json:"numa_stats,omitempty"`
|
||||
}
|
||||
|
||||
type MemstatCollectorNode struct {
|
||||
file string
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
type MemstatCollector struct {
|
||||
metricCollector
|
||||
stats map[string]int64
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config MemstatCollectorConfig
|
||||
stats map[string]int64
|
||||
tags map[string]string
|
||||
matches map[string]string
|
||||
config MemstatCollectorConfig
|
||||
nodefiles map[int]MemstatCollectorNode
|
||||
sendMemUsed bool
|
||||
}
|
||||
|
||||
type MemstatStats struct {
|
||||
value float64
|
||||
unit string
|
||||
}
|
||||
|
||||
func getStats(filename string) map[string]MemstatStats {
|
||||
stats := make(map[string]MemstatStats)
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
cclog.Error(err.Error())
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
linefields := strings.Fields(line)
|
||||
if len(linefields) == 3 {
|
||||
v, err := strconv.ParseFloat(linefields[1], 64)
|
||||
if err == nil {
|
||||
stats[strings.Trim(linefields[0], ":")] = MemstatStats{
|
||||
value: v,
|
||||
unit: linefields[2],
|
||||
}
|
||||
}
|
||||
} else if len(linefields) == 5 {
|
||||
v, err := strconv.ParseFloat(linefields[3], 64)
|
||||
if err == nil {
|
||||
stats[strings.Trim(linefields[0], ":")] = MemstatStats{
|
||||
value: v,
|
||||
unit: linefields[4],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
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 {
|
||||
err = json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.meta = map[string]string{"source": m.name, "group": "Memory", "unit": "kByte"}
|
||||
m.meta = map[string]string{"source": m.name, "group": "Memory"}
|
||||
m.stats = make(map[string]int64)
|
||||
m.matches = make(map[string]string)
|
||||
m.tags = map[string]string{"type": "node"}
|
||||
matches := map[string]string{`MemTotal`: "mem_total",
|
||||
matches := map[string]string{
|
||||
"MemTotal": "mem_total",
|
||||
"SwapTotal": "swap_total",
|
||||
"SReclaimable": "mem_sreclaimable",
|
||||
"Slab": "mem_slab",
|
||||
@@ -48,79 +103,126 @@ func (m *MemstatCollector) Init(config json.RawMessage) error {
|
||||
"Buffers": "mem_buffers",
|
||||
"Cached": "mem_cached",
|
||||
"MemAvailable": "mem_available",
|
||||
"SwapFree": "swap_free"}
|
||||
"SwapFree": "swap_free",
|
||||
"MemShared": "mem_shared",
|
||||
}
|
||||
for k, v := range matches {
|
||||
_, skip := stringArrayContains(m.config.ExcludeMetrics, k)
|
||||
if !skip {
|
||||
m.matches[k] = v
|
||||
}
|
||||
}
|
||||
m.sendMemUsed = false
|
||||
if _, skip := stringArrayContains(m.config.ExcludeMetrics, "mem_used"); !skip {
|
||||
m.sendMemUsed = true
|
||||
}
|
||||
if len(m.matches) == 0 {
|
||||
return errors.New("No metrics to collect")
|
||||
return errors.New("no metrics to collect")
|
||||
}
|
||||
m.setup()
|
||||
_, err = ioutil.ReadFile(string(MEMSTATFILE))
|
||||
if err == nil {
|
||||
m.init = true
|
||||
|
||||
if m.config.NodeStats {
|
||||
if stats := getStats(MEMSTATFILE); len(stats) == 0 {
|
||||
return fmt.Errorf("cannot read data from file %s", MEMSTATFILE)
|
||||
}
|
||||
}
|
||||
|
||||
if m.config.NumaStats {
|
||||
globPattern := filepath.Join(NUMA_MEMSTAT_BASE, "node[0-9]*", "meminfo")
|
||||
regex := regexp.MustCompile(filepath.Join(NUMA_MEMSTAT_BASE, "node(\\d+)", "meminfo"))
|
||||
files, err := filepath.Glob(globPattern)
|
||||
if err == nil {
|
||||
m.nodefiles = make(map[int]MemstatCollectorNode)
|
||||
for _, f := range files {
|
||||
if stats := getStats(f); len(stats) == 0 {
|
||||
return fmt.Errorf("cannot read data from file %s", f)
|
||||
}
|
||||
rematch := regex.FindStringSubmatch(f)
|
||||
if len(rematch) == 2 {
|
||||
id, err := strconv.Atoi(rematch[1])
|
||||
if err == nil {
|
||||
f := MemstatCollectorNode{
|
||||
file: f,
|
||||
tags: map[string]string{
|
||||
"type": "memoryDomain",
|
||||
"type-id": fmt.Sprintf("%d", id),
|
||||
},
|
||||
}
|
||||
m.nodefiles[id] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *MemstatCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
if !m.init {
|
||||
cclog.ComponentPrint(m.name, "Here")
|
||||
return
|
||||
}
|
||||
|
||||
buffer, err := ioutil.ReadFile(string(MEMSTATFILE))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
ll := strings.Split(string(buffer), "\n")
|
||||
for _, line := range ll {
|
||||
ls := strings.Split(line, `:`)
|
||||
if len(ls) > 1 {
|
||||
lv := strings.Fields(ls[1])
|
||||
m.stats[ls[0]], err = strconv.ParseInt(lv[0], 0, 64)
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := m.stats[`MemTotal`]; !exists {
|
||||
err = errors.New("Parse error")
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
for match, name := range m.matches {
|
||||
if _, exists := m.stats[match]; !exists {
|
||||
err = fmt.Errorf("Parse error for %s : %s", match, name)
|
||||
log.Print(err)
|
||||
continue
|
||||
}
|
||||
y, err := lp.New(name, m.tags, m.meta, map[string]interface{}{"value": int(float64(m.stats[match]) * 1.0e-3)}, time.Now())
|
||||
if err == nil {
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
|
||||
if _, free := m.stats[`MemFree`]; free {
|
||||
if _, buffers := m.stats[`Buffers`]; buffers {
|
||||
if _, cached := m.stats[`Cached`]; cached {
|
||||
memUsed := m.stats[`MemTotal`] - (m.stats[`MemFree`] + m.stats[`Buffers`] + m.stats[`Cached`])
|
||||
_, skip := stringArrayContains(m.config.ExcludeMetrics, "mem_used")
|
||||
y, err := lp.New("mem_used", m.tags, m.meta, map[string]interface{}{"value": int(float64(memUsed) * 1.0e-3)}, time.Now())
|
||||
if err == nil && !skip {
|
||||
output <- y
|
||||
sendStats := func(stats map[string]MemstatStats, tags map[string]string) {
|
||||
for match, name := range m.matches {
|
||||
var value float64 = 0
|
||||
var unit string = ""
|
||||
if v, ok := stats[match]; ok {
|
||||
value = v.value
|
||||
if len(v.unit) > 0 {
|
||||
unit = v.unit
|
||||
}
|
||||
}
|
||||
|
||||
y, err := lp.New(name, tags, m.meta, map[string]interface{}{"value": value}, time.Now())
|
||||
if err == nil {
|
||||
if len(unit) > 0 {
|
||||
y.AddMeta("unit", unit)
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
if m.sendMemUsed {
|
||||
memUsed := 0.0
|
||||
unit := ""
|
||||
if totalVal, total := stats["MemTotal"]; total {
|
||||
if freeVal, free := stats["MemFree"]; free {
|
||||
if bufVal, buffers := stats["Buffers"]; buffers {
|
||||
if cacheVal, cached := stats["Cached"]; cached {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
y, err := lp.New("mem_used", tags, m.meta, map[string]interface{}{"value": memUsed}, time.Now())
|
||||
if err == nil {
|
||||
if len(unit) > 0 {
|
||||
y.AddMeta("unit", unit)
|
||||
}
|
||||
output <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, found := m.stats[`MemShared`]; found {
|
||||
_, skip := stringArrayContains(m.config.ExcludeMetrics, "mem_shared")
|
||||
y, err := lp.New("mem_shared", m.tags, m.meta, map[string]interface{}{"value": int(float64(m.stats[`MemShared`]) * 1.0e-3)}, time.Now())
|
||||
if err == nil && !skip {
|
||||
output <- y
|
||||
|
||||
if m.config.NodeStats {
|
||||
nodestats := getStats(MEMSTATFILE)
|
||||
sendStats(nodestats, m.tags)
|
||||
}
|
||||
|
||||
if m.config.NumaStats {
|
||||
for _, nodeConf := range m.nodefiles {
|
||||
stats := getStats(nodeConf.file)
|
||||
sendStats(stats, nodeConf.tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,39 +3,43 @@ package collectors
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
type MetricCollector interface {
|
||||
Name() string
|
||||
Init(config json.RawMessage) error
|
||||
Initialized() bool
|
||||
Read(duration time.Duration, output chan lp.CCMetric)
|
||||
Close()
|
||||
Name() string // Name of the metric collector
|
||||
Init(config json.RawMessage) error // Initialize metric collector
|
||||
Initialized() bool // Is metric collector initialized?
|
||||
Parallel() bool
|
||||
Read(duration time.Duration, output chan lp.CCMetric) // Read metrics from metric collector
|
||||
Close() // Close / finish metric collector
|
||||
}
|
||||
|
||||
type metricCollector struct {
|
||||
name string
|
||||
init bool
|
||||
meta map[string]string
|
||||
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() returns the name of the metric collector
|
||||
// Name returns the name of the metric collector
|
||||
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
|
||||
}
|
||||
|
||||
// Initialized() indicates whether the metric collector has been initialized.
|
||||
// Initialized indicates whether the metric collector has been initialized
|
||||
func (c *metricCollector) Initialized() bool {
|
||||
return c.init
|
||||
}
|
||||
@@ -64,63 +68,13 @@ func stringArrayContains(array []string, str string) (int, bool) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
for i, item := range s {
|
||||
if r == item {
|
||||
for i := range s {
|
||||
if r == s[i] {
|
||||
return append(s[:i], s[i+1:]...), nil
|
||||
}
|
||||
}
|
||||
return s, fmt.Errorf("No such string in list")
|
||||
return s, fmt.Errorf("no such string in list")
|
||||
}
|
||||
|
@@ -13,38 +13,62 @@ import (
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
const NETSTATFILE = `/proc/net/dev`
|
||||
const NETSTATFILE = "/proc/net/dev"
|
||||
|
||||
type NetstatCollectorConfig struct {
|
||||
IncludeDevices []string `json:"include_devices"`
|
||||
IncludeDevices []string `json:"include_devices"`
|
||||
SendAbsoluteValues bool `json:"send_abs_values"`
|
||||
SendDerivedValues bool `json:"send_derived_values"`
|
||||
}
|
||||
|
||||
type NetstatCollectorMetric struct {
|
||||
index int
|
||||
lastValue float64
|
||||
name string
|
||||
index int
|
||||
tags map[string]string
|
||||
meta map[string]string
|
||||
meta_rates map[string]string
|
||||
lastValue int64
|
||||
}
|
||||
|
||||
type NetstatCollector struct {
|
||||
metricCollector
|
||||
config NetstatCollectorConfig
|
||||
matches map[string]map[string]NetstatCollectorMetric
|
||||
devtags map[string]map[string]string
|
||||
matches map[string][]NetstatCollectorMetric
|
||||
lastTimestamp time.Time
|
||||
}
|
||||
|
||||
func (m *NetstatCollector) Init(config json.RawMessage) error {
|
||||
m.name = "NetstatCollector"
|
||||
m.parallel = true
|
||||
m.setup()
|
||||
m.lastTimestamp = time.Now()
|
||||
m.meta = map[string]string{"source": m.name, "group": "Network"}
|
||||
m.devtags = make(map[string]map[string]string)
|
||||
nameIndexMap := map[string]int{
|
||||
"net_bytes_in": 1,
|
||||
"net_pkts_in": 2,
|
||||
"net_bytes_out": 9,
|
||||
"net_pkts_out": 10,
|
||||
}
|
||||
m.matches = make(map[string]map[string]NetstatCollectorMetric)
|
||||
|
||||
const (
|
||||
fieldInterface = iota
|
||||
fieldReceiveBytes
|
||||
fieldReceivePackets
|
||||
fieldReceiveErrs
|
||||
fieldReceiveDrop
|
||||
fieldReceiveFifo
|
||||
fieldReceiveFrame
|
||||
fieldReceiveCompressed
|
||||
fieldReceiveMulticast
|
||||
fieldTransmitBytes
|
||||
fieldTransmitPackets
|
||||
fieldTransmitErrs
|
||||
fieldTransmitDrop
|
||||
fieldTransmitFifo
|
||||
fieldTransmitColls
|
||||
fieldTransmitCarrier
|
||||
fieldTransmitCompressed
|
||||
)
|
||||
|
||||
m.matches = make(map[string][]NetstatCollectorMetric)
|
||||
|
||||
// Set default configuration,
|
||||
m.config.SendAbsoluteValues = true
|
||||
m.config.SendDerivedValues = false
|
||||
// Read configuration file, allow overwriting default config
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &m.config)
|
||||
if err != nil {
|
||||
@@ -52,7 +76,9 @@ func (m *NetstatCollector) Init(config json.RawMessage) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
file, err := os.Open(string(NETSTATFILE))
|
||||
|
||||
// Check access to net statistic file
|
||||
file, err := os.Open(NETSTATFILE)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return err
|
||||
@@ -62,23 +88,65 @@ func (m *NetstatCollector) Init(config json.RawMessage) error {
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
l := scanner.Text()
|
||||
|
||||
// Skip lines with no net device entry
|
||||
if !strings.Contains(l, ":") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split line into fields
|
||||
f := strings.Fields(l)
|
||||
|
||||
// Get net device entry
|
||||
dev := strings.Trim(f[0], ": ")
|
||||
|
||||
// Check if device is a included device
|
||||
if _, ok := stringArrayContains(m.config.IncludeDevices, dev); ok {
|
||||
m.matches[dev] = make(map[string]NetstatCollectorMetric)
|
||||
for name, idx := range nameIndexMap {
|
||||
m.matches[dev][name] = NetstatCollectorMetric{
|
||||
index: idx,
|
||||
lastValue: 0,
|
||||
}
|
||||
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"}
|
||||
meta_unit_pkts_per_sec := map[string]string{"source": m.name, "group": "Network", "unit": "packets/sec"}
|
||||
|
||||
m.matches[dev] = []NetstatCollectorMetric{
|
||||
{
|
||||
name: "net_bytes_in",
|
||||
index: fieldReceiveBytes,
|
||||
lastValue: -1,
|
||||
tags: tags,
|
||||
meta: meta_unit_byte,
|
||||
meta_rates: meta_unit_byte_per_sec,
|
||||
},
|
||||
{
|
||||
name: "net_pkts_in",
|
||||
index: fieldReceivePackets,
|
||||
lastValue: -1,
|
||||
tags: tags,
|
||||
meta: meta_unit_pkts,
|
||||
meta_rates: meta_unit_pkts_per_sec,
|
||||
},
|
||||
{
|
||||
name: "net_bytes_out",
|
||||
index: fieldTransmitBytes,
|
||||
lastValue: -1,
|
||||
tags: tags,
|
||||
meta: meta_unit_byte,
|
||||
meta_rates: meta_unit_byte_per_sec,
|
||||
},
|
||||
{
|
||||
name: "net_pkts_out",
|
||||
index: fieldTransmitPackets,
|
||||
lastValue: -1,
|
||||
tags: tags,
|
||||
meta: meta_unit_pkts,
|
||||
meta_rates: meta_unit_pkts_per_sec,
|
||||
},
|
||||
}
|
||||
m.devtags[dev] = map[string]string{"device": dev, "type": "node"}
|
||||
}
|
||||
|
||||
}
|
||||
if len(m.devtags) == 0 {
|
||||
|
||||
if len(m.matches) == 0 {
|
||||
return errors.New("no devices to collector metrics found")
|
||||
}
|
||||
m.init = true
|
||||
@@ -89,50 +157,62 @@ func (m *NetstatCollector) Read(interval time.Duration, output chan lp.CCMetric)
|
||||
if !m.init {
|
||||
return
|
||||
}
|
||||
// Current time stamp
|
||||
now := time.Now()
|
||||
// time difference to last time stamp
|
||||
timeDiff := now.Sub(m.lastTimestamp).Seconds()
|
||||
// Save current timestamp
|
||||
m.lastTimestamp = now
|
||||
|
||||
file, err := os.Open(string(NETSTATFILE))
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
tdiff := now.Sub(m.lastTimestamp)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
l := scanner.Text()
|
||||
|
||||
// Skip lines with no net device entry
|
||||
if !strings.Contains(l, ":") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split line into fields
|
||||
f := strings.Fields(l)
|
||||
|
||||
// Get net device entry
|
||||
dev := strings.Trim(f[0], ":")
|
||||
|
||||
// Check if device is a included device
|
||||
if devmetrics, ok := m.matches[dev]; ok {
|
||||
for name, data := range devmetrics {
|
||||
v, err := strconv.ParseFloat(f[data.index], 64)
|
||||
if err == nil {
|
||||
vdiff := v - data.lastValue
|
||||
value := vdiff / tdiff.Seconds()
|
||||
if data.lastValue == 0 {
|
||||
value = 0
|
||||
}
|
||||
data.lastValue = v
|
||||
y, err := lp.New(name, m.devtags[dev], m.meta, map[string]interface{}{"value": value}, now)
|
||||
if err == nil {
|
||||
switch {
|
||||
case strings.Contains(name, "byte"):
|
||||
y.AddMeta("unit", "bytes/sec")
|
||||
case strings.Contains(name, "pkt"):
|
||||
y.AddMeta("unit", "packets/sec")
|
||||
}
|
||||
for i := range devmetrics {
|
||||
metric := &devmetrics[i]
|
||||
|
||||
// Read value
|
||||
v, err := strconv.ParseInt(f[metric.index], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
devmetrics[name] = data
|
||||
}
|
||||
if m.config.SendDerivedValues {
|
||||
if metric.lastValue >= 0 {
|
||||
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
|
||||
}
|
||||
}
|
||||
metric.lastValue = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.lastTimestamp = time.Now()
|
||||
}
|
||||
|
||||
func (m *NetstatCollector) Close() {
|
||||
|
@@ -5,17 +5,23 @@
|
||||
"netstat": {
|
||||
"include_devices": [
|
||||
"eth0"
|
||||
]
|
||||
],
|
||||
"send_abs_values" : true,
|
||||
"send_derived_values" : true
|
||||
}
|
||||
```
|
||||
|
||||
The `netstat` collector reads data from `/proc/net/dev` and outputs a handful **node** metrics. With the `include_devices` list you can specify which network devices should be measured. **Note**: Most other collectors use an _exclude_ list instead of an include list.
|
||||
|
||||
Metrics:
|
||||
* `net_bytes_in` (`unit=bytes/sec`)
|
||||
* `net_bytes_out` (`unit=bytes/sec`)
|
||||
* `net_pkts_in` (`unit=packets/sec`)
|
||||
* `net_pkts_out` (`unit=packets/sec`)
|
||||
* `net_bytes_in` (`unit=bytes`)
|
||||
* `net_bytes_out` (`unit=bytes`)
|
||||
* `net_pkts_in` (`unit=packets`)
|
||||
* `net_pkts_out` (`unit=packets`)
|
||||
* `net_bytes_in_bw` (`unit=bytes/sec` if `send_derived_values == true`)
|
||||
* `net_bytes_out_bw` (`unit=bytes/sec` if `send_derived_values == true`)
|
||||
* `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 `device`.
|
||||
|
||||
|
@@ -36,7 +36,7 @@ type nfsCollector struct {
|
||||
}
|
||||
|
||||
func (m *nfsCollector) initStats() error {
|
||||
cmd := exec.Command(m.config.Nfsstats, `-l`)
|
||||
cmd := exec.Command(m.config.Nfsstats, `-l`, `--all`)
|
||||
cmd.Wait()
|
||||
buffer, err := cmd.Output()
|
||||
if err == nil {
|
||||
@@ -52,7 +52,7 @@ func (m *nfsCollector) initStats() error {
|
||||
if err == nil {
|
||||
x := m.data[name]
|
||||
x.current = value
|
||||
x.last = 0
|
||||
x.last = value
|
||||
m.data[name] = x
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func (m *nfsCollector) initStats() error {
|
||||
}
|
||||
|
||||
func (m *nfsCollector) updateStats() error {
|
||||
cmd := exec.Command(m.config.Nfsstats, `-l`)
|
||||
cmd := exec.Command(m.config.Nfsstats, `-l`, `--all`)
|
||||
cmd.Wait()
|
||||
buffer, err := cmd.Output()
|
||||
if err == nil {
|
||||
@@ -114,6 +114,7 @@ func (m *nfsCollector) MainInit(config json.RawMessage) error {
|
||||
m.data = make(map[string]NfsCollectorData)
|
||||
m.initStats()
|
||||
m.init = true
|
||||
m.parallel = true
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -54,6 +54,7 @@ func (m *NUMAStatsCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
|
||||
m.name = "NUMAStatsCollector"
|
||||
m.parallel = true
|
||||
m.setup()
|
||||
m.meta = map[string]string{
|
||||
"source": m.name,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -3,38 +3,74 @@
|
||||
|
||||
```json
|
||||
"nvidia": {
|
||||
"exclude_devices" : [
|
||||
"0","1"
|
||||
"exclude_devices": [
|
||||
"0","1", "0000000:ff:01.0"
|
||||
],
|
||||
"exclude_metrics": [
|
||||
"nv_fb_memory",
|
||||
"nv_fb_mem_used",
|
||||
"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_mem_total`
|
||||
* `nv_fb_memory`
|
||||
* `nv_fb_mem_total`
|
||||
* `nv_fb_mem_used`
|
||||
* `nv_bar1_mem_total`
|
||||
* `nv_bar1_mem_used`
|
||||
* `nv_temp`
|
||||
* `nv_fan`
|
||||
* `nv_ecc_mode`
|
||||
* `nv_perf_state`
|
||||
* `nv_power_usage_report`
|
||||
* `nv_graphics_clock_report`
|
||||
* `nv_sm_clock_report`
|
||||
* `nv_mem_clock_report`
|
||||
* `nv_power_usage`
|
||||
* `nv_graphics_clock`
|
||||
* `nv_sm_clock`
|
||||
* `nv_mem_clock`
|
||||
* `nv_video_clock`
|
||||
* `nv_max_graphics_clock`
|
||||
* `nv_max_sm_clock`
|
||||
* `nv_max_mem_clock`
|
||||
* `nv_ecc_db_error`
|
||||
* `nv_ecc_sb_error`
|
||||
* `nv_power_man_limit`
|
||||
* `nv_max_video_clock`
|
||||
* `nv_ecc_uncorrected_error`
|
||||
* `nv_ecc_corrected_error`
|
||||
* `nv_power_max_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`
|
||||
|
||||
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>`
|
||||
|
||||
Some metrics add the additional sub type tag (`stype`) like the `nv_nvlink_*` metrics set `stype=nvlink,stype-id=<link_number>`.
|
319
collectors/rocmsmiMetric.go
Normal file
319
collectors/rocmsmiMetric.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/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
|
||||
}
|
47
collectors/rocmsmiMetric.md
Normal file
47
collectors/rocmsmiMetric.md
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
## `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>`.
|
101
collectors/sampleMetric.go
Normal file
101
collectors/sampleMetric.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
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
|
||||
type SampleCollectorConfig struct {
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
// This contains all variables we need during execution and the variables
|
||||
// defined by metricCollector (name, init, ...)
|
||||
type SampleCollector struct {
|
||||
metricCollector
|
||||
config SampleTimerCollectorConfig // the configuration structure
|
||||
meta map[string]string // default meta information
|
||||
tags map[string]string // 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 *SampleCollector) 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 = "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 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": "SAMPLE"}
|
||||
// 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)
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Set up everything that the collector requires during the Read() execution
|
||||
// Check files required, test execution of some commands, create data structure
|
||||
// for all topological entities (sockets, NUMA domains, ...)
|
||||
// Return some useful error message in case of any failures
|
||||
|
||||
// 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 *SampleCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
// Create a sample metric
|
||||
timestamp := time.Now()
|
||||
|
||||
value := 1.0
|
||||
// If you want to measure something for a specific amount of time, use interval
|
||||
// start := readState()
|
||||
// time.Sleep(interval)
|
||||
// stop := readState()
|
||||
// value = (stop - start) / interval.Seconds()
|
||||
|
||||
y, err := lp.New("sample_metric", m.tags, m.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil {
|
||||
// Send it to output channel
|
||||
output <- y
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Close metric collector: close network connection, close files, close libraries, ...
|
||||
// Called once by the collector manager
|
||||
func (m *SampleCollector) Close() {
|
||||
// Unset flag
|
||||
m.init = false
|
||||
}
|
122
collectors/sampleTimerMetric.go
Normal file
122
collectors/sampleTimerMetric.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package collectors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
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
|
||||
type SampleTimerCollectorConfig struct {
|
||||
Interval string `json:"interval"`
|
||||
}
|
||||
|
||||
// This contains all variables we need during execution and the variables
|
||||
// defined by metricCollector (name, init, ...)
|
||||
type SampleTimerCollector struct {
|
||||
metricCollector
|
||||
wg sync.WaitGroup // sync group for management
|
||||
done chan bool // channel for management
|
||||
meta map[string]string // default meta information
|
||||
tags map[string]string // default tags
|
||||
config SampleTimerCollectorConfig // the configuration structure
|
||||
interval time.Duration // the interval parsed from configuration
|
||||
ticker *time.Ticker // own timer
|
||||
output chan lp.CCMetric // own internal output channel
|
||||
}
|
||||
|
||||
func (m *SampleTimerCollector) Init(name string, config json.RawMessage) error {
|
||||
var err error = nil
|
||||
// Always set the name early in Init() to use it in cclog.Component* functions
|
||||
m.name = "SampleTimerCollector"
|
||||
// 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": "SAMPLE"}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
// Parse the read interval duration
|
||||
m.interval, err = time.ParseDuration(m.config.Interval)
|
||||
if err != nil {
|
||||
cclog.ComponentError(m.name, "Error parsing interval:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Storage for output channel
|
||||
m.output = nil
|
||||
// Mangement channel for the timer function.
|
||||
m.done = make(chan bool)
|
||||
// Create the own ticker
|
||||
m.ticker = time.NewTicker(m.interval)
|
||||
|
||||
// Start the timer loop with return functionality by sending 'true' to the done channel
|
||||
m.wg.Add(1)
|
||||
go func() {
|
||||
select {
|
||||
case <-m.done:
|
||||
// Exit the timer loop
|
||||
cclog.ComponentDebug(m.name, "Closing...")
|
||||
m.wg.Done()
|
||||
return
|
||||
case timestamp := <-m.ticker.C:
|
||||
// This is executed every timer tick but we have to wait until the first
|
||||
// Read() to get the output channel
|
||||
if m.output != nil {
|
||||
m.ReadMetrics(timestamp)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Set this flag only if everything is initialized properly, all required files exist, ...
|
||||
m.init = true
|
||||
return err
|
||||
}
|
||||
|
||||
// This function is called at each interval timer tick
|
||||
func (m *SampleTimerCollector) ReadMetrics(timestamp time.Time) {
|
||||
// Create a sample metric
|
||||
|
||||
value := 1.0
|
||||
|
||||
// If you want to measure something for a specific amout of time, use interval
|
||||
// start := readState()
|
||||
// time.Sleep(interval)
|
||||
// stop := readState()
|
||||
// value = (stop - start) / interval.Seconds()
|
||||
|
||||
y, err := lp.New("sample_metric", m.tags, m.meta, map[string]interface{}{"value": value}, timestamp)
|
||||
if err == nil && m.output != nil {
|
||||
// Send it to output channel if we have a valid channel
|
||||
m.output <- y
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SampleTimerCollector) Read(interval time.Duration, output chan lp.CCMetric) {
|
||||
// Capture output channel
|
||||
m.output = output
|
||||
}
|
||||
|
||||
func (m *SampleTimerCollector) Close() {
|
||||
// Send signal to the timer loop to stop it
|
||||
m.done <- true
|
||||
// Wait until the timer loop is done
|
||||
m.wg.Wait()
|
||||
// Unset flag
|
||||
m.init = false
|
||||
}
|
@@ -50,6 +50,7 @@ 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)
|
||||
@@ -70,10 +71,10 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
globPattern := filepath.Join("/sys/class/hwmon", "*", "temp*_input")
|
||||
inputFiles, err := filepath.Glob(globPattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to glob files with pattern '%s': %v", globPattern, err)
|
||||
return fmt.Errorf("unable to glob files with pattern '%s': %v", globPattern, err)
|
||||
}
|
||||
if inputFiles == nil {
|
||||
return fmt.Errorf("Unable to find any files with pattern '%s'", globPattern)
|
||||
return fmt.Errorf("unable to find any files with pattern '%s'", globPattern)
|
||||
}
|
||||
|
||||
// Get sensor name for each temperature sensor file
|
||||
@@ -116,6 +117,10 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
}
|
||||
|
||||
// Sensor file
|
||||
_, err = ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sensor.file = file
|
||||
|
||||
// Sensor tags
|
||||
@@ -158,7 +163,7 @@ func (m *TempCollector) Init(config json.RawMessage) error {
|
||||
|
||||
// Empty sensors map
|
||||
if len(m.sensors) == 0 {
|
||||
return fmt.Errorf("No temperature sensors found")
|
||||
return fmt.Errorf("no temperature sensors found")
|
||||
}
|
||||
|
||||
// Finished initialization
|
||||
|
@@ -28,6 +28,7 @@ type TopProcsCollector struct {
|
||||
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 {
|
||||
@@ -39,14 +40,14 @@ func (m *TopProcsCollector) Init(config json.RawMessage) error {
|
||||
m.config.Num_procs = int(DEFAULT_NUM_PROCS)
|
||||
}
|
||||
if m.config.Num_procs <= 0 || m.config.Num_procs > MAX_NUM_PROCS {
|
||||
return errors.New(fmt.Sprintf("num_procs option must be set in 'topprocs' config (range: 1-%d)", MAX_NUM_PROCS))
|
||||
return fmt.Errorf("num_procs option must be set in 'topprocs' config (range: 1-%d)", MAX_NUM_PROCS)
|
||||
}
|
||||
m.setup()
|
||||
command := exec.Command("ps", "-Ao", "comm", "--sort=-pcpu")
|
||||
command.Wait()
|
||||
_, err = command.Output()
|
||||
if err != nil {
|
||||
return errors.New("Failed to execute command")
|
||||
return errors.New("failed to execute command")
|
||||
}
|
||||
m.init = true
|
||||
return nil
|
||||
|
12
config.json
12
config.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"sinks": "sinks.json",
|
||||
"collectors" : "collectors.json",
|
||||
"receivers" : "receivers.json",
|
||||
"router" : "router.json",
|
||||
"interval": 10,
|
||||
"duration": 1
|
||||
"sinks": "./sinks.json",
|
||||
"collectors" : "./collectors.json",
|
||||
"receivers" : "./receivers.json",
|
||||
"router" : "./router.json",
|
||||
"interval": "10s",
|
||||
"duration": "1s"
|
||||
}
|
||||
|
189
docs/configuration.md
Normal file
189
docs/configuration.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Configuring the CC metric collector
|
||||
|
||||
The configuration of the CC metric collector consists of five configuration files: one global file and four component related files.
|
||||
|
||||
## Global configuration
|
||||
|
||||
The global file contains the paths to the other four files and some global options.
|
||||
|
||||
```json
|
||||
{
|
||||
"sinks": "sinks.json",
|
||||
"collectors" : "collectors.json",
|
||||
"receivers" : "receivers.json",
|
||||
"router" : "router.json",
|
||||
"interval": 10,
|
||||
"duration": 1
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that the paths are relative to the execution folder of the cc-metric-collector binary, so it is recommended to use absolute paths.
|
||||
|
||||
## Component configuration
|
||||
|
||||
The others are mainly list of of subcomponents: the collectors, the receivers, the router and the sinks. Their role is best shown in a picture:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
subgraph col ["Collectors"]
|
||||
direction TB
|
||||
cpustat["cpustat"]
|
||||
memstat["memstat"]
|
||||
tempstat["tempstat"]
|
||||
misc["..."]
|
||||
end
|
||||
|
||||
subgraph Receivers ["Receivers"]
|
||||
direction TB
|
||||
nats["NATS"]
|
||||
httprecv["HTTP"]
|
||||
miscrecv[...]
|
||||
end
|
||||
|
||||
subgraph calc["Aggregator"]
|
||||
direction LR
|
||||
cache["Cache"]
|
||||
agg["Calculator"]
|
||||
end
|
||||
|
||||
subgraph sinks ["Sinks"]
|
||||
direction RL
|
||||
influx["InfluxDB"]
|
||||
ganglia["Ganglia"]
|
||||
logger["Logfile"]
|
||||
miscsink["..."]
|
||||
end
|
||||
|
||||
cpustat --> CollectorManager["CollectorManager"]
|
||||
memstat --> CollectorManager
|
||||
tempstat --> CollectorManager
|
||||
misc --> CollectorManager
|
||||
|
||||
nats --> ReceiverManager["ReceiverManager"]
|
||||
httprecv --> ReceiverManager
|
||||
miscrecv --> ReceiverManager
|
||||
|
||||
CollectorManager --> newrouter["Router"]
|
||||
ReceiverManager -.-> newrouter
|
||||
calc -.-> newrouter
|
||||
newrouter --> SinkManager["SinkManager"]
|
||||
newrouter -.-> calc
|
||||
|
||||
SinkManager --> influx
|
||||
SinkManager --> ganglia
|
||||
SinkManager --> logger
|
||||
SinkManager --> miscsink
|
||||
|
||||
|
||||
```
|
||||
|
||||
There are four parts:
|
||||
- The collectors read data from files, execute commands and call dynamically loaded library function and send it to the router
|
||||
- The router can process metrics by cacheing and evaluating functions and conditions on them
|
||||
- The sinks send the metrics to storage backends
|
||||
- The receivers can be used to receive metrics from other collectors and forward them to the router. They can be used to create a tree-like structure of collectors.
|
||||
|
||||
(A maybe better differentiation between collectors and receivers is that the collectors are called periodically while the receivers have their own logic and submit metrics at any time)
|
||||
|
||||
|
||||
### Collectors configuration file
|
||||
|
||||
The collectors configuration file tells which metrics should be queried from the system. The metric gathering is logically grouped in so called 'Collectors'. So there are Collectors to read CPU, memory or filesystem statistics. The collectors configuration file is a list of these collectors with collector-specific configurations:
|
||||
|
||||
```json
|
||||
{
|
||||
"cpustat" : {},
|
||||
"diskstat": {
|
||||
"exclude_metrics": [
|
||||
"disk_total"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The first one is the CPU statistics collector without any collector-specific setting. The second one enables disk mount statistics but excludes the metric `disk_total`.
|
||||
|
||||
All names and possible collector-specific configuration options can be found [here](../collectors/README.md).
|
||||
|
||||
Some collectors might dynamically load shared libraries. In order to enable these collectors, make sure that the shared library path is part of the `LD_LIBRARY_PATH` environment variable.
|
||||
|
||||
### Sinks configuration file
|
||||
|
||||
The sinks define the output/sending of metrics. The metrics can be forwarded to multiple sinks, even to sinks of the same type. The sinks configuration file is a list of these sinks, each with an individual name.
|
||||
|
||||
```json
|
||||
{
|
||||
"myinflux" : {
|
||||
"type" : "influxasync",
|
||||
"host": "localhost",
|
||||
"port": "8086",
|
||||
"organization" : "testorga",
|
||||
"database" : "testbucket",
|
||||
"password" : "<my secret JWT>"
|
||||
},
|
||||
"companyinflux" : {
|
||||
"type" : "influxasync",
|
||||
"host": "companyhost",
|
||||
"port": "8086",
|
||||
"organization" : "company",
|
||||
"database" : "main",
|
||||
"password" : "<company's secret JWT>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The above example configuration file defines two sink, both ot type `influxasync`. They are differentiated internally by the names: `myinflux` and `companyinflux`.
|
||||
|
||||
All types and possible sink-specific configuration options can be found [here](../sinks/README.md).
|
||||
|
||||
Some sinks might dynamically load shared libraries. In order to enable these sinks, make sure that the shared library path is part of the `LD_LIBRARY_PATH` environment variable.
|
||||
|
||||
### Router configuration file
|
||||
|
||||
The collectors and the sinks are connected through the router. The router forwards the metrics to the sinks but enables some data processing. A common example is to tag all passing metrics like adding `cluster=mycluster`. But also aggregations like "take the average of all 'ipc' metrics" (ipc -> Instructions Per Cycle). Since the configurations of these aggregations can be quite complicated, we refer to the router's [README](../internal/metricRouter/README.md).
|
||||
|
||||
A simple router configuration file to start with looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"add_tags" : [
|
||||
{
|
||||
"key" : "cluster",
|
||||
"value" : "mycluster",
|
||||
"if" : "*"
|
||||
}
|
||||
],
|
||||
"interval_timestamp" : false,
|
||||
"num_cache_intervals" : 0
|
||||
}
|
||||
```
|
||||
|
||||
With the `add_tags` section, we tell to attach the `cluster=mycluster` tag to each (`*` metric). The `interval_timestamp` tell the router to not touch the timestamp of metrics. It is possible to send all metrics within an interval with a common time stamp to avoid later alignment issues. The `num_cache_intervals` diables the cache completely. The cache is only required if you want to do complex metric aggregations.
|
||||
|
||||
All configuration options can be found [here](../internal/metricRouter/README.md).
|
||||
|
||||
### Receivers configuration file
|
||||
|
||||
The receivers are a special feature of the CC Metric Collector to enable simpler integration into exising setups. While collectors query data from the local system, the receivers commonly get data from other systems through some network technology like HTTP or NATS. The idea is keep the current setup but send it to a CC Metric Collector which forwards it to the the destination system (if a sink exists for it). For most setups, the receivers are not required and an the receiver config file should contain only an empty JSON map (`{}`).
|
||||
|
||||
```json
|
||||
{
|
||||
"nats_rack0": {
|
||||
"type": "nats",
|
||||
"address" : "nats-server.example.org",
|
||||
"port" : "4222",
|
||||
"subject" : "rack0",
|
||||
},
|
||||
"nats_rack1": {
|
||||
"type": "nats",
|
||||
"address" : "nats-server.example.org",
|
||||
"port" : "4222",
|
||||
"subject" : "rack1",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This example configuration creates two receivers with the names `nats_rack0` and `nats_rack1`. While one subscribes to metrics published with the `rack0` subject, the other one subscribes to the `rack0` subject. The NATS server is the same as it manages all subjects in a subnet. (As example, the router could add tags `rack=0` and `rack=1` respectively to the received metrics.)
|
||||
|
||||
All types and possible receiver-specific configuration options can be found [here](../receivers/README.md).
|
42
go.mod
42
go.mod
@@ -1,19 +1,35 @@
|
||||
module github.com/ClusterCockpit/cc-metric-collector
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
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/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
|
||||
)
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/ClusterCockpit/cc-units v0.0.0-20220318130935-92a0c6442220
|
||||
github.com/NVIDIA/go-nvml v0.11.6-0
|
||||
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
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.8.1
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf
|
||||
github.com/nats-io/nats.go v1.14.0
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/stmcginnis/gofish v0.13.0
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.8.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/nats-io/nats-server/v2 v2.8.0 // indirect
|
||||
github.com/nats-io/nkeys v0.3.0 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
16
go.mod.1.16
Normal file
16
go.mod.1.16
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/ClusterCockpit/cc-metric-collector
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/NVIDIA/go-nvml v0.11.6-0
|
||||
github.com/PaesslerAG/gval v1.1.2
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.7.0
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf
|
||||
github.com/nats-io/nats-server/v2 v2.8.0 // indirect
|
||||
github.com/nats-io/nats.go v1.14.0
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/stmcginnis/gofish v0.13.0
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
|
||||
)
|
16
go.mod.1.17+
Normal file
16
go.mod.1.17+
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/ClusterCockpit/cc-metric-collector
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/NVIDIA/go-nvml v0.11.6-0
|
||||
github.com/PaesslerAG/gval v1.1.2
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.8.1
|
||||
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf
|
||||
github.com/nats-io/nats-server/v2 v2.8.0 // indirect
|
||||
github.com/nats-io/nats.go v1.14.0
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/stmcginnis/gofish v0.13.0
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad
|
||||
)
|
@@ -24,8 +24,8 @@ type ccMetric struct {
|
||||
|
||||
// ccMetric access functions
|
||||
type CCMetric interface {
|
||||
ToPoint(metaAsTags bool) *write.Point // Generate influxDB point for data type ccMetric
|
||||
ToLineProtocol(metaAsTags bool) string // Generate influxDB line protocol for data type ccMetric
|
||||
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
|
||||
|
||||
Name() string // Get metric name
|
||||
SetName(name string) // Set metric name
|
||||
@@ -61,25 +61,18 @@ func (m *ccMetric) String() string {
|
||||
}
|
||||
|
||||
// ToLineProtocol generates influxDB line protocol for data type ccMetric
|
||||
func (m *ccMetric) ToPoint(metaAsTags bool) (p *write.Point) {
|
||||
|
||||
if !metaAsTags {
|
||||
p = influxdb2.NewPoint(m.name, m.tags, m.fields, m.tm)
|
||||
} else {
|
||||
tags := make(map[string]string, len(m.tags)+len(m.meta))
|
||||
for key, value := range m.tags {
|
||||
tags[key] = value
|
||||
func (m *ccMetric) ToPoint(metaAsTags map[string]bool) (p *write.Point) {
|
||||
p = influxdb2.NewPoint(m.name, m.tags, m.fields, m.tm)
|
||||
for key, ok1 := range metaAsTags {
|
||||
if val, ok2 := m.GetMeta(key); ok1 && ok2 {
|
||||
p.AddTag(key, val)
|
||||
}
|
||||
for key, value := range m.meta {
|
||||
tags[key] = value
|
||||
}
|
||||
p = influxdb2.NewPoint(m.name, tags, m.fields, m.tm)
|
||||
}
|
||||
return
|
||||
return p
|
||||
}
|
||||
|
||||
// ToLineProtocol generates influxDB line protocol for data type ccMetric
|
||||
func (m *ccMetric) ToLineProtocol(metaAsTags bool) string {
|
||||
func (m *ccMetric) ToLineProtocol(metaAsTags map[string]bool) string {
|
||||
|
||||
return write.PointToLineProtocol(
|
||||
m.ToPoint(metaAsTags),
|
||||
|
@@ -29,6 +29,7 @@ func intArrayContains(array []int, str int) (int, bool) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// Used internally for sysfs file reads
|
||||
func fileToInt(path string) int {
|
||||
buffer, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -47,6 +48,7 @@ func fileToInt(path string) int {
|
||||
return int(id)
|
||||
}
|
||||
|
||||
// Get list of CPU socket IDs
|
||||
func SocketList() []int {
|
||||
buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
|
||||
if err != nil {
|
||||
@@ -54,7 +56,7 @@ func SocketList() []int {
|
||||
return nil
|
||||
}
|
||||
ll := strings.Split(string(buffer), "\n")
|
||||
var packs []int
|
||||
packs := make([]int, 0)
|
||||
for _, line := range ll {
|
||||
if strings.HasPrefix(line, "physical id") {
|
||||
lv := strings.Fields(line)
|
||||
@@ -72,7 +74,8 @@ func SocketList() []int {
|
||||
return packs
|
||||
}
|
||||
|
||||
func CpuList() []int {
|
||||
// Get list of hardware thread IDs in the order of listing in /proc/cpuinfo
|
||||
func HwthreadList() []int {
|
||||
buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
@@ -97,6 +100,13 @@ func CpuList() []int {
|
||||
return cpulist
|
||||
}
|
||||
|
||||
// Get list of hardware thread IDs in the order of listing in /proc/cpuinfo
|
||||
// Deprecated! Use HwthreadList()
|
||||
func CpuList() []int {
|
||||
return HwthreadList()
|
||||
}
|
||||
|
||||
// Get list of CPU core IDs in the order of listing in /proc/cpuinfo
|
||||
func CoreList() []int {
|
||||
buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
|
||||
if err != nil {
|
||||
@@ -122,6 +132,7 @@ func CoreList() []int {
|
||||
return corelist
|
||||
}
|
||||
|
||||
// Get list of NUMA node IDs
|
||||
func NumaNodeList() []int {
|
||||
numaList := make([]int, 0)
|
||||
globPath := filepath.Join(string(SYSFS_NUMABASE), "node*")
|
||||
@@ -156,8 +167,9 @@ func NumaNodeList() []int {
|
||||
return numaList
|
||||
}
|
||||
|
||||
// Get list of CPU die IDs
|
||||
func DieList() []int {
|
||||
cpulist := CpuList()
|
||||
cpulist := HwthreadList()
|
||||
dielist := make([]int, 0)
|
||||
for _, c := range cpulist {
|
||||
diepath := filepath.Join(string(SYSFS_CPUBASE), fmt.Sprintf("cpu%d", c), "topology/die_id")
|
||||
@@ -169,10 +181,33 @@ func DieList() []int {
|
||||
}
|
||||
}
|
||||
}
|
||||
return dielist
|
||||
if len(dielist) > 0 {
|
||||
return dielist
|
||||
}
|
||||
return SocketList()
|
||||
}
|
||||
|
||||
type CpuEntry struct {
|
||||
// Get 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{}
|
||||
}
|
||||
|
||||
// Structure holding all information about a hardware thread
|
||||
type HwthreadEntry struct {
|
||||
Cpuid int
|
||||
SMT int
|
||||
Core int
|
||||
@@ -181,25 +216,25 @@ type CpuEntry struct {
|
||||
Die int
|
||||
}
|
||||
|
||||
func CpuData() []CpuEntry {
|
||||
func CpuData() []HwthreadEntry {
|
||||
|
||||
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)
|
||||
}
|
||||
// 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))
|
||||
}
|
||||
@@ -257,11 +292,11 @@ func CpuData() []CpuEntry {
|
||||
return 0
|
||||
}
|
||||
|
||||
clist := make([]CpuEntry, 0)
|
||||
for _, c := range CpuList() {
|
||||
clist = append(clist, CpuEntry{Cpuid: c})
|
||||
clist := make([]HwthreadEntry, 0)
|
||||
for _, c := range HwthreadList() {
|
||||
clist = append(clist, HwthreadEntry{Cpuid: c})
|
||||
}
|
||||
for _, centry := range clist {
|
||||
for i, centry := range clist {
|
||||
centry.Socket = -1
|
||||
centry.Numadomain = -1
|
||||
centry.Die = -1
|
||||
@@ -289,10 +324,13 @@ func CpuData() []CpuEntry {
|
||||
// Lookup NUMA domain id
|
||||
centry.Numadomain = getNumaDomain(base)
|
||||
|
||||
// Update values in output list
|
||||
clist[i] = centry
|
||||
}
|
||||
return clist
|
||||
}
|
||||
|
||||
// Structure holding basic information about a CPU
|
||||
type CpuInformation struct {
|
||||
NumHWthreads int
|
||||
SMTWidth int
|
||||
@@ -302,6 +340,7 @@ type CpuInformation struct {
|
||||
NumNumaDomains int
|
||||
}
|
||||
|
||||
// Get basic information about the CPU
|
||||
func CpuInfo() CpuInformation {
|
||||
var c CpuInformation
|
||||
|
||||
@@ -337,7 +376,8 @@ func CpuInfo() CpuInformation {
|
||||
return c
|
||||
}
|
||||
|
||||
func GetCpuSocket(cpuid int) int {
|
||||
// Get the CPU socket ID for a given hardware thread ID
|
||||
func GetHwthreadSocket(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
@@ -347,7 +387,8 @@ func GetCpuSocket(cpuid int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetCpuNumaDomain(cpuid int) int {
|
||||
// Get the NUMA node ID for a given hardware thread ID
|
||||
func GetHwthreadNumaDomain(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
@@ -357,7 +398,8 @@ func GetCpuNumaDomain(cpuid int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetCpuDie(cpuid int) int {
|
||||
// Get the CPU die ID for a given hardware thread ID
|
||||
func GetHwthreadDie(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
@@ -367,7 +409,8 @@ func GetCpuDie(cpuid int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetCpuCore(cpuid int) int {
|
||||
// Get the CPU core ID for a given hardware thread ID
|
||||
func GetHwthreadCore(cpuid int) int {
|
||||
cdata := CpuData()
|
||||
for _, d := range cdata {
|
||||
if d.Cpuid == cpuid {
|
||||
@@ -377,7 +420,8 @@ func GetCpuCore(cpuid int) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func GetSocketCpus(socket int) []int {
|
||||
// Get the all hardware thread ID associated with a CPU socket
|
||||
func GetSocketHwthreads(socket int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
@@ -388,7 +432,8 @@ func GetSocketCpus(socket int) []int {
|
||||
return cpulist
|
||||
}
|
||||
|
||||
func GetNumaDomainCpus(domain int) []int {
|
||||
// Get the all hardware thread ID associated with a NUMA node
|
||||
func GetNumaDomainHwthreads(domain int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
@@ -399,7 +444,8 @@ func GetNumaDomainCpus(domain int) []int {
|
||||
return cpulist
|
||||
}
|
||||
|
||||
func GetDieCpus(die int) []int {
|
||||
// Get the all hardware thread ID associated with a CPU die
|
||||
func GetDieHwthreads(die int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
@@ -410,7 +456,8 @@ func GetDieCpus(die int) []int {
|
||||
return cpulist
|
||||
}
|
||||
|
||||
func GetCoreCpus(core int) []int {
|
||||
// Get the all hardware thread ID associated with a CPU core
|
||||
func GetCoreHwthreads(core int) []int {
|
||||
all := CpuData()
|
||||
cpulist := make([]int, 0)
|
||||
for _, d := range all {
|
||||
|
@@ -246,7 +246,7 @@ func matchfunc(args ...interface{}) (interface{}, error) {
|
||||
func getCpuCoreFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetCpuCore(cpuid), nil
|
||||
return topo.GetHwthreadCore(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuCore' accepts only an 'int' cpuid")
|
||||
}
|
||||
@@ -255,7 +255,7 @@ func getCpuCoreFunc(args ...interface{}) (interface{}, error) {
|
||||
func getCpuSocketFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetCpuSocket(cpuid), nil
|
||||
return topo.GetHwthreadSocket(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuCore' accepts only an 'int' cpuid")
|
||||
}
|
||||
@@ -264,7 +264,7 @@ func getCpuSocketFunc(args ...interface{}) (interface{}, error) {
|
||||
func getCpuNumaDomainFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetCpuNumaDomain(cpuid), nil
|
||||
return topo.GetHwthreadNumaDomain(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuNuma' accepts only an 'int' cpuid")
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func getCpuNumaDomainFunc(args ...interface{}) (interface{}, error) {
|
||||
func getCpuDieFunc(args ...interface{}) (interface{}, error) {
|
||||
switch cpuid := args[0].(type) {
|
||||
case int:
|
||||
return topo.GetCpuDie(cpuid), nil
|
||||
return topo.GetHwthreadDie(cpuid), nil
|
||||
}
|
||||
return -1, errors.New("function 'getCpuDie' accepts only an 'int' cpuid")
|
||||
}
|
||||
@@ -336,7 +336,7 @@ func getCpuListOfDieFunc(args ...interface{}) (interface{}, error) {
|
||||
|
||||
// wrapper function to get a list of all cpuids of the node
|
||||
func getCpuListOfNode(args ...interface{}) (interface{}, error) {
|
||||
return topo.CpuList(), nil
|
||||
return topo.HwthreadList(), nil
|
||||
}
|
||||
|
||||
// helper function to get the cpuid list for a CCMetric type tag set (type and type-id)
|
||||
@@ -348,14 +348,14 @@ func getCpuListOfType(args ...interface{}) (interface{}, error) {
|
||||
case string:
|
||||
switch typ {
|
||||
case "node":
|
||||
return topo.CpuList(), nil
|
||||
return topo.HwthreadList(), nil
|
||||
case "socket":
|
||||
return getCpuListOfSocketFunc(args[1])
|
||||
case "numadomain":
|
||||
return getCpuListOfNumaDomainFunc(args[1])
|
||||
case "core":
|
||||
return getCpuListOfCoreFunc(args[1])
|
||||
case "cpu":
|
||||
case "hwthread":
|
||||
var cpu int
|
||||
|
||||
switch id := args[1].(type) {
|
||||
|
@@ -8,6 +8,8 @@ The CCMetric router sits in between the collectors and the sinks and can be used
|
||||
{
|
||||
"num_cache_intervals" : 1,
|
||||
"interval_timestamp" : true,
|
||||
"hostname_tag" : "hostname",
|
||||
"max_forward" : 50,
|
||||
"add_tags" : [
|
||||
{
|
||||
"key" : "cluster",
|
||||
@@ -50,11 +52,30 @@ 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
There are three main options `add_tags`, `delete_tags` and `interval_timestamp`. `add_tags` and `delete_tags` are lists consisting of dicts with `key`, `value` and `if`. The `value` can be omitted in the `delete_tags` part as it only uses the `key` for removal. The `interval_timestamp` setting means that a unique timestamp is applied to all metrics traversing the router during an interval.
|
||||
|
||||
# Processing order in the router
|
||||
|
||||
- Add the `hostname_tag` tag (if sent by collectors or cache)
|
||||
- If `interval_timestamp == true`, change time of metrics
|
||||
- Check if metric should be dropped (`drop_metrics` and `drop_metrics_if`)
|
||||
- Add tags from `add_tags`
|
||||
- Delete tags from `del_tags`
|
||||
- Rename metric based on `rename_metrics` and store old name as `oldname` in meta information
|
||||
- Add tags from `add_tags` (if you used the new name in the `if` condition)
|
||||
- Delete tags from `del_tags` (if you used the new name in the `if` condition)
|
||||
- Send to sinks
|
||||
- Move to cache (if `num_cache_intervals > 0`)
|
||||
|
||||
# The `interval_timestamp` option
|
||||
|
||||
The collectors' `Read()` functions are not called simultaneously and therefore the metrics gathered in an interval can have different timestamps. If you want to avoid that and have a common timestamp (the beginning of the interval), set this option to `true` and the MetricRouter sets the time.
|
||||
@@ -65,6 +86,14 @@ If the MetricRouter should buffer metrics of intervals in a MetricCache, this op
|
||||
|
||||
A `num_cache_intervals > 0` is required to use the `interval_aggregates` option.
|
||||
|
||||
# The `hostname_tag` option
|
||||
|
||||
By default, the router tags metrics with the hostname for all locally created metrics. The default tag name is `hostname`, but it can be changed if your organization wants anything else
|
||||
|
||||
# The `max_forward` option
|
||||
|
||||
Every time the router receives a metric through any of the channels, it tries to directly read up to `max_forward` metrics from the same channel. This was done as the router thread would go to sleep and wake up with every arriving metric. The default are `50` metrics at once and `max_forward` needs to greater than `1`.
|
||||
|
||||
# The `rename_metrics` option
|
||||
|
||||
In the ClusterCockpit world we specified a set of standard metrics. Since some collectors determine the metric names based on files, execuables and libraries, they might change from system to system (or installation to installtion, OS to OS, ...). In order to get the common names, you can rename incoming metrics before sending them to the sink. If the metric name matches the `oldname`, it is changed to `newname`
|
||||
@@ -168,6 +197,14 @@ 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
|
||||
|
||||
@@ -215,3 +252,22 @@ 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
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
agg "github.com/ClusterCockpit/cc-metric-collector/internal/metricAggregator"
|
||||
mct "github.com/ClusterCockpit/cc-metric-collector/internal/multiChanTicker"
|
||||
units "github.com/ClusterCockpit/cc-units"
|
||||
)
|
||||
|
||||
const ROUTER_MAX_FORWARD = 50
|
||||
@@ -25,6 +26,7 @@ type metricRouterTagConfig struct {
|
||||
|
||||
// Metric router configuration
|
||||
type metricRouterConfig struct {
|
||||
HostnameTagName string `json:"hostname_tag"` // Key name used when adding the hostname to a metric (default 'hostname')
|
||||
AddTags []metricRouterTagConfig `json:"add_tags"` // List of tags that are added when the condition is met
|
||||
DelTags []metricRouterTagConfig `json:"delete_tags"` // List of tags that are removed when the condition is met
|
||||
IntervalAgg []agg.MetricAggregatorIntervalConfig `json:"interval_aggregates"` // List of aggregation function processed at the end of an interval
|
||||
@@ -33,6 +35,9 @@ type metricRouterConfig struct {
|
||||
RenameMetrics map[string]string `json:"rename_metrics"` // Map to rename metric name from key to value
|
||||
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
|
||||
}
|
||||
|
||||
@@ -46,7 +51,6 @@ type metricRouter struct {
|
||||
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
|
||||
@@ -76,7 +80,8 @@ func (r *metricRouter) Init(ticker mct.MultiChanTicker, wg *sync.WaitGroup, rout
|
||||
r.cache_input = make(chan lp.CCMetric)
|
||||
r.wg = wg
|
||||
r.ticker = ticker
|
||||
r.maxForward = ROUTER_MAX_FORWARD
|
||||
r.config.MaxForward = ROUTER_MAX_FORWARD
|
||||
r.config.HostnameTagName = "hostname"
|
||||
|
||||
// Set hostname
|
||||
hostname, err := os.Hostname()
|
||||
@@ -100,6 +105,10 @@ func (r *metricRouter) Init(ticker mct.MultiChanTicker, wg *sync.WaitGroup, rout
|
||||
cclog.ComponentError("MetricRouter", err.Error())
|
||||
return err
|
||||
}
|
||||
r.maxForward = 1
|
||||
if r.config.MaxForward > r.maxForward {
|
||||
r.maxForward = r.config.MaxForward
|
||||
}
|
||||
if r.config.NumCacheIntervals > 0 {
|
||||
r.cache, err = NewCache(r.cache_input, r.ticker, &r.cachewg, r.config.NumCacheIntervals)
|
||||
if err != nil {
|
||||
@@ -117,29 +126,6 @@ func (r *metricRouter) Init(ticker mct.MultiChanTicker, wg *sync.WaitGroup, rout
|
||||
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:
|
||||
r.timestamp = t
|
||||
}
|
||||
}
|
||||
}()
|
||||
cclog.ComponentDebug("MetricRouter", "TIMER START")
|
||||
}
|
||||
|
||||
func getParamMap(point lp.CCMetric) map[string]interface{} {
|
||||
params := make(map[string]interface{})
|
||||
params["metric"] = point
|
||||
@@ -224,12 +210,45 @@ 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.StartTimer()
|
||||
r.ticker.AddChannel(timeChan)
|
||||
}
|
||||
|
||||
// Router manager is done
|
||||
@@ -244,11 +263,15 @@ func (r *metricRouter) Start() {
|
||||
cclog.ComponentDebug("MetricRouter", "FORWARD", point)
|
||||
r.DoAddTags(point)
|
||||
r.DoDelTags(point)
|
||||
if new, ok := r.config.RenameMetrics[point.Name()]; ok {
|
||||
name := point.Name()
|
||||
if new, ok := r.config.RenameMetrics[name]; ok {
|
||||
point.SetName(new)
|
||||
point.AddMeta("oldname", name)
|
||||
r.DoAddTags(point)
|
||||
r.DoDelTags(point)
|
||||
}
|
||||
r.DoAddTags(point)
|
||||
r.DoDelTags(point)
|
||||
|
||||
r.prepareUnit(point)
|
||||
|
||||
for _, o := range r.outputs {
|
||||
o <- point
|
||||
@@ -258,7 +281,7 @@ func (r *metricRouter) Start() {
|
||||
// Foward message received from collector channel
|
||||
coll_forward := func(p lp.CCMetric) {
|
||||
// receive from metric collector
|
||||
p.AddTag("hostname", r.hostname)
|
||||
p.AddTag(r.config.HostnameTagName, r.hostname)
|
||||
if r.config.IntervalStamp {
|
||||
p.SetTime(r.timestamp)
|
||||
}
|
||||
@@ -287,7 +310,7 @@ func (r *metricRouter) Start() {
|
||||
cache_forward := func(p lp.CCMetric) {
|
||||
// receive from metric collector
|
||||
if !r.dropMetric(p) {
|
||||
p.AddTag("hostname", r.hostname)
|
||||
p.AddTag(r.config.HostnameTagName, r.hostname)
|
||||
forward(p)
|
||||
}
|
||||
}
|
||||
@@ -307,21 +330,25 @@ 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; i++ {
|
||||
for i := 0; len(r.coll_input) > 0 && i < (r.maxForward-1); i++ {
|
||||
coll_forward(<-r.coll_input)
|
||||
}
|
||||
|
||||
case p := <-r.recv_input:
|
||||
recv_forward(p)
|
||||
for i := 0; len(r.recv_input) > 0 && i < r.maxForward; i++ {
|
||||
for i := 0; len(r.recv_input) > 0 && i < (r.maxForward-1); i++ {
|
||||
recv_forward(<-r.recv_input)
|
||||
}
|
||||
|
||||
case p := <-r.cache_input:
|
||||
cache_forward(p)
|
||||
for i := 0; len(r.cache_input) > 0 && i < r.maxForward; i++ {
|
||||
for i := 0; len(r.cache_input) > 0 && i < (r.maxForward-1); i++ {
|
||||
cache_forward(<-r.cache_input)
|
||||
}
|
||||
}
|
||||
@@ -352,14 +379,6 @@ 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")
|
||||
|
@@ -4,5 +4,22 @@
|
||||
"address": "nats://my-url",
|
||||
"port" : "4222",
|
||||
"database": "testcluster"
|
||||
},
|
||||
"redfish_recv": {
|
||||
"type": "redfish",
|
||||
"client_config": [
|
||||
{
|
||||
"hostname": "my-host-1",
|
||||
"username": "username-1",
|
||||
"password": "password-1",
|
||||
"endpoint": "https://my-endpoint-1"
|
||||
},
|
||||
{
|
||||
"hostname": "my-host-2",
|
||||
"username": "username-2",
|
||||
"password": "password-2",
|
||||
"endpoint": "https://my-endpoint-2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -6,39 +6,24 @@ This folder contains the ReceiveManager and receiver implementations for the cc-
|
||||
|
||||
The configuration file for the receivers is a list of configurations. The `type` field in each specifies which receiver to initialize.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "nats",
|
||||
"address": "nats://my-url",
|
||||
"port" : "4222",
|
||||
"database": "testcluster"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Type `nats`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "nats",
|
||||
"address": "<nats-URI or hostname>",
|
||||
"port" : "<portnumber>",
|
||||
"database": "<subscribe topic>"
|
||||
"myreceivername" : {
|
||||
"type": "receiver-type",
|
||||
<receiver-specific configuration>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `nats` receiver subscribes to the topic `database` and listens on `address` and `port` for metrics in the InfluxDB line protocol.
|
||||
This allows to specify
|
||||
|
||||
## Available receivers
|
||||
|
||||
- [`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
|
||||
|
||||
# Contributing own receivers
|
||||
A receiver contains three functions and is derived from the type `Receiver` (in `metricReceiver.go`):
|
||||
* `Init(config ReceiverConfig) error`
|
||||
* `Start() error`
|
||||
* `Close()`
|
||||
* `Name() string`
|
||||
* `SetSink(sink chan ccMetric.CCMetric)`
|
||||
A receiver contains a few functions and is derived from the type `Receiver` (in `metricReceiver.go`):
|
||||
|
||||
The data structures should be set up in `Init()` like opening a file or server connection. The `Start()` function should either start a go routine or issue some other asynchronous mechanism for receiving metrics. The `Close()` function should tear down anything created in `Init()`.
|
||||
|
||||
Finally, the receiver needs to be registered in the `receiveManager.go`. There is a list of receivers called `AvailableReceivers` which is a map (`receiver_type_string` -> `pointer to Receiver interface`). Add a new entry with a descriptive name and the new receiver.
|
||||
For an example, check the [sample receiver](./sampleReceiver.go)
|
||||
|
118
receivers/httpReceiver.go
Normal file
118
receivers/httpReceiver.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
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"
|
||||
|
||||
type HttpReceiverConfig struct {
|
||||
Type string `json:"type"`
|
||||
Addr string `json:"address"`
|
||||
Port string `json:"port"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type HttpReceiver struct {
|
||||
receiver
|
||||
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)
|
||||
r.config.Port = HTTP_RECEIVER_PORT
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &r.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(r.config.Port) == 0 {
|
||||
return errors.New("not all configuration variables set required by HttpReceiver")
|
||||
}
|
||||
r.meta = map[string]string{"source": r.name}
|
||||
p := r.config.Path
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
p = "/" + p
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (r *HttpReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
err := r.server.ListenAndServe()
|
||||
if err != nil && err.Error() != "http: Server closed" {
|
||||
cclog.ComponentError(r.name, err.Error())
|
||||
}
|
||||
r.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *HttpReceiver) ServerHttp(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
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)
|
||||
}
|
||||
|
||||
for _, m := range metrics {
|
||||
y := lp.FromInfluxMetric(m)
|
||||
for k, v := range r.meta {
|
||||
y.AddMeta(k, v)
|
||||
}
|
||||
if r.sink != nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (r *HttpReceiver) Close() {
|
||||
r.server.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func NewHttpReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(HttpReceiver)
|
||||
err := r.Init(name, config)
|
||||
return r, err
|
||||
}
|
23
receivers/httpReceiver.md
Normal file
23
receivers/httpReceiver.md
Normal file
@@ -0,0 +1,23 @@
|
||||
## `http` receiver
|
||||
|
||||
The `http` receiver can be used receive metrics through HTTP POST requests.
|
||||
|
||||
### Configuration structure
|
||||
|
||||
```json
|
||||
{
|
||||
"<name>": {
|
||||
"type": "http",
|
||||
"address" : "",
|
||||
"port" : "8080",
|
||||
"path" : "/write"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `type`: makes the receiver a `http` receiver
|
||||
- `address`: Listen address
|
||||
- `port`: Listen port
|
||||
- `path`: URL path for the write endpoint
|
||||
|
||||
The HTTP endpoint listens to `http://<address>:<port>/<path>`
|
@@ -1,9 +1,6 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
// "time"
|
||||
"encoding/json"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
@@ -11,6 +8,7 @@ type defaultReceiverConfig struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// Receiver configuration: Listen address, port
|
||||
type ReceiverConfig struct {
|
||||
Addr string `json:"address"`
|
||||
Port string `json:"port"`
|
||||
@@ -20,23 +18,23 @@ type ReceiverConfig struct {
|
||||
}
|
||||
|
||||
type receiver struct {
|
||||
typename string
|
||||
name string
|
||||
sink chan lp.CCMetric
|
||||
name string
|
||||
sink chan lp.CCMetric
|
||||
}
|
||||
|
||||
type Receiver interface {
|
||||
Init(name string, config json.RawMessage) error
|
||||
Start()
|
||||
Close()
|
||||
Name() string
|
||||
SetSink(sink chan lp.CCMetric)
|
||||
Close() // Close / finish metric receiver
|
||||
Name() string // Name of the metric receiver
|
||||
SetSink(sink chan lp.CCMetric) // Set sink channel
|
||||
}
|
||||
|
||||
// Name returns the name of the metric receiver
|
||||
func (r *receiver) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
// SetSink set the sink channel
|
||||
func (r *receiver) SetSink(sink chan lp.CCMetric) {
|
||||
r.sink = sink
|
||||
}
|
||||
|
@@ -32,39 +32,6 @@ var DefaultTime = func() time.Time {
|
||||
return time.Unix(42, 0)
|
||||
}
|
||||
|
||||
func (r *NatsReceiver) Init(name string, config json.RawMessage) error {
|
||||
r.typename = "NatsReceiver"
|
||||
r.name = name
|
||||
r.config.Addr = nats.DefaultURL
|
||||
r.config.Port = "4222"
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &r.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(r.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(r.config.Addr) == 0 ||
|
||||
len(r.config.Port) == 0 ||
|
||||
len(r.config.Subject) == 0 {
|
||||
return errors.New("not all configuration variables set required by NatsReceiver")
|
||||
}
|
||||
r.meta = map[string]string{"source": r.name}
|
||||
uri := fmt.Sprintf("%s:%s", r.config.Addr, r.config.Port)
|
||||
cclog.ComponentDebug(r.name, "INIT", uri, "Subject", r.config.Subject)
|
||||
nc, err := nats.Connect(uri)
|
||||
if err == nil {
|
||||
r.nc = nc
|
||||
} else {
|
||||
r.nc = nil
|
||||
return err
|
||||
}
|
||||
r.handler = influx.NewMetricHandler()
|
||||
r.parser = influx.NewParser(r.handler)
|
||||
r.parser.SetTimeFunc(DefaultTime)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *NatsReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
r.nc.Subscribe(r.config.Subject, r._NatsReceive)
|
||||
@@ -91,3 +58,35 @@ func (r *NatsReceiver) Close() {
|
||||
r.nc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func NewNatsReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(NatsReceiver)
|
||||
r.name = fmt.Sprintf("NatsReceiver(%s)", name)
|
||||
r.config.Addr = nats.DefaultURL
|
||||
r.config.Port = "4222"
|
||||
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
|
||||
}
|
||||
}
|
||||
if len(r.config.Addr) == 0 ||
|
||||
len(r.config.Port) == 0 ||
|
||||
len(r.config.Subject) == 0 {
|
||||
return nil, errors.New("not all configuration variables set required by NatsReceiver")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
21
receivers/natsReceiver.md
Normal file
21
receivers/natsReceiver.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## `nats` receiver
|
||||
|
||||
The `nats` receiver can be used receive metrics from the NATS network. The `nats` receiver subscribes to the topic `database` and listens on `address` and `port` for metrics in the InfluxDB line protocol.
|
||||
|
||||
### Configuration structure
|
||||
|
||||
```json
|
||||
{
|
||||
"<name>": {
|
||||
"type": "nats",
|
||||
"address" : "nats-server.example.org",
|
||||
"port" : "4222",
|
||||
"subject" : "subject"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `type`: makes the receiver a `nats` receiver
|
||||
- `address`: Address of the NATS control server
|
||||
- `port`: Port of the NATS control server
|
||||
- `subject`: Subscribes to this subject and receive metrics
|
122
receivers/prometheusReceiver.go
Normal file
122
receivers/prometheusReceiver.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
type PrometheusReceiverConfig struct {
|
||||
defaultReceiverConfig
|
||||
Addr string `json:"address"`
|
||||
Port string `json:"port"`
|
||||
Path string `json:"path"`
|
||||
Interval string `json:"interval"`
|
||||
SSL bool `json:"ssl"`
|
||||
}
|
||||
|
||||
type PrometheusReceiver struct {
|
||||
receiver
|
||||
meta map[string]string
|
||||
config PrometheusReceiverConfig
|
||||
interval time.Duration
|
||||
done chan bool
|
||||
wg sync.WaitGroup
|
||||
ticker *time.Ticker
|
||||
uri string
|
||||
}
|
||||
|
||||
func (r *PrometheusReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START", r.uri)
|
||||
r.wg.Add(1)
|
||||
|
||||
r.ticker = time.NewTicker(r.interval)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-r.done:
|
||||
r.wg.Done()
|
||||
return
|
||||
case t := <-r.ticker.C:
|
||||
resp, err := http.Get(r.uri)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
lineSplit := strings.Fields(line)
|
||||
// separate metric name from tags (labels in Prometheus)
|
||||
tags := map[string]string{}
|
||||
name := lineSplit[0]
|
||||
if sindex := strings.Index(name, "{"); sindex >= 0 {
|
||||
eindex := strings.Index(name, "}")
|
||||
for _, kv := range strings.Split(name[sindex+1:eindex], ",") {
|
||||
eq := strings.Index(kv, "=")
|
||||
tags[kv[0:eq]] = strings.Trim(kv[eq+1:], "\"")
|
||||
}
|
||||
name = lineSplit[0][0:sindex]
|
||||
}
|
||||
value, err := strconv.ParseFloat(lineSplit[1], 64)
|
||||
if err == nil {
|
||||
y, err := lp.New(name, tags, r.meta, map[string]interface{}{"value": value}, t)
|
||||
if err == nil {
|
||||
r.sink <- y
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *PrometheusReceiver) Close() {
|
||||
cclog.ComponentDebug(r.name, "CLOSE")
|
||||
r.done <- true
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
func NewPrometheusReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(PrometheusReceiver)
|
||||
r.name = fmt.Sprintf("PrometheusReceiver(%s)", name)
|
||||
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
|
||||
}
|
||||
}
|
||||
if len(r.config.Addr) == 0 ||
|
||||
len(r.config.Port) == 0 ||
|
||||
len(r.config.Interval) == 0 {
|
||||
return nil, errors.New("not all configuration variables set required by PrometheusReceiver (address and port)")
|
||||
}
|
||||
if len(r.config.Interval) > 0 {
|
||||
t, err := time.ParseDuration(r.config.Interval)
|
||||
if err == nil {
|
||||
r.interval = t
|
||||
}
|
||||
}
|
||||
r.meta = map[string]string{"source": r.name}
|
||||
proto := "http"
|
||||
if r.config.SSL {
|
||||
proto = "https"
|
||||
}
|
||||
r.uri = fmt.Sprintf("%s://%s:%s/%s", proto, r.config.Addr, r.config.Port, r.config.Path)
|
||||
return r, nil
|
||||
}
|
27
receivers/prometheusReceiver.md
Normal file
27
receivers/prometheusReceiver.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## `prometheus` receiver
|
||||
|
||||
The `prometheus` receiver can be used to scrape the metrics of a single `prometheus` client. It does **not** use any official Golang library but making simple HTTP get requests and parse the response.
|
||||
|
||||
### Configuration structure
|
||||
|
||||
```json
|
||||
{
|
||||
"<name>": {
|
||||
"type": "prometheus",
|
||||
"address" : "testpromhost",
|
||||
"port" : "12345",
|
||||
"path" : "/prometheus",
|
||||
"interval": "5s",
|
||||
"ssl" : true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `type`: makes the receiver a `prometheus` receiver
|
||||
- `address`: Hostname or IP of the Prometheus agent
|
||||
- `port`: Port of Prometheus agent
|
||||
- `path`: Path to the Prometheus endpoint
|
||||
- `interval`: Scrape the Prometheus endpoint in this interval (default '5s')
|
||||
- `ssl`: Use SSL or not
|
||||
|
||||
The receiver requests data from `http(s)://<address>:<port>/<path>`.
|
@@ -9,15 +9,14 @@ import (
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
var AvailableReceivers = map[string]Receiver{
|
||||
"nats": &NatsReceiver{},
|
||||
var AvailableReceivers = map[string]func(name string, config json.RawMessage) (Receiver, error){
|
||||
"nats": NewNatsReceiver,
|
||||
"redfish": NewRedfishReceiver,
|
||||
}
|
||||
|
||||
type receiveManager struct {
|
||||
inputs []Receiver
|
||||
output chan lp.CCMetric
|
||||
done chan bool
|
||||
wg *sync.WaitGroup
|
||||
config []json.RawMessage
|
||||
}
|
||||
|
||||
@@ -30,11 +29,11 @@ type ReceiveManager interface {
|
||||
}
|
||||
|
||||
func (rm *receiveManager) Init(wg *sync.WaitGroup, receiverConfigFile string) error {
|
||||
// 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)
|
||||
if err != nil {
|
||||
cclog.ComponentError("ReceiveManager", err.Error())
|
||||
@@ -51,11 +50,12 @@ func (rm *receiveManager) Init(wg *sync.WaitGroup, receiverConfigFile string) er
|
||||
for name, raw := range rawConfigs {
|
||||
rm.AddInput(name, raw)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rm *receiveManager) Start() {
|
||||
rm.wg.Add(1)
|
||||
cclog.ComponentDebug("ReceiveManager", "START")
|
||||
|
||||
for _, r := range rm.inputs {
|
||||
cclog.ComponentDebug("ReceiveManager", "START", r.Name())
|
||||
@@ -75,10 +75,9 @@ func (rm *receiveManager) AddInput(name string, rawConfig json.RawMessage) error
|
||||
cclog.ComponentError("ReceiveManager", "SKIP", config.Type, "unknown receiver:", err.Error())
|
||||
return err
|
||||
}
|
||||
r := AvailableReceivers[config.Type]
|
||||
err = r.Init(name, rawConfig)
|
||||
r, err := AvailableReceivers[config.Type](name, rawConfig)
|
||||
if err != nil {
|
||||
cclog.ComponentError("ReceiveManager", "SKIP", r.Name(), "initialization failed:", err.Error())
|
||||
cclog.ComponentError("ReceiveManager", "SKIP", name, "initialization failed:", err.Error())
|
||||
return err
|
||||
}
|
||||
rm.inputs = append(rm.inputs, r)
|
||||
@@ -95,16 +94,19 @@ 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()
|
||||
}
|
||||
rm.wg.Done()
|
||||
cclog.ComponentDebug("ReceiveManager", "CLOSE")
|
||||
|
||||
cclog.ComponentDebug("ReceiveManager", "DONE")
|
||||
}
|
||||
|
||||
func New(wg *sync.WaitGroup, receiverConfigFile string) (ReceiveManager, error) {
|
||||
r := &receiveManager{}
|
||||
r := new(receiveManager)
|
||||
err := r.Init(wg, receiverConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
324
receivers/redfishReceiver.go
Normal file
324
receivers/redfishReceiver.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
|
||||
// See: https://pkg.go.dev/github.com/stmcginnis/gofish
|
||||
"github.com/stmcginnis/gofish"
|
||||
)
|
||||
|
||||
// RedfishReceiver configuration:
|
||||
type RedfishReceiver struct {
|
||||
receiver
|
||||
config struct {
|
||||
Type string `json:"type"`
|
||||
Fanout int `json:"fanout,omitempty"` // Default fanout: 64
|
||||
Interval int `json:"interval,omitempty"` // Default interval: 30s
|
||||
|
||||
// Client config for each redfish service
|
||||
ClientConfigs []struct {
|
||||
Hostname *string `json:"hostname"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
Endpoint *string `json:"endpoint"`
|
||||
Insecure *bool `json:"insecure,omitempty"`
|
||||
ExcludeMetrics []string `json:"exclude_metrics,omitempty"`
|
||||
gofish gofish.ClientConfig
|
||||
} `json:"client_config"`
|
||||
}
|
||||
|
||||
done chan bool // channel to finish / stop redfish receiver
|
||||
wg sync.WaitGroup // wait group for redfish receiver
|
||||
}
|
||||
|
||||
// Start starts the redfish receiver
|
||||
func (r *RedfishReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
|
||||
// readPowerMetric reads readfish power metric from the endpoint configured in conf
|
||||
readPowerMetric := func(clientConfigIndex int) error {
|
||||
|
||||
clientConfig := &r.config.ClientConfigs[clientConfigIndex]
|
||||
|
||||
// Connect to redfish service
|
||||
c, err := gofish.Connect(clientConfig.gofish)
|
||||
if err != nil {
|
||||
c := struct {
|
||||
Username string
|
||||
Endpoint string
|
||||
BasicAuth bool
|
||||
Insecure bool
|
||||
}{
|
||||
Username: clientConfig.gofish.Username,
|
||||
Endpoint: clientConfig.gofish.Endpoint,
|
||||
BasicAuth: clientConfig.gofish.BasicAuth,
|
||||
Insecure: clientConfig.gofish.Insecure,
|
||||
}
|
||||
return fmt.Errorf("readPowerMetric: gofish.Connect(%+v) failed: %v", c, err)
|
||||
}
|
||||
defer c.Logout()
|
||||
|
||||
// Get all chassis managed by this service
|
||||
chassis_list, err := c.Service.Chassis()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readPowerMetric: c.Service.Chassis() failed: %v", err)
|
||||
}
|
||||
|
||||
for _, chassis := range chassis_list {
|
||||
timestamp := time.Now()
|
||||
|
||||
// Get power information for each chassis
|
||||
power, err := chassis.Power()
|
||||
if err != nil {
|
||||
return fmt.Errorf("readPowerMetric: chassis.Power() failed: %v", err)
|
||||
}
|
||||
if power == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read min, max and average consumed watts for each power control
|
||||
for _, pc := range power.PowerControl {
|
||||
|
||||
// Map of collected metrics
|
||||
metrics := map[string]float32{
|
||||
// PowerConsumedWatts shall represent the actual power being consumed (in
|
||||
// Watts) by the chassis
|
||||
"consumed_watts": pc.PowerConsumedWatts,
|
||||
// AverageConsumedWatts shall represent the
|
||||
// average power level that occurred averaged over the last IntervalInMin
|
||||
// minutes.
|
||||
"average_consumed_watts": pc.PowerMetrics.AverageConsumedWatts,
|
||||
// MinConsumedWatts shall represent the
|
||||
// minimum power level in watts that occurred within the last
|
||||
// IntervalInMin minutes.
|
||||
"min_consumed_watts": pc.PowerMetrics.MinConsumedWatts,
|
||||
// MaxConsumedWatts shall represent the
|
||||
// maximum power level in watts that occurred within the last
|
||||
// IntervalInMin minutes
|
||||
"max_consumed_watts": pc.PowerMetrics.MaxConsumedWatts,
|
||||
}
|
||||
intervalInMin := strconv.FormatFloat(float64(pc.PowerMetrics.IntervalInMin), 'f', -1, 32)
|
||||
|
||||
// Metrics to exclude
|
||||
for _, key := range clientConfig.ExcludeMetrics {
|
||||
delete(metrics, key)
|
||||
}
|
||||
|
||||
// Set tags
|
||||
tags := map[string]string{
|
||||
"hostname": *clientConfig.Hostname,
|
||||
"type": "node",
|
||||
// ID uniquely identifies the resource
|
||||
"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.
|
||||
"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.
|
||||
"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
|
||||
}
|
||||
|
||||
// doReadPowerMetric read power metrics for all configure redfish services.
|
||||
// To compensate latencies of the Redfish services a fanout is used.
|
||||
doReadPowerMetric := func() {
|
||||
|
||||
// Compute fanout to use
|
||||
realFanout := r.config.Fanout
|
||||
if len(r.config.ClientConfigs) < realFanout {
|
||||
realFanout = len(r.config.ClientConfigs)
|
||||
}
|
||||
|
||||
// Create wait group and input channel for workers
|
||||
var workerWaitGroup sync.WaitGroup
|
||||
workerInput := make(chan int, realFanout)
|
||||
|
||||
// Create worker go routines
|
||||
for i := 0; i < realFanout; 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 clientConfigIndex := range workerInput {
|
||||
err := readPowerMetric(clientConfigIndex)
|
||||
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 <- 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 redfish receiver
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
|
||||
// Create ticker
|
||||
ticker := time.NewTicker(time.Duration(r.config.Interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
doReadPowerMetric()
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// process ticker event -> continue
|
||||
continue
|
||||
case <-r.done:
|
||||
// process done event
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cclog.ComponentDebug(r.name, "STARTED")
|
||||
}
|
||||
|
||||
// Close 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")
|
||||
}
|
||||
|
||||
// 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 NewRedfishReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(RedfishReceiver)
|
||||
|
||||
// Set name
|
||||
r.name = fmt.Sprintf("RedfishReceiver(%s)", name)
|
||||
|
||||
// Create done channel
|
||||
r.done = make(chan bool)
|
||||
|
||||
// Set defaults in r.config
|
||||
// Allow overwriting these defaults by reading config JSON
|
||||
r.config.Fanout = 64
|
||||
r.config.Interval = 30
|
||||
|
||||
// Read the redfish 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
|
||||
}
|
||||
}
|
||||
|
||||
// Create gofish client config
|
||||
for i := range r.config.ClientConfigs {
|
||||
clientConfig := &r.config.ClientConfigs[i]
|
||||
gofishConfig := &clientConfig.gofish
|
||||
|
||||
if clientConfig.Hostname == nil {
|
||||
err := fmt.Errorf("client config number %v requires hostname", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if clientConfig.Endpoint == nil {
|
||||
err := fmt.Errorf("client config number %v requires endpoint", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
gofishConfig.Endpoint = *clientConfig.Endpoint
|
||||
|
||||
if clientConfig.Username == nil {
|
||||
err := fmt.Errorf("client config number %v requires username", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
gofishConfig.Username = *clientConfig.Username
|
||||
|
||||
if clientConfig.Password == nil {
|
||||
err := fmt.Errorf("client config number %v requires password", i)
|
||||
cclog.ComponentError(r.name, err)
|
||||
return nil, err
|
||||
}
|
||||
gofishConfig.Password = *clientConfig.Password
|
||||
|
||||
gofishConfig.Insecure = true
|
||||
if clientConfig.Insecure != nil {
|
||||
gofishConfig.Insecure = *clientConfig.Insecure
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
101
receivers/sampleReceiver.go
Normal file
101
receivers/sampleReceiver.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package receivers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
)
|
||||
|
||||
// SampleReceiver configuration: receiver type, listen address, port
|
||||
type SampleReceiverConfig struct {
|
||||
Type string `json:"type"`
|
||||
Addr string `json:"address"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
type SampleReceiver struct {
|
||||
receiver
|
||||
config SampleReceiverConfig
|
||||
|
||||
// Storage for static information
|
||||
meta map[string]string
|
||||
// Use in case of own go routine
|
||||
// done chan bool
|
||||
// wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// Implement functions required for Receiver interface
|
||||
// Start(), Close()
|
||||
// See: metricReceiver.go
|
||||
|
||||
func (r *SampleReceiver) Start() {
|
||||
cclog.ComponentDebug(r.name, "START")
|
||||
|
||||
// Start server process like http.ListenAndServe()
|
||||
|
||||
// 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
|
||||
// }
|
||||
// }
|
||||
// }()
|
||||
}
|
||||
|
||||
// Close receiver: close network connection, close files, close libraries, ...
|
||||
func (r *SampleReceiver) Close() {
|
||||
cclog.ComponentDebug(r.name, "CLOSE")
|
||||
|
||||
// Close server like http.Shutdown()
|
||||
|
||||
// in case of own go routine, send the signal and wait
|
||||
// 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 NewSampleReceiver(name string, config json.RawMessage) (Receiver, error) {
|
||||
r := new(SampleReceiver)
|
||||
|
||||
// Set name of SampleReceiver
|
||||
// The name should be chosen in such a way that different instances of SampleReceiver can be distinguished
|
||||
r.name = fmt.Sprintf("SampleReceiver(%s)", name)
|
||||
|
||||
// Set static information
|
||||
r.meta = map[string]string{"source": r.name}
|
||||
|
||||
// Set defaults in r.config
|
||||
// Allow overwriting these defaults by reading config JSON
|
||||
|
||||
// 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
|
||||
// Use 'if len(r.config.Option) > 0' for strings
|
||||
|
||||
return r, nil
|
||||
}
|
@@ -15,3 +15,9 @@ 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
|
||||
|
12
scripts/cc-metric-collector.deb.control
Normal file
12
scripts/cc-metric-collector.deb.control
Normal file
@@ -0,0 +1,12 @@
|
||||
Package: cc-metric-collector
|
||||
Version: {VERSION}
|
||||
Installed-Size: {INSTALLED_SIZE}
|
||||
Architecture: {ARCH}
|
||||
Maintainer: thomas.gruber@fau.de
|
||||
Depends: libc6 (>= 2.2.1)
|
||||
Build-Depends: debhelper-compat (= 13), git, golang-go
|
||||
Description: Metric collection daemon from the ClusterCockpit suite
|
||||
Homepage: https://github.com/ClusterCockpit/cc-metric-collector
|
||||
Source: cc-metric-collector
|
||||
Rules-Requires-Root: no
|
||||
|
@@ -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 -- $DAEMON_OPTS
|
||||
start-stop-daemon --start -b --chdir "$WORK_DIR" --user "$CC_USER" -c "$CC_USER" --pidfile "$PID_FILE" --exec $DAEMON -- $CC_OPTS
|
||||
return=$?
|
||||
if [ $return -eq 0 ]
|
||||
then
|
||||
|
@@ -1,5 +1,5 @@
|
||||
Name: cc-metric-collector
|
||||
Version: 0.2
|
||||
Version: %{VERS}
|
||||
Release: 1%{?dist}
|
||||
Summary: Metric collection daemon from the ClusterCockpit suite
|
||||
|
||||
@@ -7,8 +7,9 @@ License: MIT
|
||||
Source0: %{name}-%{version}.tar.gz
|
||||
|
||||
BuildRequires: go-toolset
|
||||
# for internal LIKWID installation
|
||||
BuildRequires: wget perl-Data-Dumper
|
||||
BuildRequires: systemd-rpm-macros
|
||||
# for header downloads
|
||||
BuildRequires: wget
|
||||
|
||||
Provides: %{name} = %{version}
|
||||
|
||||
@@ -34,11 +35,15 @@ install -Dpm 0600 receivers.json %{buildroot}%{_sysconfdir}/%{name}/receivers.js
|
||||
install -Dpm 0600 router.json %{buildroot}%{_sysconfdir}/%{name}/router.json
|
||||
install -Dpm 0644 scripts/%{name}.service %{buildroot}%{_unitdir}/%{name}.service
|
||||
install -Dpm 0600 scripts/%{name}.config %{buildroot}%{_sysconfdir}/default/%{name}
|
||||
install -Dpm 0644 scripts/%{name}.sysusers %{buildroot}%{_sysusersdir}/%{name}.conf
|
||||
|
||||
|
||||
%check
|
||||
# go test should be here... :)
|
||||
|
||||
%pre
|
||||
%sysusers_create_package scripts/%{name}.sysusers
|
||||
|
||||
%post
|
||||
%systemd_post %{name}.service
|
||||
|
||||
@@ -46,17 +51,23 @@ install -Dpm 0600 scripts/%{name}.config %{buildroot}%{_sysconfdir}/default/%{na
|
||||
%systemd_preun %{name}.service
|
||||
|
||||
%files
|
||||
# Binary
|
||||
%attr(-,clustercockpit,clustercockpit) %{_sbindir}/%{name}
|
||||
# Config
|
||||
%dir %{_sysconfdir}/%{name}
|
||||
%{_sbindir}/%{name}
|
||||
%attr(0600,clustercockpit,clustercockpit) %config(noreplace) %{_sysconfdir}/%{name}/%{name}.json
|
||||
%attr(0600,clustercockpit,clustercockpit) %config(noreplace) %{_sysconfdir}/%{name}/collectors.json
|
||||
%attr(0600,clustercockpit,clustercockpit) %config(noreplace) %{_sysconfdir}/%{name}/sinks.json
|
||||
%attr(0600,clustercockpit,clustercockpit) %config(noreplace) %{_sysconfdir}/%{name}/receivers.json
|
||||
%attr(0600,clustercockpit,clustercockpit) %config(noreplace) %{_sysconfdir}/%{name}/router.json
|
||||
# Systemd
|
||||
%{_unitdir}/%{name}.service
|
||||
%{_sysconfdir}/default/%{name}
|
||||
%attr(0600,root,root) %config(noreplace) %{_sysconfdir}/%{name}/%{name}.json
|
||||
%attr(0600,root,root) %config(noreplace) %{_sysconfdir}/%{name}/collectors.json
|
||||
%attr(0600,root,root) %config(noreplace) %{_sysconfdir}/%{name}/sinks.json
|
||||
%attr(0600,root,root) %config(noreplace) %{_sysconfdir}/%{name}/receivers.json
|
||||
%attr(0600,root,root) %config(noreplace) %{_sysconfdir}/%{name}/router.json
|
||||
%{_sysusersdir}/%{name}.conf
|
||||
|
||||
%changelog
|
||||
* Thu Mar 03 2022 Thomas Gruber - 0.3
|
||||
- Add clustercockpit user installation
|
||||
* Mon Feb 14 2022 Thomas Gruber - 0.2
|
||||
- Add component specific configuration files
|
||||
- Add %attr to config files
|
||||
|
2
scripts/cc-metric-collector.sysusers
Normal file
2
scripts/cc-metric-collector.sysusers
Normal file
@@ -0,0 +1,2 @@
|
||||
#Type Name ID GECOS Home directory Shell
|
||||
u clustercockpit - "User for ClusterCockpit" /run/cc-metric-collector /sbin/nologin
|
10
sinks.json
10
sinks.json
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"mystdout" : {
|
||||
"type" : "stdout",
|
||||
"meta_as_tags" : true
|
||||
"mystdout": {
|
||||
"type": "stdout",
|
||||
"meta_as_tags": [
|
||||
"unit"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
|
||||
all: libganglia.so
|
||||
|
||||
libganglia.so:
|
||||
@find /usr ! -readable -prune -o -type d ! -executable -prune -o -name "$@*" -print0 | \
|
||||
xargs --null --no-run-if-empty --replace \
|
||||
ln --symbolic --verbose --force '{}' "$@"
|
||||
|
||||
clean:
|
||||
rm -f libganglia.so
|
||||
|
||||
.PHONY: clean
|
@@ -6,9 +6,11 @@ This folder contains the SinkManager and sink implementations for the cc-metric-
|
||||
- [`stdout`](./stdoutSink.md): Print all metrics to `stdout`, `stderr` or a file
|
||||
- [`http`](./httpSink.md): Send metrics to an HTTP server as POST requests
|
||||
- [`influxdb`](./influxSink.md): Send metrics to an [InfluxDB](https://www.influxdata.com/products/influxdb/) database
|
||||
- [`influxasync`](./influxAsyncSink.md): Send metrics to an [InfluxDB](https://www.influxdata.com/products/influxdb/) database with non-blocking write API
|
||||
- [`nats`](./natsSink.md): Publish metrics to the [NATS](https://nats.io/) network overlay system
|
||||
- [`ganglia`](./gangliaSink.md): Publish metrics in the [Ganglia Monitoring System](http://ganglia.info/) using the `gmetric` CLI tool
|
||||
- [`libganglia`](./libgangliaSink.md): Publish metrics in the [Ganglia Monitoring System](http://ganglia.info/) directly using `libganglia.so`
|
||||
- [`prometeus`](./prometheusSink.md): Publish metrics for the [Prometheus Monitoring System](https://prometheus.io/)
|
||||
|
||||
# Configuration
|
||||
|
||||
@@ -34,11 +36,12 @@ The configuration file for the sinks is a list of configurations. The `type` fie
|
||||
|
||||
|
||||
# Contributing own sinks
|
||||
A sink contains four functions and is derived from the type `sink`:
|
||||
* `Init(config json.RawMessage) error`
|
||||
A sink contains five functions and is derived from the type `sink`:
|
||||
* `Init(name string, config json.RawMessage) error`
|
||||
* `Write(point CCMetric) error`
|
||||
* `Flush() error`
|
||||
* `Close()`
|
||||
* `New<Typename>(name string, config json.RawMessage) (Sink, error)` (calls the `Init()` function)
|
||||
|
||||
The data structures should be set up in `Init()` like opening a file or server connection. The `Write()` function writes/sends the data. For non-blocking sinks, the `Flush()` method tells the sink to drain its internal buffers. The `Close()` function should tear down anything created in `Init()`.
|
||||
|
||||
@@ -65,8 +68,8 @@ type SampleSink struct {
|
||||
}
|
||||
|
||||
// Initialize the sink by giving it a name and reading in the config JSON
|
||||
func (s *SampleSink) Init(config json.RawMessage) error {
|
||||
s.name = "SampleSink" // Always specify a name here
|
||||
func (s *SampleSink) Init(name string, config json.RawMessage) error {
|
||||
s.name = fmt.Sprintf("SampleSink(%s)", name) // Always specify a name here
|
||||
// Read in the config JSON
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
@@ -91,4 +94,13 @@ func (s *SampleSink) Flush() error {
|
||||
|
||||
// Close sink: close network connection, close files, close libraries, ...
|
||||
func (s *SampleSink) Close() {}
|
||||
|
||||
|
||||
// New function to create a new instance of the sink
|
||||
func NewSampleSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(SampleSink)
|
||||
err := s.Init(name, config)
|
||||
return s, err
|
||||
}
|
||||
|
||||
```
|
@@ -1,6 +1,7 @@
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
@@ -23,11 +24,8 @@ func GangliaMetricName(point lp.CCMetric) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func GangliaMetricRename(point lp.CCMetric) string {
|
||||
name := point.Name()
|
||||
if name == "mem_total" || name == "swap_total" {
|
||||
return name
|
||||
} else if name == "net_bytes_in" {
|
||||
func GangliaMetricRename(name string) string {
|
||||
if name == "net_bytes_in" {
|
||||
return "bytes_in"
|
||||
} else if name == "net_bytes_out" {
|
||||
return "bytes_out"
|
||||
@@ -48,3 +46,224 @@ func GangliaSlopeType(point lp.CCMetric) uint {
|
||||
}
|
||||
return 3
|
||||
}
|
||||
|
||||
const DEFAULT_GANGLIA_METRIC_TMAX = 300
|
||||
const DEFAULT_GANGLIA_METRIC_SLOPE = "both"
|
||||
|
||||
type GangliaMetric struct {
|
||||
Name string
|
||||
Type string
|
||||
Slope string
|
||||
Tmax int
|
||||
Unit string
|
||||
}
|
||||
|
||||
type GangliaMetricGroup struct {
|
||||
Name string
|
||||
Metrics []GangliaMetric
|
||||
}
|
||||
|
||||
var CommonGangliaMetrics = []GangliaMetricGroup{
|
||||
{
|
||||
Name: "memory",
|
||||
Metrics: []GangliaMetric{
|
||||
{"mem_total", "float", "zero", 1200, "KB"},
|
||||
{"swap_total", "float", "zero", 1200, "KB"},
|
||||
{"mem_free", "float", "both", 180, "KB"},
|
||||
{"mem_shared", "float", "both", 180, "KB"},
|
||||
{"mem_buffers", "float", "both", 180, "KB"},
|
||||
{"mem_cached", "float", "both", 180, "KB"},
|
||||
{"swap_free", "float", "both", 180, "KB"},
|
||||
{"mem_sreclaimable", "float", "both", 180, "KB"},
|
||||
{"mem_slab", "float", "both", 180, "KB"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "cpu",
|
||||
Metrics: []GangliaMetric{
|
||||
{"cpu_num", "uint32", "zero", 1200, "CPUs"},
|
||||
{"cpu_speed", "uint32", "zero", 1200, "MHz"},
|
||||
{"cpu_user", "float", "both", 90, "%"},
|
||||
{"cpu_nice", "float", "both", 90, "%"},
|
||||
{"cpu_system", "float", "both", 90, "%"},
|
||||
{"cpu_idle", "float", "both", 3800, "%"},
|
||||
{"cpu_aidle", "float", "both", 90, "%"},
|
||||
{"cpu_wio", "float", "both", 90, "%"},
|
||||
{"cpu_intr", "float", "both", 90, "%"},
|
||||
{"cpu_sintr", "float", "both", 90, "%"},
|
||||
{"cpu_steal", "float", "both", 90, "%"},
|
||||
{"cpu_guest", "float", "both", 90, "%"},
|
||||
{"cpu_gnice", "float", "both", 90, "%"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "load",
|
||||
Metrics: []GangliaMetric{
|
||||
{"load_one", "float", "both", 70, ""},
|
||||
{"load_five", "float", "both", 325, ""},
|
||||
{"load_fifteen", "float", "both", 950, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "disk",
|
||||
Metrics: []GangliaMetric{
|
||||
{"disk_total", "double", "both", 1200, "GB"},
|
||||
{"disk_free", "double", "both", 180, "GB"},
|
||||
{"part_max_used", "float", "both", 180, "%"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "network",
|
||||
Metrics: []GangliaMetric{
|
||||
{"bytes_out", "float", "both", 300, "bytes/sec"},
|
||||
{"bytes_in", "float", "both", 300, "bytes/sec"},
|
||||
{"pkts_in", "float", "both", 300, "packets/sec"},
|
||||
{"pkts_out", "float", "both", 300, "packets/sec"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "process",
|
||||
Metrics: []GangliaMetric{
|
||||
{"proc_run", "uint32", "both", 950, ""},
|
||||
{"proc_total", "uint32", "both", 950, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "system",
|
||||
Metrics: []GangliaMetric{
|
||||
{"boottime", "uint32", "zero", 1200, "s"},
|
||||
{"sys_clock", "uint32", "zero", 1200, "s"},
|
||||
{"machine_type", "string", "zero", 1200, ""},
|
||||
{"os_name", "string", "zero", 1200, ""},
|
||||
{"os_release", "string", "zero", 1200, ""},
|
||||
{"mtu", "uint32", "both", 1200, ""},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type GangliaMetricConfig struct {
|
||||
Type string
|
||||
Slope string
|
||||
Tmax int
|
||||
Unit string
|
||||
Group string
|
||||
Value string
|
||||
Name string
|
||||
}
|
||||
|
||||
func GetCommonGangliaConfig(point lp.CCMetric) GangliaMetricConfig {
|
||||
mname := GangliaMetricRename(point.Name())
|
||||
if oldname, ok := point.GetMeta("oldname"); ok {
|
||||
mname = GangliaMetricRename(oldname)
|
||||
}
|
||||
for _, group := range CommonGangliaMetrics {
|
||||
for _, metric := range group.Metrics {
|
||||
if metric.Name == mname {
|
||||
valueStr := ""
|
||||
value, ok := point.GetField("value")
|
||||
if ok {
|
||||
switch real := value.(type) {
|
||||
case float64:
|
||||
valueStr = fmt.Sprintf("%f", real)
|
||||
case float32:
|
||||
valueStr = fmt.Sprintf("%f", real)
|
||||
case int64:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
case int32:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
case int:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
case uint64:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
case uint32:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
case uint:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
case string:
|
||||
valueStr = real
|
||||
default:
|
||||
}
|
||||
}
|
||||
return GangliaMetricConfig{
|
||||
Group: group.Name,
|
||||
Type: metric.Type,
|
||||
Slope: metric.Slope,
|
||||
Tmax: metric.Tmax,
|
||||
Unit: metric.Unit,
|
||||
Value: valueStr,
|
||||
Name: GangliaMetricRename(mname),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return GangliaMetricConfig{
|
||||
Group: "",
|
||||
Type: "",
|
||||
Slope: "",
|
||||
Tmax: 0,
|
||||
Unit: "",
|
||||
Value: "",
|
||||
Name: "",
|
||||
}
|
||||
}
|
||||
|
||||
func GetGangliaConfig(point lp.CCMetric) GangliaMetricConfig {
|
||||
mname := GangliaMetricRename(point.Name())
|
||||
if oldname, ok := point.GetMeta("oldname"); ok {
|
||||
mname = GangliaMetricRename(oldname)
|
||||
}
|
||||
group := ""
|
||||
if g, ok := point.GetMeta("group"); ok {
|
||||
group = g
|
||||
}
|
||||
unit := ""
|
||||
if u, ok := point.GetMeta("unit"); ok {
|
||||
unit = u
|
||||
}
|
||||
valueType := "double"
|
||||
valueStr := ""
|
||||
value, ok := point.GetField("value")
|
||||
if ok {
|
||||
switch real := value.(type) {
|
||||
case float64:
|
||||
valueStr = fmt.Sprintf("%f", real)
|
||||
valueType = "double"
|
||||
case float32:
|
||||
valueStr = fmt.Sprintf("%f", real)
|
||||
valueType = "float"
|
||||
case int64:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
valueType = "int32"
|
||||
case int32:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
valueType = "int32"
|
||||
case int:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
valueType = "int32"
|
||||
case uint64:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
valueType = "uint32"
|
||||
case uint32:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
valueType = "uint32"
|
||||
case uint:
|
||||
valueStr = fmt.Sprintf("%d", real)
|
||||
valueType = "uint32"
|
||||
case string:
|
||||
valueStr = real
|
||||
valueType = "string"
|
||||
default:
|
||||
valueType = "invalid"
|
||||
}
|
||||
}
|
||||
|
||||
return GangliaMetricConfig{
|
||||
Group: group,
|
||||
Type: valueType,
|
||||
Slope: DEFAULT_GANGLIA_METRIC_SLOPE,
|
||||
Tmax: DEFAULT_GANGLIA_METRIC_TMAX,
|
||||
Unit: unit,
|
||||
Value: valueStr,
|
||||
Name: GangliaMetricRename(mname),
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ type GangliaSinkConfig struct {
|
||||
AddTagsAsDesc bool `json:"add_tags_as_desc,omitempty"`
|
||||
ClusterName string `json:"cluster_name,omitempty"`
|
||||
AddTypeToName bool `json:"add_type_to_name,omitempty"`
|
||||
AddUnits bool `json:"add_units,omitempty"`
|
||||
}
|
||||
|
||||
type GangliaSink struct {
|
||||
@@ -33,16 +34,70 @@ type GangliaSink struct {
|
||||
config GangliaSinkConfig
|
||||
}
|
||||
|
||||
func (s *GangliaSink) Init(config json.RawMessage) error {
|
||||
func (s *GangliaSink) Write(point lp.CCMetric) error {
|
||||
var err error = nil
|
||||
s.name = "GangliaSink"
|
||||
//var tagsstr []string
|
||||
var argstr []string
|
||||
|
||||
// Get metric config (type, value, ... in suitable format)
|
||||
conf := GetCommonGangliaConfig(point)
|
||||
if len(conf.Type) == 0 {
|
||||
conf = GetGangliaConfig(point)
|
||||
}
|
||||
if len(conf.Type) == 0 {
|
||||
return fmt.Errorf("metric %q (Ganglia name %q) has no 'value' field", point.Name(), conf.Name)
|
||||
}
|
||||
|
||||
if s.config.AddGangliaGroup {
|
||||
argstr = append(argstr, fmt.Sprintf("--group=%s", conf.Group))
|
||||
}
|
||||
if s.config.AddUnits && len(conf.Unit) > 0 {
|
||||
argstr = append(argstr, fmt.Sprintf("--units=%s", conf.Unit))
|
||||
}
|
||||
|
||||
if len(s.config.ClusterName) > 0 {
|
||||
argstr = append(argstr, fmt.Sprintf("--cluster=%s", s.config.ClusterName))
|
||||
}
|
||||
// if s.config.AddTagsAsDesc && len(tagsstr) > 0 {
|
||||
// argstr = append(argstr, fmt.Sprintf("--desc=%q", strings.Join(tagsstr, ",")))
|
||||
// }
|
||||
if len(s.gmetric_config) > 0 {
|
||||
argstr = append(argstr, fmt.Sprintf("--conf=%s", s.gmetric_config))
|
||||
}
|
||||
if s.config.AddTypeToName {
|
||||
argstr = append(argstr, fmt.Sprintf("--name=%s", GangliaMetricName(point)))
|
||||
} else {
|
||||
argstr = append(argstr, fmt.Sprintf("--name=%s", conf.Name))
|
||||
}
|
||||
argstr = append(argstr, fmt.Sprintf("--slope=%s", conf.Slope))
|
||||
argstr = append(argstr, fmt.Sprintf("--value=%s", conf.Value))
|
||||
argstr = append(argstr, fmt.Sprintf("--type=%s", conf.Type))
|
||||
argstr = append(argstr, fmt.Sprintf("--tmax=%d", conf.Tmax))
|
||||
|
||||
cclog.ComponentDebug(s.name, s.gmetric_path, strings.Join(argstr, " "))
|
||||
command := exec.Command(s.gmetric_path, argstr...)
|
||||
command.Wait()
|
||||
_, err = command.Output()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *GangliaSink) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GangliaSink) Close() {
|
||||
}
|
||||
|
||||
func NewGangliaSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(GangliaSink)
|
||||
s.name = fmt.Sprintf("GangliaSink(%s)", name)
|
||||
s.config.AddTagsAsDesc = false
|
||||
s.config.AddGangliaGroup = false
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "Error reading config for", s.name, ":", err.Error())
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s.gmetric_path = ""
|
||||
@@ -60,98 +115,10 @@ func (s *GangliaSink) Init(config json.RawMessage) error {
|
||||
}
|
||||
}
|
||||
if len(s.gmetric_path) == 0 {
|
||||
err = errors.New("cannot find executable 'gmetric'")
|
||||
return nil, errors.New("cannot find executable 'gmetric'")
|
||||
}
|
||||
if len(s.config.GmetricConfig) > 0 {
|
||||
s.gmetric_config = s.config.GmetricConfig
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *GangliaSink) Write(point lp.CCMetric) error {
|
||||
var err error = nil
|
||||
var tagsstr []string
|
||||
var argstr []string
|
||||
if s.config.AddGangliaGroup {
|
||||
if point.HasTag("group") {
|
||||
g, _ := point.GetTag("group")
|
||||
argstr = append(argstr, fmt.Sprintf("--group=%s", g))
|
||||
} else if point.HasMeta("group") {
|
||||
g, _ := point.GetMeta("group")
|
||||
argstr = append(argstr, fmt.Sprintf("--group=%s", g))
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range point.Tags() {
|
||||
switch key {
|
||||
case "unit":
|
||||
argstr = append(argstr, fmt.Sprintf("--units=%s", value))
|
||||
default:
|
||||
tagsstr = append(tagsstr, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
}
|
||||
if s.config.MetaAsTags {
|
||||
for key, value := range point.Meta() {
|
||||
switch key {
|
||||
case "unit":
|
||||
argstr = append(argstr, fmt.Sprintf("--units=%s", value))
|
||||
default:
|
||||
tagsstr = append(tagsstr, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.config.ClusterName) > 0 {
|
||||
argstr = append(argstr, fmt.Sprintf("--cluster=%s", s.config.ClusterName))
|
||||
}
|
||||
if s.config.AddTagsAsDesc && len(tagsstr) > 0 {
|
||||
argstr = append(argstr, fmt.Sprintf("--desc=%q", strings.Join(tagsstr, ",")))
|
||||
}
|
||||
if len(s.gmetric_config) > 0 {
|
||||
argstr = append(argstr, fmt.Sprintf("--conf=%s", s.gmetric_config))
|
||||
}
|
||||
name := GangliaMetricRename(point)
|
||||
if s.config.AddTypeToName {
|
||||
argstr = append(argstr, fmt.Sprintf("--name=%s", GangliaMetricName(point)))
|
||||
} else {
|
||||
argstr = append(argstr, fmt.Sprintf("--name=%s", name))
|
||||
}
|
||||
slope := GangliaSlopeType(point)
|
||||
slopeStr := "both"
|
||||
if slope == 0 {
|
||||
slopeStr = "zero"
|
||||
}
|
||||
argstr = append(argstr, fmt.Sprintf("--slope=%s", slopeStr))
|
||||
|
||||
for k, v := range point.Fields() {
|
||||
if k == "value" {
|
||||
switch value := v.(type) {
|
||||
case float64:
|
||||
argstr = append(argstr,
|
||||
fmt.Sprintf("--value=%v", value), "--type=double")
|
||||
case float32:
|
||||
argstr = append(argstr,
|
||||
fmt.Sprintf("--value=%v", value), "--type=float")
|
||||
case int:
|
||||
argstr = append(argstr,
|
||||
fmt.Sprintf("--value=%d", value), "--type=int32")
|
||||
case int64:
|
||||
argstr = append(argstr,
|
||||
fmt.Sprintf("--value=%d", value), "--type=int32")
|
||||
case string:
|
||||
argstr = append(argstr,
|
||||
fmt.Sprintf("--value=%q", value), "--type=string")
|
||||
}
|
||||
}
|
||||
}
|
||||
command := exec.Command(s.gmetric_path, argstr...)
|
||||
command.Wait()
|
||||
_, err = command.Output()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *GangliaSink) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GangliaSink) Close() {
|
||||
return s, nil
|
||||
}
|
||||
|
@@ -19,9 +19,9 @@ type HttpSinkConfig struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
JWT string `json:"jwt,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
MaxIdleConns int `json:"max_idle_connections,omitempty"`
|
||||
IdleConnTimeout string `json:"idle_connection_timeout,omitempty"`
|
||||
FlushDelay string `json:"flush_delay,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
}
|
||||
|
||||
type HttpSink struct {
|
||||
@@ -32,32 +32,118 @@ type HttpSink struct {
|
||||
buffer *bytes.Buffer
|
||||
flushTimer *time.Timer
|
||||
config HttpSinkConfig
|
||||
maxIdleConns int
|
||||
idleConnTimeout time.Duration
|
||||
timeout time.Duration
|
||||
flushDelay time.Duration
|
||||
}
|
||||
|
||||
func (s *HttpSink) Init(config json.RawMessage) error {
|
||||
func (s *HttpSink) Write(m lp.CCMetric) error {
|
||||
p := m.ToPoint(s.meta_as_tags)
|
||||
s.lock.Lock()
|
||||
firstWriteOfBatch := s.buffer.Len() == 0
|
||||
_, err := s.encoder.Encode(p)
|
||||
s.lock.Unlock()
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "encoding failed:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if s.flushDelay == 0 {
|
||||
return s.Flush()
|
||||
}
|
||||
|
||||
if firstWriteOfBatch {
|
||||
if s.flushTimer == nil {
|
||||
s.flushTimer = time.AfterFunc(s.flushDelay, func() {
|
||||
if err := s.Flush(); err != nil {
|
||||
cclog.ComponentError(s.name, "flush failed:", err.Error())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
s.flushTimer.Reset(s.flushDelay)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HttpSink) Flush() error {
|
||||
// Own lock for as short as possible: the time it takes to copy the buffer.
|
||||
s.lock.Lock()
|
||||
buf := make([]byte, s.buffer.Len())
|
||||
copy(buf, s.buffer.Bytes())
|
||||
s.buffer.Reset()
|
||||
s.lock.Unlock()
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var res *http.Response
|
||||
for i := 0; i < s.config.MaxRetries; i++ {
|
||||
// Create new request to send buffer
|
||||
req, err := http.NewRequest(http.MethodPost, s.config.URL, bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "failed to create request:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Set authorization header
|
||||
if len(s.config.JWT) != 0 {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.config.JWT))
|
||||
}
|
||||
|
||||
// Do request
|
||||
res, err = s.client.Do(req)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "transport/tcp error:", err.Error())
|
||||
// Wait between retries
|
||||
time.Sleep(time.Duration(i+1) * (time.Second / 2))
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if res == nil {
|
||||
return errors.New("flush failed due to repeated errors")
|
||||
}
|
||||
|
||||
// Handle application errors
|
||||
if res.StatusCode != http.StatusOK {
|
||||
err := errors.New(res.Status)
|
||||
cclog.ComponentError(s.name, "application error:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HttpSink) Close() {
|
||||
s.flushTimer.Stop()
|
||||
if err := s.Flush(); err != nil {
|
||||
cclog.ComponentError(s.name, "flush failed:", err.Error())
|
||||
}
|
||||
s.client.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func NewHttpSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(HttpSink)
|
||||
// Set default values
|
||||
s.name = "HttpSink"
|
||||
s.config.MaxIdleConns = 10
|
||||
s.config.IdleConnTimeout = "5s"
|
||||
s.name = fmt.Sprintf("HttpSink(%s)", name)
|
||||
s.config.IdleConnTimeout = "120s" // should be larger than the measurement interval.
|
||||
s.config.Timeout = "5s"
|
||||
s.config.FlushDelay = "1s"
|
||||
s.config.FlushDelay = "5s"
|
||||
s.config.MaxRetries = 3
|
||||
|
||||
// Read config
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(s.config.URL) == 0 {
|
||||
return errors.New("`url` config option is required for HTTP sink")
|
||||
}
|
||||
if s.config.MaxIdleConns > 0 {
|
||||
s.maxIdleConns = s.config.MaxIdleConns
|
||||
return nil, errors.New("`url` config option is required for HTTP sink")
|
||||
}
|
||||
if len(s.config.IdleConnTimeout) > 0 {
|
||||
t, err := time.ParseDuration(s.config.IdleConnTimeout)
|
||||
@@ -77,95 +163,18 @@ func (s *HttpSink) Init(config json.RawMessage) error {
|
||||
s.flushDelay = t
|
||||
}
|
||||
}
|
||||
// Create lookup map to use meta infos as tags in the output metric
|
||||
s.meta_as_tags = make(map[string]bool)
|
||||
for _, k := range s.config.MetaAsTags {
|
||||
s.meta_as_tags[k] = true
|
||||
}
|
||||
tr := &http.Transport{
|
||||
MaxIdleConns: s.maxIdleConns,
|
||||
MaxIdleConns: 1, // We will only ever talk to one host.
|
||||
IdleConnTimeout: s.idleConnTimeout,
|
||||
}
|
||||
s.client = &http.Client{Transport: tr, Timeout: s.timeout}
|
||||
s.buffer = &bytes.Buffer{}
|
||||
s.encoder = influx.NewEncoder(s.buffer)
|
||||
s.encoder.SetPrecision(time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HttpSink) Write(m lp.CCMetric) error {
|
||||
if s.buffer.Len() == 0 && s.flushDelay != 0 {
|
||||
// This is the first write since the last flush, start the flushTimer!
|
||||
if s.flushTimer != nil && s.flushTimer.Stop() {
|
||||
cclog.ComponentDebug("HttpSink", "unexpected: the flushTimer was already running?")
|
||||
}
|
||||
|
||||
// Run a batched flush for all lines that have arrived in the last second
|
||||
s.flushTimer = time.AfterFunc(s.flushDelay, func() {
|
||||
if err := s.Flush(); err != nil {
|
||||
cclog.ComponentError("HttpSink", "flush failed:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
p := m.ToPoint(s.config.MetaAsTags)
|
||||
|
||||
s.lock.Lock()
|
||||
_, err := s.encoder.Encode(p)
|
||||
s.lock.Unlock() // defer does not work here as Flush() takes the lock as well
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Flush synchronously if "flush_delay" is zero
|
||||
if s.flushDelay == 0 {
|
||||
return s.Flush()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *HttpSink) Flush() error {
|
||||
// buffer is read by client.Do, prevent concurrent modifications
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// Do not flush empty buffer
|
||||
if s.buffer.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create new request to send buffer
|
||||
req, err := http.NewRequest(http.MethodPost, s.config.URL, s.buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set authorization header
|
||||
if len(s.config.JWT) != 0 {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.config.JWT))
|
||||
}
|
||||
|
||||
// Send
|
||||
res, err := s.client.Do(req)
|
||||
|
||||
// Clear buffer
|
||||
s.buffer.Reset()
|
||||
|
||||
// Handle transport/tcp errors
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle application errors
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *HttpSink) Close() {
|
||||
s.flushTimer.Stop()
|
||||
if err := s.Flush(); err != nil {
|
||||
cclog.ComponentError("HttpSink", "flush failed:", err.Error())
|
||||
}
|
||||
s.client.CloseIdleConnections()
|
||||
return s, nil
|
||||
}
|
||||
|
@@ -1,15 +1,19 @@
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
influxdb2Api "github.com/influxdata/influxdb-client-go/v2/api"
|
||||
influxdb2ApiHttp "github.com/influxdata/influxdb-client-go/v2/api/http"
|
||||
)
|
||||
|
||||
type InfluxAsyncSinkConfig struct {
|
||||
@@ -21,20 +25,28 @@ type InfluxAsyncSinkConfig struct {
|
||||
Password string `json:"password,omitempty"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
SSL bool `json:"ssl,omitempty"`
|
||||
RetentionPol string `json:"retention_policy,omitempty"`
|
||||
// Maximum number of points sent to server in single request. Default 5000
|
||||
BatchSize uint `json:"batch_size,omitempty"`
|
||||
// Interval, in ms, in which is buffer flushed if it has not been already written (by reaching batch size) . Default 1000ms
|
||||
FlushInterval uint `json:"flush_interval,omitempty"`
|
||||
FlushInterval uint `json:"flush_interval,omitempty"`
|
||||
InfluxRetryInterval string `json:"retry_interval,omitempty"`
|
||||
InfluxExponentialBase uint `json:"retry_exponential_base,omitempty"`
|
||||
InfluxMaxRetries uint `json:"max_retries,omitempty"`
|
||||
InfluxMaxRetryTime string `json:"max_retry_time,omitempty"`
|
||||
CustomFlushInterval string `json:"custom_flush_interval,omitempty"`
|
||||
MaxRetryAttempts uint `json:"max_retry_attempts,omitempty"`
|
||||
}
|
||||
|
||||
type InfluxAsyncSink struct {
|
||||
sink
|
||||
client influxdb2.Client
|
||||
writeApi influxdb2Api.WriteAPI
|
||||
retPolicy string
|
||||
errors <-chan error
|
||||
config InfluxAsyncSinkConfig
|
||||
client influxdb2.Client
|
||||
writeApi influxdb2Api.WriteAPI
|
||||
errors <-chan error
|
||||
config InfluxAsyncSinkConfig
|
||||
influxRetryInterval uint
|
||||
influxMaxRetryTime uint
|
||||
customFlushInterval time.Duration
|
||||
flushTimer *time.Timer
|
||||
}
|
||||
|
||||
func (s *InfluxAsyncSink) connect() error {
|
||||
@@ -53,63 +65,73 @@ func (s *InfluxAsyncSink) connect() error {
|
||||
cclog.ComponentDebug(s.name, "Using URI", uri, "Org", s.config.Organization, "Bucket", s.config.Database)
|
||||
clientOptions := influxdb2.DefaultOptions()
|
||||
if s.config.BatchSize != 0 {
|
||||
cclog.ComponentDebug(s.name, "Batch size", s.config.BatchSize)
|
||||
clientOptions.SetBatchSize(s.config.BatchSize)
|
||||
}
|
||||
if s.config.FlushInterval != 0 {
|
||||
cclog.ComponentDebug(s.name, "Flush interval", s.config.FlushInterval)
|
||||
clientOptions.SetFlushInterval(s.config.FlushInterval)
|
||||
}
|
||||
if s.influxRetryInterval != 0 {
|
||||
cclog.ComponentDebug(s.name, "MaxRetryInterval", s.influxRetryInterval)
|
||||
clientOptions.SetMaxRetryInterval(s.influxRetryInterval)
|
||||
}
|
||||
if s.influxMaxRetryTime != 0 {
|
||||
cclog.ComponentDebug(s.name, "MaxRetryTime", s.influxMaxRetryTime)
|
||||
clientOptions.SetMaxRetryTime(s.influxMaxRetryTime)
|
||||
}
|
||||
if s.config.InfluxExponentialBase != 0 {
|
||||
cclog.ComponentDebug(s.name, "Exponential Base", s.config.InfluxExponentialBase)
|
||||
clientOptions.SetExponentialBase(s.config.InfluxExponentialBase)
|
||||
}
|
||||
if s.config.InfluxMaxRetries != 0 {
|
||||
cclog.ComponentDebug(s.name, "Max Retries", s.config.InfluxMaxRetries)
|
||||
clientOptions.SetMaxRetries(s.config.InfluxMaxRetries)
|
||||
}
|
||||
clientOptions.SetTLSConfig(
|
||||
&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
)
|
||||
).SetPrecision(time.Second)
|
||||
|
||||
s.client = influxdb2.NewClientWithOptions(uri, auth, clientOptions)
|
||||
s.writeApi = s.client.WriteAPI(s.config.Organization, s.config.Database)
|
||||
ok, err := s.client.Ping(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("connection to %s not healthy", uri)
|
||||
}
|
||||
s.writeApi.SetWriteFailedCallback(func(batch string, err influxdb2ApiHttp.Error, retryAttempts uint) bool {
|
||||
mlist := strings.Split(batch, "\n")
|
||||
cclog.ComponentError(s.name, fmt.Sprintf("Failed to write batch with %d metrics %d times (max: %d): %s", len(mlist), retryAttempts, s.config.MaxRetryAttempts, err.Error()))
|
||||
return retryAttempts <= s.config.MaxRetryAttempts
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InfluxAsyncSink) Init(config json.RawMessage) error {
|
||||
s.name = "InfluxSink"
|
||||
|
||||
// Set default for maximum number of points sent to server in single request.
|
||||
s.config.BatchSize = 100
|
||||
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(s.config.Host) == 0 ||
|
||||
len(s.config.Port) == 0 ||
|
||||
len(s.config.Database) == 0 ||
|
||||
len(s.config.Organization) == 0 ||
|
||||
len(s.config.Password) == 0 {
|
||||
return errors.New("not all configuration variables set required by InfluxAsyncSink")
|
||||
}
|
||||
|
||||
// Connect to InfluxDB server
|
||||
err := s.connect()
|
||||
|
||||
// Start background: Read from error channel
|
||||
s.errors = s.writeApi.Errors()
|
||||
go func() {
|
||||
for err := range s.errors {
|
||||
cclog.ComponentError(s.name, err.Error())
|
||||
}
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *InfluxAsyncSink) Write(m lp.CCMetric) error {
|
||||
if s.customFlushInterval != 0 && s.flushTimer == nil {
|
||||
// Run a batched flush for all lines that have arrived in the defined interval
|
||||
s.flushTimer = time.AfterFunc(s.customFlushInterval, func() {
|
||||
if err := s.Flush(); err != nil {
|
||||
cclog.ComponentError(s.name, "flush failed:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
s.writeApi.WritePoint(
|
||||
m.ToPoint(s.config.MetaAsTags),
|
||||
m.ToPoint(s.meta_as_tags),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InfluxAsyncSink) Flush() error {
|
||||
cclog.ComponentDebug(s.name, "Flushing")
|
||||
s.writeApi.Flush()
|
||||
if s.customFlushInterval != 0 && s.flushTimer != nil {
|
||||
s.flushTimer = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -118,3 +140,100 @@ func (s *InfluxAsyncSink) Close() {
|
||||
s.writeApi.Flush()
|
||||
s.client.Close()
|
||||
}
|
||||
|
||||
func NewInfluxAsyncSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(InfluxAsyncSink)
|
||||
s.name = fmt.Sprintf("InfluxSink(%s)", name)
|
||||
|
||||
// Set default for maximum number of points sent to server in single request.
|
||||
s.config.BatchSize = 0
|
||||
s.influxRetryInterval = 0
|
||||
//s.config.InfluxRetryInterval = "1s"
|
||||
s.influxMaxRetryTime = 0
|
||||
//s.config.InfluxMaxRetryTime = "168h"
|
||||
s.config.InfluxMaxRetries = 0
|
||||
s.config.InfluxExponentialBase = 0
|
||||
s.config.FlushInterval = 0
|
||||
s.config.CustomFlushInterval = ""
|
||||
s.customFlushInterval = time.Duration(0)
|
||||
s.config.MaxRetryAttempts = 1
|
||||
|
||||
// Default retry intervals (in seconds)
|
||||
// 1 2
|
||||
// 2 4
|
||||
// 4 8
|
||||
// 8 16
|
||||
// 16 32
|
||||
// 32 64
|
||||
// 64 128
|
||||
// 128 256
|
||||
// 256 512
|
||||
// 512 1024
|
||||
// 1024 2048
|
||||
// 2048 4096
|
||||
// 4096 8192
|
||||
// 8192 16384
|
||||
// 16384 32768
|
||||
// 32768 65536
|
||||
// 65536 131072
|
||||
// 131072 262144
|
||||
// 262144 524288
|
||||
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(s.config.Port) == 0 {
|
||||
return nil, errors.New("Missing port configuration required by InfluxSink")
|
||||
}
|
||||
if len(s.config.Database) == 0 {
|
||||
return nil, errors.New("Missing database configuration required by InfluxSink")
|
||||
}
|
||||
if len(s.config.Organization) == 0 {
|
||||
return nil, errors.New("Missing organization configuration required by InfluxSink")
|
||||
}
|
||||
if len(s.config.Password) == 0 {
|
||||
return nil, errors.New("Missing password configuration required by InfluxSink")
|
||||
}
|
||||
// Create lookup map to use meta infos as tags in the output metric
|
||||
s.meta_as_tags = make(map[string]bool)
|
||||
for _, k := range s.config.MetaAsTags {
|
||||
s.meta_as_tags[k] = true
|
||||
}
|
||||
|
||||
toUint := func(duration string, def uint) uint {
|
||||
t, err := time.ParseDuration(duration)
|
||||
if err == nil {
|
||||
return uint(t.Milliseconds())
|
||||
}
|
||||
return def
|
||||
}
|
||||
s.influxRetryInterval = toUint(s.config.InfluxRetryInterval, s.influxRetryInterval)
|
||||
s.influxMaxRetryTime = toUint(s.config.InfluxMaxRetryTime, s.influxMaxRetryTime)
|
||||
|
||||
// Use a own timer for calling Flush()
|
||||
if len(s.config.CustomFlushInterval) > 0 {
|
||||
t, err := time.ParseDuration(s.config.CustomFlushInterval)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid duration in 'custom_flush_interval': %v", err)
|
||||
}
|
||||
s.customFlushInterval = t
|
||||
}
|
||||
|
||||
// Connect to InfluxDB server
|
||||
if err := s.connect(); err != nil {
|
||||
return nil, fmt.Errorf("unable to connect: %v", err)
|
||||
}
|
||||
|
||||
// Start background: Read from error channel
|
||||
s.errors = s.writeApi.Errors()
|
||||
go func() {
|
||||
for err := range s.errors {
|
||||
cclog.ComponentError(s.name, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
@@ -18,6 +18,10 @@ The `influxasync` sink uses the official [InfluxDB golang client](https://pkg.go
|
||||
"organization": "myorg",
|
||||
"ssl": true,
|
||||
"batch_size": 200,
|
||||
"retry_interval" : "1s",
|
||||
"retry_exponential_base" : 2,
|
||||
"max_retries": 20,
|
||||
"max_retry_time" : "168h"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -31,4 +35,10 @@ The `influxasync` sink uses the official [InfluxDB golang client](https://pkg.go
|
||||
- `password`: Password for basic authentification
|
||||
- `organization`: Organization in the InfluxDB
|
||||
- `ssl`: Use SSL connection
|
||||
- `batch_size`: batch up metrics internally, default 100
|
||||
- `batch_size`: batch up metrics internally, default 100
|
||||
- `retry_interval`: Base retry interval for failed write requests, default 1s
|
||||
- `retry_exponential_base`: The retry interval is exponentially increased with this base, default 2
|
||||
- `max_retries`: Maximal number of retry attempts
|
||||
- `max_retry_time`: Maximal time to retry failed writes, default 168h (one week)
|
||||
|
||||
For information about the calculation of the retry interval settings, see [offical influxdb-client-go documentation](https://github.com/influxdata/influxdb-client-go#handling-of-failed-async-writes)
|
@@ -6,91 +6,216 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
influxdb2 "github.com/influxdata/influxdb-client-go/v2"
|
||||
influxdb2Api "github.com/influxdata/influxdb-client-go/v2/api"
|
||||
"github.com/influxdata/influxdb-client-go/v2/api/write"
|
||||
)
|
||||
|
||||
type InfluxSinkConfig struct {
|
||||
defaultSinkConfig
|
||||
Host string `json:"host,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Database string `json:"database,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
SSL bool `json:"ssl,omitempty"`
|
||||
RetentionPol string `json:"retention_policy,omitempty"`
|
||||
}
|
||||
|
||||
type InfluxSink struct {
|
||||
sink
|
||||
client influxdb2.Client
|
||||
writeApi influxdb2Api.WriteAPIBlocking
|
||||
config InfluxSinkConfig
|
||||
config struct {
|
||||
defaultSinkConfig
|
||||
Host string `json:"host,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Database string `json:"database,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
SSL bool `json:"ssl,omitempty"`
|
||||
// Maximum number of points sent to server in single request. Default 100
|
||||
BatchSize int `json:"batch_size,omitempty"`
|
||||
// Interval, in which is buffer flushed if it has not been already written (by reaching batch size). Default 1s
|
||||
FlushInterval string `json:"flush_delay,omitempty"`
|
||||
}
|
||||
batch []*write.Point
|
||||
flushTimer *time.Timer
|
||||
flushDelay time.Duration
|
||||
lock sync.Mutex // Flush() runs in another goroutine, so this lock has to protect the buffer
|
||||
}
|
||||
|
||||
// connect connects to the InfluxDB server
|
||||
func (s *InfluxSink) connect() error {
|
||||
var auth string
|
||||
|
||||
// URI options:
|
||||
// * http://host:port
|
||||
// * https://host:port
|
||||
var uri string
|
||||
if s.config.SSL {
|
||||
uri = fmt.Sprintf("https://%s:%s", s.config.Host, s.config.Port)
|
||||
} else {
|
||||
uri = fmt.Sprintf("http://%s:%s", s.config.Host, s.config.Port)
|
||||
}
|
||||
|
||||
// Authentication options:
|
||||
// * token
|
||||
// * username:password
|
||||
var auth string
|
||||
if len(s.config.User) == 0 {
|
||||
auth = s.config.Password
|
||||
} else {
|
||||
auth = fmt.Sprintf("%s:%s", s.config.User, s.config.Password)
|
||||
}
|
||||
cclog.ComponentDebug(s.name, "Using URI", uri, "Org", s.config.Organization, "Bucket", s.config.Database)
|
||||
|
||||
// Set influxDB client options
|
||||
clientOptions := influxdb2.DefaultOptions()
|
||||
|
||||
// Do not check InfluxDB certificate
|
||||
clientOptions.SetTLSConfig(
|
||||
&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
)
|
||||
|
||||
clientOptions.SetPrecision(time.Second)
|
||||
|
||||
// Create new writeAPI
|
||||
s.client = influxdb2.NewClientWithOptions(uri, auth, clientOptions)
|
||||
s.writeApi = s.client.WriteAPIBlocking(s.config.Organization, s.config.Database)
|
||||
|
||||
// Check InfluxDB server accessibility
|
||||
ok, err := s.client.Ping(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("connection to %s not healthy", uri)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InfluxSink) Init(config json.RawMessage) error {
|
||||
s.name = "InfluxSink"
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(s.config.Host) == 0 ||
|
||||
len(s.config.Port) == 0 ||
|
||||
len(s.config.Database) == 0 ||
|
||||
len(s.config.Organization) == 0 ||
|
||||
len(s.config.Password) == 0 {
|
||||
return errors.New("not all configuration variables set required by InfluxSink")
|
||||
}
|
||||
|
||||
// Connect to InfluxDB server
|
||||
return s.connect()
|
||||
}
|
||||
|
||||
func (s *InfluxSink) Write(m lp.CCMetric) error {
|
||||
err :=
|
||||
s.writeApi.WritePoint(
|
||||
context.Background(),
|
||||
m.ToPoint(s.config.MetaAsTags),
|
||||
)
|
||||
return err
|
||||
|
||||
if len(s.batch) == 0 && s.flushDelay != 0 {
|
||||
// This is the first write since the last flush, start the flushTimer!
|
||||
if s.flushTimer != nil && s.flushTimer.Stop() {
|
||||
cclog.ComponentDebug(s.name, "unexpected: the flushTimer was already running?")
|
||||
}
|
||||
|
||||
// Run a batched flush for all lines that have arrived in the last flush delay interval
|
||||
s.flushTimer = time.AfterFunc(
|
||||
s.flushDelay,
|
||||
func() {
|
||||
if err := s.Flush(); err != nil {
|
||||
cclog.ComponentError(s.name, "flush failed:", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Append metric to batch slice
|
||||
p := m.ToPoint(s.meta_as_tags)
|
||||
s.lock.Lock()
|
||||
s.batch = append(s.batch, p)
|
||||
s.lock.Unlock()
|
||||
|
||||
// Flush synchronously if "flush_delay" is zero
|
||||
if s.flushDelay == 0 {
|
||||
return s.Flush()
|
||||
}
|
||||
|
||||
// Flush if batch size is reached
|
||||
if len(s.batch) == s.config.BatchSize {
|
||||
return s.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush sends all metrics buffered in batch slice to InfluxDB server
|
||||
func (s *InfluxSink) Flush() error {
|
||||
|
||||
// Lock access to batch slice
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// Nothing to do, batch slice is empty
|
||||
if len(s.batch) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send metrics from batch slice
|
||||
err := s.writeApi.WritePoint(context.Background(), s.batch...)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "flush failed:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// Clear batch slice
|
||||
for i := range s.batch {
|
||||
s.batch[i] = nil
|
||||
}
|
||||
s.batch = s.batch[:0]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InfluxSink) Close() {
|
||||
cclog.ComponentDebug(s.name, "Closing InfluxDB connection")
|
||||
s.flushTimer.Stop()
|
||||
s.Flush()
|
||||
s.client.Close()
|
||||
}
|
||||
|
||||
// NewInfluxSink create a new InfluxDB sink
|
||||
func NewInfluxSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(InfluxSink)
|
||||
s.name = fmt.Sprintf("InfluxSink(%s)", name)
|
||||
|
||||
// Set config default values
|
||||
s.config.BatchSize = 100
|
||||
s.config.FlushInterval = "1s"
|
||||
|
||||
// Read config
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.config.Host) == 0 {
|
||||
return nil, errors.New("Missing host configuration required by InfluxSink")
|
||||
}
|
||||
if len(s.config.Port) == 0 {
|
||||
return nil, errors.New("Missing port configuration required by InfluxSink")
|
||||
}
|
||||
if len(s.config.Database) == 0 {
|
||||
return nil, errors.New("Missing database configuration required by InfluxSink")
|
||||
}
|
||||
if len(s.config.Organization) == 0 {
|
||||
return nil, errors.New("Missing organization configuration required by InfluxSink")
|
||||
}
|
||||
if len(s.config.Password) == 0 {
|
||||
return nil, errors.New("Missing password configuration required by InfluxSink")
|
||||
}
|
||||
|
||||
// Create lookup map to use meta infos as tags in the output metric
|
||||
s.meta_as_tags = make(map[string]bool)
|
||||
for _, k := range s.config.MetaAsTags {
|
||||
s.meta_as_tags[k] = true
|
||||
}
|
||||
|
||||
// Configure flush delay duration
|
||||
if len(s.config.FlushInterval) > 0 {
|
||||
t, err := time.ParseDuration(s.config.FlushInterval)
|
||||
if err == nil {
|
||||
s.flushDelay = t
|
||||
}
|
||||
}
|
||||
|
||||
// allocate batch slice
|
||||
s.batch = make([]*write.Point, 0, s.config.BatchSize)
|
||||
|
||||
// Connect to InfluxDB server
|
||||
if err := s.connect(); err != nil {
|
||||
return nil, fmt.Errorf("unable to connect: %v", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
@@ -17,6 +17,8 @@ The `influxdb` sink uses the official [InfluxDB golang client](https://pkg.go.de
|
||||
"password" : "examplepw",
|
||||
"organization": "myorg",
|
||||
"ssl": true,
|
||||
"flush_delay" : "1s",
|
||||
"batch_size" : 100
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -29,4 +31,7 @@ The `influxdb` sink uses the official [InfluxDB golang client](https://pkg.go.de
|
||||
- `user`: Username for basic authentification
|
||||
- `password`: Password for basic authentification
|
||||
- `organization`: Organization in the InfluxDB
|
||||
- `ssl`: Use SSL connection
|
||||
- `ssl`: Use SSL connection
|
||||
- `flush_delay`: Group metrics coming in to a single batch
|
||||
- `batch_size`: Maximal batch size
|
||||
|
||||
|
@@ -82,21 +82,21 @@ const (
|
||||
GMOND_CONFIG_FILE = `/etc/ganglia/gmond.conf`
|
||||
)
|
||||
|
||||
type LibgangliaSinkSpecialMetric struct {
|
||||
MetricName string `json:"metric_name,omitempty"`
|
||||
NewName string `json:"new_name,omitempty"`
|
||||
Slope string `json:"slope,omitempty"`
|
||||
}
|
||||
// type LibgangliaSinkSpecialMetric struct {
|
||||
// MetricName string `json:"metric_name,omitempty"`
|
||||
// NewName string `json:"new_name,omitempty"`
|
||||
// Slope string `json:"slope,omitempty"`
|
||||
// }
|
||||
|
||||
type LibgangliaSinkConfig struct {
|
||||
defaultSinkConfig
|
||||
GangliaLib string `json:"libganglia_path,omitempty"`
|
||||
GmondConfig string `json:"gmond_config,omitempty"`
|
||||
AddGangliaGroup bool `json:"add_ganglia_group,omitempty"`
|
||||
AddTypeToName bool `json:"add_type_to_name,omitempty"`
|
||||
AddUnits bool `json:"add_units,omitempty"`
|
||||
ClusterName string `json:"cluster_name,omitempty"`
|
||||
SpecialMetrics map[string]LibgangliaSinkSpecialMetric `json:"rename_metrics,omitempty"` // Map to rename metric name from key to value
|
||||
GangliaLib string `json:"libganglia_path,omitempty"`
|
||||
GmondConfig string `json:"gmond_config,omitempty"`
|
||||
AddGangliaGroup bool `json:"add_ganglia_group,omitempty"`
|
||||
AddTypeToName bool `json:"add_type_to_name,omitempty"`
|
||||
AddUnits bool `json:"add_units,omitempty"`
|
||||
ClusterName string `json:"cluster_name,omitempty"`
|
||||
//SpecialMetrics map[string]LibgangliaSinkSpecialMetric `json:"rename_metrics,omitempty"` // Map to rename metric name from key to value
|
||||
//AddTagsAsDesc bool `json:"add_tags_as_desc,omitempty"`
|
||||
}
|
||||
|
||||
@@ -109,65 +109,6 @@ type LibgangliaSink struct {
|
||||
cstrCache map[string]*C.char
|
||||
}
|
||||
|
||||
func (s *LibgangliaSink) Init(config json.RawMessage) error {
|
||||
var err error = nil
|
||||
s.name = "LibgangliaSink"
|
||||
//s.config.AddTagsAsDesc = false
|
||||
s.config.AddGangliaGroup = false
|
||||
s.config.AddTypeToName = false
|
||||
s.config.AddUnits = true
|
||||
s.config.GmondConfig = string(GMOND_CONFIG_FILE)
|
||||
s.config.GangliaLib = string(GANGLIA_LIB_NAME)
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "Error reading config:", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
lib := dl.New(s.config.GangliaLib, GANGLIA_LIB_DL_FLAGS)
|
||||
if lib == nil {
|
||||
return fmt.Errorf("error instantiating DynamicLibrary for %s", s.config.GangliaLib)
|
||||
}
|
||||
err = lib.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening %s: %v", s.config.GangliaLib, err)
|
||||
}
|
||||
|
||||
// Set up cache for the C strings
|
||||
s.cstrCache = make(map[string]*C.char)
|
||||
// s.cstrCache["globals"] = C.CString("globals")
|
||||
|
||||
// s.cstrCache["override_hostname"] = C.CString("override_hostname")
|
||||
// s.cstrCache["override_ip"] = C.CString("override_ip")
|
||||
|
||||
// Add some constant strings
|
||||
s.cstrCache["GROUP"] = C.CString("GROUP")
|
||||
s.cstrCache["CLUSTER"] = C.CString("CLUSTER")
|
||||
s.cstrCache[""] = C.CString("")
|
||||
|
||||
// Add cluster name for lookup in Write()
|
||||
if len(s.config.ClusterName) > 0 {
|
||||
s.cstrCache[s.config.ClusterName] = C.CString(s.config.ClusterName)
|
||||
}
|
||||
// Add supported types for later lookup in Write()
|
||||
s.cstrCache["double"] = C.CString("double")
|
||||
s.cstrCache["int32"] = C.CString("int32")
|
||||
s.cstrCache["string"] = C.CString("string")
|
||||
|
||||
// Create Ganglia pool
|
||||
s.global_context = C.Ganglia_pool_create(nil)
|
||||
// Load Ganglia configuration
|
||||
s.cstrCache[s.config.GmondConfig] = C.CString(s.config.GmondConfig)
|
||||
s.gmond_config = C.Ganglia_gmond_config_create(s.cstrCache[s.config.GmondConfig], 0)
|
||||
//globals := C.cfg_getsec(gmond_config, s.cstrCache["globals"])
|
||||
//override_hostname := C.cfg_getstr(globals, s.cstrCache["override_hostname"])
|
||||
//override_ip := C.cfg_getstr(globals, s.cstrCache["override_ip"])
|
||||
|
||||
s.send_channels = C.Ganglia_udp_send_channels_create(s.global_context, s.gmond_config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LibgangliaSink) Write(point lp.CCMetric) error {
|
||||
var err error = nil
|
||||
var c_name *C.char
|
||||
@@ -183,73 +124,46 @@ func (s *LibgangliaSink) Write(point lp.CCMetric) error {
|
||||
return s.cstrCache[key]
|
||||
}
|
||||
|
||||
// Get metric name
|
||||
metricname := GangliaMetricRename(point)
|
||||
if s.config.AddTypeToName {
|
||||
c_name = lookup(GangliaMetricName(point))
|
||||
} else {
|
||||
c_name = lookup(metricname)
|
||||
conf := GetCommonGangliaConfig(point)
|
||||
if len(conf.Type) == 0 {
|
||||
conf = GetGangliaConfig(point)
|
||||
}
|
||||
if len(conf.Type) == 0 {
|
||||
return fmt.Errorf("metric %q (Ganglia name %q) has no 'value' field", point.Name(), conf.Name)
|
||||
}
|
||||
|
||||
// Get the value C string and lookup the type string in the cache
|
||||
value, ok := point.GetField("value")
|
||||
if !ok {
|
||||
return fmt.Errorf("metric %s has no 'value' field", metricname)
|
||||
}
|
||||
switch real := value.(type) {
|
||||
case float64:
|
||||
c_value = C.CString(fmt.Sprintf("%f", real))
|
||||
c_type = lookup("double")
|
||||
case float32:
|
||||
c_value = C.CString(fmt.Sprintf("%f", real))
|
||||
c_type = lookup("float")
|
||||
case int64:
|
||||
c_value = C.CString(fmt.Sprintf("%d", real))
|
||||
c_type = lookup("int32")
|
||||
case int32:
|
||||
c_value = C.CString(fmt.Sprintf("%d", real))
|
||||
c_type = lookup("int32")
|
||||
case int:
|
||||
c_value = C.CString(fmt.Sprintf("%d", real))
|
||||
c_type = lookup("int32")
|
||||
case string:
|
||||
c_value = C.CString(real)
|
||||
c_type = lookup("string")
|
||||
default:
|
||||
return fmt.Errorf("metric %s has invalid 'value' type for %s", point.Name(), s.name)
|
||||
if s.config.AddTypeToName {
|
||||
conf.Name = GangliaMetricName(point)
|
||||
}
|
||||
|
||||
c_value = C.CString(conf.Value)
|
||||
c_type = lookup(conf.Type)
|
||||
c_name = lookup(conf.Name)
|
||||
|
||||
// Add unit
|
||||
unit := ""
|
||||
if s.config.AddUnits {
|
||||
if tagunit, tagok := point.GetTag("unit"); tagok {
|
||||
c_unit = lookup(tagunit)
|
||||
} else if metaunit, metaok := point.GetMeta("unit"); metaok {
|
||||
c_unit = lookup(metaunit)
|
||||
} else {
|
||||
c_unit = lookup("")
|
||||
}
|
||||
} else {
|
||||
c_unit = lookup("")
|
||||
unit = conf.Unit
|
||||
}
|
||||
c_unit = lookup(unit)
|
||||
|
||||
// Determine the slope of the metric. Ganglia's own collector mostly use
|
||||
// 'both' but the mem and swap total uses 'zero'.
|
||||
slope := GangliaSlopeType(point)
|
||||
slope_type := C.GANGLIA_SLOPE_BOTH
|
||||
switch slope {
|
||||
case 0:
|
||||
switch conf.Slope {
|
||||
case "zero":
|
||||
slope_type = C.GANGLIA_SLOPE_ZERO
|
||||
case "both":
|
||||
slope_type = C.GANGLIA_SLOPE_BOTH
|
||||
}
|
||||
|
||||
// Create a new Ganglia metric
|
||||
gmetric := C.Ganglia_metric_create(s.global_context)
|
||||
// Set name, value, type and unit in the Ganglia metric
|
||||
// Since we don't have this information from the collectors,
|
||||
// we assume that the metric value can go up and down (slope),
|
||||
// and there is no maximum for 'dmax' and 'tmax'.
|
||||
// Ganglia's collectors set 'tmax' but not 'dmax'
|
||||
// The default slope_type is both directions, so up and down. Some metrics want 'zero' slope, probably constant.
|
||||
// The 'tmax' value is by default 300.
|
||||
rval := C.int(0)
|
||||
rval = C.Ganglia_metric_set(gmetric, c_name, c_value, c_type, c_unit, C.uint(slope_type), 0, 0)
|
||||
rval = C.Ganglia_metric_set(gmetric, c_name, c_value, c_type, c_unit, C.uint(slope_type), C.uint(conf.Tmax), 0)
|
||||
switch rval {
|
||||
case 1:
|
||||
C.free(unsafe.Pointer(c_value))
|
||||
@@ -259,10 +173,10 @@ func (s *LibgangliaSink) Write(point lp.CCMetric) error {
|
||||
return errors.New("one of your parameters has an invalid character '\"'")
|
||||
case 3:
|
||||
C.free(unsafe.Pointer(c_value))
|
||||
return fmt.Errorf("the type parameter \"%s\" is not a valid type", C.GoString(c_type))
|
||||
return fmt.Errorf("the type parameter \"%s\" is not a valid type", conf.Type)
|
||||
case 4:
|
||||
C.free(unsafe.Pointer(c_value))
|
||||
return fmt.Errorf("the value parameter \"%s\" does not represent a number", C.GoString(c_value))
|
||||
return fmt.Errorf("the value parameter \"%s\" does not represent a number", conf.Value)
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -271,8 +185,8 @@ func (s *LibgangliaSink) Write(point lp.CCMetric) error {
|
||||
C.Ganglia_metadata_add(gmetric, lookup("CLUSTER"), lookup(s.config.ClusterName))
|
||||
}
|
||||
// Set the group metadata in the Ganglia metric if configured
|
||||
if group, ok := point.GetMeta("group"); ok && s.config.AddGangliaGroup {
|
||||
c_group := lookup(group)
|
||||
if s.config.AddGangliaGroup {
|
||||
c_group := lookup(conf.Group)
|
||||
C.Ganglia_metadata_add(gmetric, lookup("GROUP"), c_group)
|
||||
}
|
||||
|
||||
@@ -307,3 +221,63 @@ func (s *LibgangliaSink) Close() {
|
||||
C.free(unsafe.Pointer(cstr))
|
||||
}
|
||||
}
|
||||
|
||||
func NewLibgangliaSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(LibgangliaSink)
|
||||
var err error = nil
|
||||
s.name = fmt.Sprintf("LibgangliaSink(%s)", name)
|
||||
//s.config.AddTagsAsDesc = false
|
||||
s.config.AddGangliaGroup = false
|
||||
s.config.AddTypeToName = false
|
||||
s.config.AddUnits = true
|
||||
s.config.GmondConfig = string(GMOND_CONFIG_FILE)
|
||||
s.config.GangliaLib = string(GANGLIA_LIB_NAME)
|
||||
if len(config) > 0 {
|
||||
err = json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "Error reading config:", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
lib := dl.New(s.config.GangliaLib, GANGLIA_LIB_DL_FLAGS)
|
||||
if lib == nil {
|
||||
return nil, fmt.Errorf("error instantiating DynamicLibrary for %s", s.config.GangliaLib)
|
||||
}
|
||||
err = lib.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening %s: %v", s.config.GangliaLib, err)
|
||||
}
|
||||
|
||||
// Set up cache for the C strings
|
||||
s.cstrCache = make(map[string]*C.char)
|
||||
// s.cstrCache["globals"] = C.CString("globals")
|
||||
|
||||
// s.cstrCache["override_hostname"] = C.CString("override_hostname")
|
||||
// s.cstrCache["override_ip"] = C.CString("override_ip")
|
||||
|
||||
// Add some constant strings
|
||||
s.cstrCache["GROUP"] = C.CString("GROUP")
|
||||
s.cstrCache["CLUSTER"] = C.CString("CLUSTER")
|
||||
s.cstrCache[""] = C.CString("")
|
||||
|
||||
// Add cluster name for lookup in Write()
|
||||
if len(s.config.ClusterName) > 0 {
|
||||
s.cstrCache[s.config.ClusterName] = C.CString(s.config.ClusterName)
|
||||
}
|
||||
// Add supported types for later lookup in Write()
|
||||
s.cstrCache["double"] = C.CString("double")
|
||||
s.cstrCache["int32"] = C.CString("int32")
|
||||
s.cstrCache["string"] = C.CString("string")
|
||||
|
||||
// Create Ganglia pool
|
||||
s.global_context = C.Ganglia_pool_create(nil)
|
||||
// Load Ganglia configuration
|
||||
s.cstrCache[s.config.GmondConfig] = C.CString(s.config.GmondConfig)
|
||||
s.gmond_config = C.Ganglia_gmond_config_create(s.cstrCache[s.config.GmondConfig], 0)
|
||||
//globals := C.cfg_getsec(gmond_config, s.cstrCache["globals"])
|
||||
//override_hostname := C.cfg_getstr(globals, s.cstrCache["override_hostname"])
|
||||
//override_ip := C.cfg_getstr(globals, s.cstrCache["override_ip"])
|
||||
|
||||
s.send_channels = C.Ganglia_udp_send_channels_create(s.global_context, s.gmond_config)
|
||||
return s, nil
|
||||
}
|
||||
|
@@ -1,29 +1,27 @@
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
type defaultSinkConfig struct {
|
||||
MetaAsTags bool `json:"meta_as_tags,omitempty"`
|
||||
Type string `json:"type"`
|
||||
MetaAsTags []string `json:"meta_as_tags,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type sink struct {
|
||||
meta_as_tags bool
|
||||
name string
|
||||
meta_as_tags map[string]bool // Use meta data tags as tags
|
||||
name string // Name of the sink
|
||||
}
|
||||
|
||||
type Sink interface {
|
||||
Init(config json.RawMessage) error
|
||||
Write(point lp.CCMetric) error
|
||||
Flush() error
|
||||
Close()
|
||||
Name() string
|
||||
Write(point lp.CCMetric) error // Write metric to the sink
|
||||
Flush() error // Flush buffered metrics
|
||||
Close() // Close / finish metric sink
|
||||
Name() string // Name of the metric sink
|
||||
}
|
||||
|
||||
// Name returns the name of the metric sink
|
||||
func (s *sink) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
@@ -15,11 +16,12 @@ import (
|
||||
|
||||
type NatsSinkConfig struct {
|
||||
defaultSinkConfig
|
||||
Host string `json:"host,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Database string `json:"database,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
FlushDelay string `json:"flush_delay,omitempty"`
|
||||
}
|
||||
|
||||
type NatsSink struct {
|
||||
@@ -28,6 +30,10 @@ type NatsSink struct {
|
||||
encoder *influx.Encoder
|
||||
buffer *bytes.Buffer
|
||||
config NatsSinkConfig
|
||||
|
||||
lock sync.Mutex
|
||||
flushDelay time.Duration
|
||||
flushTimer *time.Timer
|
||||
}
|
||||
|
||||
func (s *NatsSink) connect() error {
|
||||
@@ -53,19 +59,70 @@ func (s *NatsSink) connect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NatsSink) Init(config json.RawMessage) error {
|
||||
s.name = "NatsSink"
|
||||
func (s *NatsSink) Write(m lp.CCMetric) error {
|
||||
s.lock.Lock()
|
||||
_, err := s.encoder.Encode(m.ToPoint(s.meta_as_tags))
|
||||
s.lock.Unlock()
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "Write:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if s.flushDelay == 0 {
|
||||
s.Flush()
|
||||
} else if s.flushTimer == nil {
|
||||
s.flushTimer = time.AfterFunc(s.flushDelay, func() {
|
||||
s.Flush()
|
||||
})
|
||||
} else {
|
||||
s.flushTimer.Reset(s.flushDelay)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NatsSink) Flush() error {
|
||||
s.lock.Lock()
|
||||
buf := append([]byte{}, s.buffer.Bytes()...) // copy bytes
|
||||
s.buffer.Reset()
|
||||
s.lock.Unlock()
|
||||
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.client.Publish(s.config.Subject, buf); err != nil {
|
||||
cclog.ComponentError(s.name, "Flush:", err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NatsSink) Close() {
|
||||
cclog.ComponentDebug(s.name, "Close")
|
||||
s.client.Close()
|
||||
}
|
||||
|
||||
func NewNatsSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(NatsSink)
|
||||
s.name = fmt.Sprintf("NatsSink(%s)", name)
|
||||
s.flushDelay = 10 * time.Second
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "Error reading config for", s.name, ":", err.Error())
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(s.config.Host) == 0 ||
|
||||
len(s.config.Port) == 0 ||
|
||||
len(s.config.Database) == 0 {
|
||||
return errors.New("not all configuration variables set required by NatsSink")
|
||||
len(s.config.Subject) == 0 {
|
||||
return nil, errors.New("not all configuration variables set required by NatsSink")
|
||||
}
|
||||
// Create lookup map to use meta infos as tags in the output metric
|
||||
s.meta_as_tags = make(map[string]bool)
|
||||
for _, k := range s.config.MetaAsTags {
|
||||
s.meta_as_tags[k] = true
|
||||
}
|
||||
// Setup Influx line protocol
|
||||
s.buffer = &bytes.Buffer{}
|
||||
@@ -74,34 +131,18 @@ func (s *NatsSink) Init(config json.RawMessage) error {
|
||||
s.encoder.SetPrecision(time.Second)
|
||||
s.encoder.SetMaxLineBytes(1024)
|
||||
// Setup infos for connection
|
||||
return s.connect()
|
||||
}
|
||||
if err := s.connect(); err != nil {
|
||||
return nil, fmt.Errorf("unable to connect: %v", err)
|
||||
}
|
||||
|
||||
func (s *NatsSink) Write(m lp.CCMetric) error {
|
||||
if s.client != nil {
|
||||
_, err := s.encoder.Encode(m.ToPoint(s.config.MetaAsTags))
|
||||
s.flushTimer = nil
|
||||
if len(s.config.FlushDelay) != 0 {
|
||||
var err error
|
||||
s.flushDelay, err = time.ParseDuration(s.config.FlushDelay)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "Write:", err.Error())
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NatsSink) Flush() error {
|
||||
if s.client != nil {
|
||||
if err := s.client.Publish(s.config.Database, s.buffer.Bytes()); err != nil {
|
||||
cclog.ComponentError(s.name, "Flush:", err.Error())
|
||||
return err
|
||||
}
|
||||
s.buffer.Reset()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NatsSink) Close() {
|
||||
if s.client != nil {
|
||||
cclog.ComponentDebug(s.name, "Close")
|
||||
s.client.Close()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
199
sinks/prometheusSink.go
Normal file
199
sinks/prometheusSink.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
type PrometheusSinkConfig struct {
|
||||
defaultSinkConfig
|
||||
Host string `json:"host,omitempty"`
|
||||
Port string `json:"port"`
|
||||
Path string `json:"path,omitempty"`
|
||||
GroupAsNameSpace bool `json:"group_as_namespace,omitempty"`
|
||||
// User string `json:"user,omitempty"`
|
||||
// Password string `json:"password,omitempty"`
|
||||
// FlushDelay string `json:"flush_delay,omitempty"`
|
||||
}
|
||||
|
||||
type PrometheusSink struct {
|
||||
sink
|
||||
config PrometheusSinkConfig
|
||||
labelMetrics map[string]*prometheus.GaugeVec
|
||||
nodeMetrics map[string]prometheus.Gauge
|
||||
promWg sync.WaitGroup
|
||||
promServer *http.Server
|
||||
}
|
||||
|
||||
func intToFloat64(input interface{}) (float64, error) {
|
||||
switch value := input.(type) {
|
||||
case float64:
|
||||
return value, nil
|
||||
case float32:
|
||||
return float64(value), nil
|
||||
case int:
|
||||
return float64(value), nil
|
||||
case int32:
|
||||
return float64(value), nil
|
||||
case int64:
|
||||
return float64(value), nil
|
||||
}
|
||||
return 0, errors.New("cannot cast value to float64")
|
||||
}
|
||||
|
||||
func getLabelValue(metric lp.CCMetric) []string {
|
||||
labelValues := []string{}
|
||||
if tid, tidok := metric.GetTag("type-id"); tidok && metric.HasTag("type") {
|
||||
labelValues = append(labelValues, tid)
|
||||
}
|
||||
if d, ok := metric.GetTag("device"); ok {
|
||||
labelValues = append(labelValues, d)
|
||||
} else if d, ok := metric.GetMeta("device"); ok {
|
||||
labelValues = append(labelValues, d)
|
||||
}
|
||||
return labelValues
|
||||
}
|
||||
|
||||
func getLabelNames(metric lp.CCMetric) []string {
|
||||
labelNames := []string{}
|
||||
if t, tok := metric.GetTag("type"); tok && metric.HasTag("type-id") {
|
||||
labelNames = append(labelNames, t)
|
||||
}
|
||||
if _, ok := metric.GetTag("device"); ok {
|
||||
labelNames = append(labelNames, "device")
|
||||
} else if _, ok := metric.GetMeta("device"); ok {
|
||||
labelNames = append(labelNames, "device")
|
||||
}
|
||||
return labelNames
|
||||
}
|
||||
|
||||
func (s *PrometheusSink) newMetric(metric lp.CCMetric) error {
|
||||
var value float64 = 0
|
||||
name := metric.Name()
|
||||
opts := prometheus.GaugeOpts{
|
||||
Name: name,
|
||||
}
|
||||
labels := getLabelNames(metric)
|
||||
labelValues := getLabelValue(metric)
|
||||
if len(labels) > 0 && len(labels) != len(labelValues) {
|
||||
return fmt.Errorf("cannot detect metric labels for metric %s", name)
|
||||
}
|
||||
|
||||
if metricValue, ok := metric.GetField("value"); ok {
|
||||
if floatValue, err := intToFloat64(metricValue); err == nil {
|
||||
value = floatValue
|
||||
} else {
|
||||
return fmt.Errorf("metric %s with value '%v' cannot be casted to float64", name, metricValue)
|
||||
}
|
||||
}
|
||||
if s.config.GroupAsNameSpace && metric.HasMeta("group") {
|
||||
g, _ := metric.GetMeta("group")
|
||||
opts.Namespace = strings.ToLower(g)
|
||||
}
|
||||
|
||||
if len(labels) > 0 {
|
||||
new := prometheus.NewGaugeVec(opts, labels)
|
||||
new.WithLabelValues(labelValues...).Set(value)
|
||||
s.labelMetrics[name] = new
|
||||
prometheus.Register(new)
|
||||
} else {
|
||||
new := prometheus.NewGauge(opts)
|
||||
new.Set(value)
|
||||
s.nodeMetrics[name] = new
|
||||
prometheus.Register(new)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PrometheusSink) updateMetric(metric lp.CCMetric) error {
|
||||
var value float64 = 0.0
|
||||
name := metric.Name()
|
||||
labelValues := getLabelValue(metric)
|
||||
|
||||
if metricValue, ok := metric.GetField("value"); ok {
|
||||
if floatValue, err := intToFloat64(metricValue); err == nil {
|
||||
value = floatValue
|
||||
} else {
|
||||
return fmt.Errorf("metric %s with value '%v' cannot be casted to float64", name, metricValue)
|
||||
}
|
||||
}
|
||||
|
||||
if len(labelValues) > 0 {
|
||||
if _, ok := s.labelMetrics[name]; !ok {
|
||||
err := s.newMetric(metric)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.labelMetrics[name].WithLabelValues(labelValues...).Set(value)
|
||||
} else {
|
||||
if _, ok := s.labelMetrics[name]; !ok {
|
||||
err := s.newMetric(metric)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.nodeMetrics[name].Set(value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PrometheusSink) Write(m lp.CCMetric) error {
|
||||
return s.updateMetric(m)
|
||||
}
|
||||
|
||||
func (s *PrometheusSink) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PrometheusSink) Close() {
|
||||
cclog.ComponentDebug(s.name, "CLOSE")
|
||||
s.promServer.Shutdown(context.Background())
|
||||
s.promWg.Wait()
|
||||
}
|
||||
|
||||
func NewPrometheusSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(PrometheusSink)
|
||||
s.name = "PrometheusSink"
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
cclog.ComponentError(s.name, "Error reading config for", s.name, ":", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(s.config.Port) == 0 {
|
||||
err := errors.New("not all configuration variables set required by PrometheusSink")
|
||||
cclog.ComponentError(s.name, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
s.labelMetrics = make(map[string]*prometheus.GaugeVec)
|
||||
s.nodeMetrics = make(map[string]prometheus.Gauge)
|
||||
s.promWg.Add(1)
|
||||
go func() {
|
||||
router := mux.NewRouter()
|
||||
// Prometheus endpoint
|
||||
router.Path("/" + s.config.Path).Handler(promhttp.Handler())
|
||||
|
||||
url := fmt.Sprintf("%s:%s", s.config.Host, s.config.Port)
|
||||
cclog.ComponentDebug(s.name, "Serving Prometheus metrics at", fmt.Sprintf("%s:%s/%s", s.config.Host, s.config.Port, s.config.Path))
|
||||
s.promServer = &http.Server{Addr: url, Handler: router}
|
||||
err := s.promServer.ListenAndServe()
|
||||
if err != nil && err.Error() != "http: Server closed" {
|
||||
cclog.ComponentError(s.name, err.Error())
|
||||
}
|
||||
s.promWg.Done()
|
||||
}()
|
||||
return s, nil
|
||||
}
|
23
sinks/prometheusSink.md
Normal file
23
sinks/prometheusSink.md
Normal file
@@ -0,0 +1,23 @@
|
||||
## `prometheus` sink
|
||||
|
||||
The `prometheus` sink publishes all metrics via an HTTP server ready to be scraped by a [Prometheus](https://prometheus.io) server. It creates gauge metrics for all node metrics and gauge vectors for all metrics with a subtype like 'device', 'cpu' or 'socket'.
|
||||
|
||||
|
||||
### Configuration structure
|
||||
|
||||
```json
|
||||
{
|
||||
"<name>": {
|
||||
"type": "prometheus",
|
||||
"host": "localhost",
|
||||
"port": "8080",
|
||||
"path": "metrics"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `type`: makes the sink an `prometheus` sink
|
||||
- `host`: The HTTP server gets bound to that IP/hostname
|
||||
- `port`: Portnumber (as string) for the HTTP server
|
||||
- `path`: Path where the metrics should be servered. The metrics will be published at `host`:`port`/`path`
|
||||
- `group_as_namespace`: Most metrics contain a group as meta information like 'memory', 'load'. With this the metric names are extended to `group`_`name` if possible.
|
80
sinks/sampleSink.go
Normal file
80
sinks/sampleSink.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
cclog "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
|
||||
lp "github.com/ClusterCockpit/cc-metric-collector/internal/ccMetric"
|
||||
)
|
||||
|
||||
type SampleSinkConfig struct {
|
||||
// defines JSON tags for 'type' and 'meta_as_tags' (string list)
|
||||
// See: metricSink.go
|
||||
defaultSinkConfig
|
||||
// Additional config options, for SampleSink
|
||||
}
|
||||
|
||||
type SampleSink struct {
|
||||
// declares elements 'name' and 'meta_as_tags' (string to bool map!)
|
||||
sink
|
||||
config SampleSinkConfig // entry point to the SampleSinkConfig
|
||||
}
|
||||
|
||||
// Implement functions required for Sink interface
|
||||
// Write(...), Flush(), Close()
|
||||
// See: metricSink.go
|
||||
|
||||
// Code to submit a single CCMetric to the sink
|
||||
func (s *SampleSink) Write(point lp.CCMetric) error {
|
||||
// based on s.meta_as_tags use meta infos as tags
|
||||
log.Print(point)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the sink uses batched sends internally, you can tell to flush its buffers
|
||||
func (s *SampleSink) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close sink: close network connection, close files, close libraries, ...
|
||||
func (s *SampleSink) Close() {
|
||||
cclog.ComponentDebug(s.name, "CLOSE")
|
||||
}
|
||||
|
||||
// New function to create a new instance of the sink
|
||||
// Initialize the sink by giving it a name and reading in the config JSON
|
||||
func NewSampleSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(SampleSink)
|
||||
|
||||
// Set name of sampleSink
|
||||
// The name should be chosen in such a way that different instances of SampleSink can be distinguished
|
||||
s.name = fmt.Sprintf("SampleSink(%s)", name) // Always specify a name here
|
||||
|
||||
// Set defaults in s.config
|
||||
// Allow overwriting these defaults by reading config JSON
|
||||
|
||||
// Read in the config JSON
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create lookup map to use meta infos as tags in the output metric
|
||||
s.meta_as_tags = make(map[string]bool)
|
||||
for _, k := range s.config.MetaAsTags {
|
||||
s.meta_as_tags[k] = true
|
||||
}
|
||||
|
||||
// Check if all required fields in the config are set
|
||||
// E.g. use 'len(s.config.Option) > 0' for string settings
|
||||
|
||||
// Establish connection to the server, library, ...
|
||||
// Check required files exist and lookup path(s) of executable(s)
|
||||
|
||||
// Return (nil, meaningful error message) in case of errors
|
||||
return s, nil
|
||||
}
|
@@ -13,14 +13,14 @@ import (
|
||||
const SINK_MAX_FORWARD = 50
|
||||
|
||||
// Map of all available sinks
|
||||
var AvailableSinks = map[string]Sink{
|
||||
"influxdb": new(InfluxSink),
|
||||
"stdout": new(StdoutSink),
|
||||
"nats": new(NatsSink),
|
||||
"http": new(HttpSink),
|
||||
"ganglia": new(GangliaSink),
|
||||
"influxasync": new(InfluxAsyncSink),
|
||||
"libganglia": new(LibgangliaSink),
|
||||
var AvailableSinks = map[string]func(name string, config json.RawMessage) (Sink, error){
|
||||
"ganglia": NewGangliaSink,
|
||||
"libganglia": NewLibgangliaSink,
|
||||
"stdout": NewStdoutSink,
|
||||
"nats": NewNatsSink,
|
||||
"influxdb": NewInfluxSink,
|
||||
"influxasync": NewInfluxAsyncSink,
|
||||
"http": NewHttpSink,
|
||||
}
|
||||
|
||||
// Metric collector manager data structure
|
||||
@@ -149,8 +149,7 @@ func (sm *sinkManager) AddOutput(name string, rawConfig json.RawMessage) error {
|
||||
cclog.ComponentError("SinkManager", "SKIP", name, "unknown sink:", sinkConfig.Type)
|
||||
return err
|
||||
}
|
||||
s := AvailableSinks[sinkConfig.Type]
|
||||
err = s.Init(rawConfig)
|
||||
s, err := AvailableSinks[sinkConfig.Type](name, rawConfig)
|
||||
if err != nil {
|
||||
cclog.ComponentError("SinkManager", "SKIP", s.Name(), "initialization failed:", err.Error())
|
||||
return err
|
||||
|
@@ -19,34 +19,6 @@ type StdoutSink struct {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StdoutSink) Init(config json.RawMessage) error {
|
||||
s.name = "StdoutSink"
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
s.output = os.Stdout
|
||||
if len(s.config.Output) > 0 {
|
||||
switch strings.ToLower(s.config.Output) {
|
||||
case "stdout":
|
||||
s.output = os.Stdout
|
||||
case "stderr":
|
||||
s.output = os.Stderr
|
||||
default:
|
||||
f, err := os.OpenFile(s.config.Output, os.O_CREATE|os.O_WRONLY, os.FileMode(0600))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.output = f
|
||||
}
|
||||
}
|
||||
s.meta_as_tags = s.config.MetaAsTags
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StdoutSink) Write(m lp.CCMetric) error {
|
||||
fmt.Fprint(
|
||||
s.output,
|
||||
@@ -65,3 +37,37 @@ func (s *StdoutSink) Close() {
|
||||
s.output.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func NewStdoutSink(name string, config json.RawMessage) (Sink, error) {
|
||||
s := new(StdoutSink)
|
||||
s.name = fmt.Sprintf("StdoutSink(%s)", name)
|
||||
if len(config) > 0 {
|
||||
err := json.Unmarshal(config, &s.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.output = os.Stdout
|
||||
if len(s.config.Output) > 0 {
|
||||
switch strings.ToLower(s.config.Output) {
|
||||
case "stdout":
|
||||
s.output = os.Stdout
|
||||
case "stderr":
|
||||
s.output = os.Stderr
|
||||
default:
|
||||
f, err := os.OpenFile(s.config.Output, os.O_CREATE|os.O_WRONLY, os.FileMode(0600))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.output = f
|
||||
}
|
||||
}
|
||||
// Create lookup map to use meta infos as tags in the output metric
|
||||
s.meta_as_tags = make(map[string]bool)
|
||||
for _, k := range s.config.MetaAsTags {
|
||||
s.meta_as_tags[k] = true
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user