chore: consolidate CI and expand Windows test coverage (#2498)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-06 17:47:21 +02:00
committed by GitHub
parent 824fdb1d00
commit 72e10af92d
41 changed files with 2576 additions and 794 deletions
+2
View File
@@ -0,0 +1,2 @@
[codespell]
ignore-words-list = notin
+5
View File
@@ -0,0 +1,5 @@
rules:
cache-poisoning:
disable: true
secrets-outside-env:
disable: true
File diff suppressed because it is too large Load Diff
-98
View File
@@ -1,98 +0,0 @@
name: Linting
# Trigger on pull requests and pushes to master branch where Go-related files
# have been changed.
on:
push:
branches:
- master
- next
- main
- "0.*"
- "1.*"
pull_request:
env:
VERSION_PROMU: '0.14.0'
PROMTOOL_VER: '2.43.0'
jobs:
test:
runs-on: windows-2025
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
go-version: '1.27.1'
- name: Test
run: make test
- name: Install e2e deps
run: |
Invoke-WebRequest -Uri https://github.com/prometheus/promu/releases/download/v$($Env:VERSION_PROMU)/promu-$($Env:VERSION_PROMU).windows-amd64.zip -OutFile promu-$($Env:VERSION_PROMU).windows-amd64.zip
Expand-Archive -Path promu-$($Env:VERSION_PROMU).windows-amd64.zip -DestinationPath .
Copy-Item -Path promu-$($Env:VERSION_PROMU).windows-amd64\promu.exe -Destination "$(go env GOPATH)\bin"
# GOPATH\bin dir must be appended to PATH else the `promu` command won't be found
echo "$(go env GOPATH)\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: e2e Test
run: make e2e-test
promtool:
runs-on: windows-2025
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
go-version: '1.27.1'
- name: Install promtool
run: |
Invoke-WebRequest -Uri https://github.com/prometheus/prometheus/releases/download/v$($Env:PROMTOOL_VER)/prometheus-$($Env:PROMTOOL_VER).windows-amd64.zip -OutFile prometheus-$($Env:PROMTOOL_VER).windows-amd64.zip
Expand-Archive -Path prometheus-$($Env:PROMTOOL_VER).windows-amd64.zip -DestinationPath .
Copy-Item -Path prometheus-$($Env:PROMTOOL_VER).windows-amd64\promtool.exe -Destination "$(go env GOPATH)\bin"
Invoke-WebRequest -Uri https://github.com/prometheus/promu/releases/download/v$($Env:VERSION_PROMU)/promu-$($Env:VERSION_PROMU).windows-amd64.zip -OutFile promu-$($Env:VERSION_PROMU).windows-amd64.zip
Expand-Archive -Path promu-$($Env:VERSION_PROMU).windows-amd64.zip -DestinationPath .
Copy-Item -Path promu-$($Env:VERSION_PROMU).windows-amd64\promu.exe -Destination "$(go env GOPATH)\bin"
# GOPATH\bin dir must be appended to PATH else the `promu` command won't be found
echo "$(go env GOPATH)\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Promtool
run: make promtool
- name: Upload windows_exporter.exe
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: windows_exporter.amd64.exe
path: 'windows_exporter.exe'
retention-days: 7
if-no-files-found: error
lint:
runs-on: windows-2025
steps:
# `gofmt` linter run by golangci-lint fails on CRLF line endings (the default for Windows)
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
go-version: '1.27.1'
- name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
# renovate: github=golangci/golangci-lint
version: v2.13.2
args: "--max-same-issues=0"
+9 -2
View File
@@ -8,10 +8,12 @@ on:
- labeled
- unlabeled
permissions: {}
jobs:
required-labels-missing:
name: required labels missing
runs-on: ubuntu-latest
runs-on: ubuntu-slim
steps:
- name: check
if: >-
@@ -31,9 +33,14 @@ jobs:
&& exit 1
title:
name: check title prefix
runs-on: ubuntu-latest
runs-on: ubuntu-slim
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: 'false'
- name: check
run: |
PR_TITLE_PREFIX=$(echo "$PR_TITLE" | cut -d':' -f1)
-244
View File
@@ -1,244 +0,0 @@
name: Releases
# Trigger on releases.
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
release:
types:
- published
permissions:
contents: write
packages: write
env:
VERSION_PROMU: '0.17.0'
jobs:
build:
runs-on: windows-2025
environment: build
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: '0'
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
go-version: '1.27.1'
- name: Install WiX
run: |
dotnet tool install --global wix --version 5.0.2
- name: Install WiX extensions
run: |
wix extension add -g WixToolset.Util.wixext/5.0.2
wix extension add -g WixToolset.Ui.wixext/5.0.2
wix extension add -g WixToolset.Firewall.wixext/5.0.2
- name: Install Build deps
run: |
Invoke-WebRequest -Uri https://github.com/prometheus/promu/releases/download/v$($Env:VERSION_PROMU)/promu-$($Env:VERSION_PROMU).windows-amd64.zip -OutFile promu-$($Env:VERSION_PROMU).windows-amd64.zip
Expand-Archive -Path promu-$($Env:VERSION_PROMU).windows-amd64.zip -DestinationPath .
Copy-Item -Path promu-$($Env:VERSION_PROMU).windows-amd64\promu.exe -Destination "$(go env GOPATH)\bin"
# GOPATH\bin dir must be added to PATH else the `promu` commands won't be found
echo "$(go env GOPATH)\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Build
run: |
$ErrorActionPreference = "Stop"
$Version = git describe --tags --always
$Version = $Version -replace 'v', ''
# '+' symbols are invalid characters in image tags
$Version = $Version -replace '\+', '_'
$Version | Set-Content VERSION -PassThru
make build-all
# GH requires all files to have different names, so add version/arch to differentiate
foreach($Arch in "amd64", "arm64") {
Move-Item output\$Arch\windows_exporter.exe output\windows_exporter-$Version-$Arch.exe
}
Get-ChildItem -Path output
- name: Sign build artifacts
if: ${{ (github.event_name != 'pull_request' && github.repository == 'prometheus-community/windows_exporter') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'prometheus-community/windows_exporter') }}
run: |
$ErrorActionPreference = "Stop"
$Version = Get-Content VERSION
$b64 = $env:CODE_SIGN_KEY
$filename = 'windows_exporter_CodeSign.pfx'
$bytes = [Convert]::FromBase64String($b64)
[IO.File]::WriteAllBytes($filename, $bytes)
$basePath = "C:\Program Files (x86)\Windows Kits\10\bin"
$latestSigntool = Get-ChildItem -Path $basePath -Directory |
Where-Object { $_.Name -match "^\d+\.\d+\.\d+\.\d+$" } |
Sort-Object { [Version]$_.Name } -Descending |
Select-Object -First 1 |
ForEach-Object { Join-Path $_.FullName "x64\signtool.exe" }
if (Test-Path $latestSigntool) {
Write-Output $latestSigntool
} else {
Write-Output "signtool.exe not found"
}
foreach($Arch in "amd64", "arm64") {
& $latestSigntool sign /v /tr "http://timestamp.digicert.com" /d "Prometheus exporter for Windows machines" /td SHA256 /fd SHA256 /a /f "windows_exporter_CodeSign.pfx" /p $env:CODE_SIGN_PASSWORD "output\windows_exporter-$Version-$Arch.exe"
}
rm windows_exporter_CodeSign.pfx
env:
CODE_SIGN_KEY: ${{ secrets.CODE_SIGN_KEY }}
CODE_SIGN_PASSWORD: ${{ secrets.CODE_SIGN_PASSWORD }}
- name: Build Release Artifacts
run: |
$ErrorActionPreference = "Stop"
$Version = Get-Content VERSION
foreach($Arch in "amd64", "arm64") {
Write-Host "Building windows_exporter $Version msi for $Arch"
.\installer\build.ps1 -PathToExecutable .\output\windows_exporter-$Version-$Arch.exe -Version $Version -Arch "$Arch"
}
Move-Item installer\*.msi output\
Get-ChildItem -Path output\ g
- name: Sign installer artifacts
if: ${{ (github.event_name != 'pull_request' && github.repository == 'prometheus-community/windows_exporter') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'prometheus-community/windows_exporter') }}
run: |
$ErrorActionPreference = "Stop"
$Version = Get-Content VERSION
$b64 = $env:CODE_SIGN_KEY
$filename = 'windows_exporter_CodeSign.pfx'
$bytes = [Convert]::FromBase64String($b64)
[IO.File]::WriteAllBytes($filename, $bytes)
$basePath = "C:\Program Files (x86)\Windows Kits\10\bin"
$latestSigntool = Get-ChildItem -Path $basePath -Directory |
Where-Object { $_.Name -match "^\d+\.\d+\.\d+\.\d+$" } |
Sort-Object { [Version]$_.Name } -Descending |
Select-Object -First 1 |
ForEach-Object { Join-Path $_.FullName "x64\signtool.exe" }
if (Test-Path $latestSigntool) {
Write-Output $latestSigntool
} else {
Write-Output "signtool.exe not found"
}
foreach($Arch in "amd64", "arm64") {
& $latestSigntool sign /v /tr "http://timestamp.digicert.com" /d "Prometheus exporter for Windows machines" /td SHA256 /fd SHA256 /a /f "windows_exporter_CodeSign.pfx" /p $env:CODE_SIGN_PASSWORD "output\windows_exporter-$Version-$Arch.msi"
}
rm windows_exporter_CodeSign.pfx
env:
CODE_SIGN_KEY: ${{ secrets.CODE_SIGN_KEY }}
CODE_SIGN_PASSWORD: ${{ secrets.CODE_SIGN_PASSWORD }}
- name: Generate checksums
run: |
promu checksum output
cat output\sha256sums.txt
- name: Upload Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows_exporter_binaries
path: |
output\windows_exporter-*.exe
output\windows_exporter-*.msi
- name: Release
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$TagName = $env:GITHUB_REF -replace 'refs/tags/', ''
Get-ChildItem -Path output\* -Include @('windows_exporter*.msi', 'windows_exporter*.exe', 'sha256sums.txt') | Foreach-Object {gh release upload $TagName $_}
docker:
name: Build docker images
runs-on: ubuntu-latest
needs:
- build
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: '0'
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: windows_exporter_binaries
- name: Login to Docker Hub
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKER_HUB_LOGIN }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Login to quay.io
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: quay.io
username: ${{ secrets.QUAY_IO_LOGIN }}
password: ${{ secrets.QUAY_IO_PASSWORD }}
- name: Login to GitHub container registry
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
ghcr.io/prometheus-community/windows-exporter
docker.io/prometheuscommunity/windows-exporter
quay.io/prometheuscommunity/windows-exporter
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
labels: |
org.opencontainers.image.title=windows_exporter
org.opencontainers.image.description=A Prometheus exporter for Windows machines.
org.opencontainers.image.vendor=The Prometheus Community
org.opencontainers.image.licenses=MIT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: windows/amd64
annotations: ${{ steps.meta.outputs.labels }}
-26
View File
@@ -1,26 +0,0 @@
name: Spell checking
# Trigger on pull requests, and pushes to master branch.
on:
push:
branches:
- master
pull_request:
branches:
- master
env:
VERSION_PROMU: 'v0.14.0'
jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: codespell-project/actions-codespell@e3258cca84ce02b0bb36a3d41f2c18f719a0cc1a # master
with:
check_filenames: true
# When using this Action in other repos, the --skip option below can be removed
skip: ./.git,go.mod,go.sum
ignore_words_list: calle,Entires
+1
View File
@@ -3,6 +3,7 @@ VERSION
*.swp
*.un~
output/
dist/
.vscode
*.syso
installer/*.msi
+28
View File
@@ -0,0 +1,28 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
builds:
- id: windows_exporter
main: ./cmd/windows_exporter
binary: windows_exporter-{{ .Arch }}
no_unique_dist_dir: true
goos:
- windows
goarch:
- amd64
- arm64
env:
- CGO_ENABLED=0
flags:
- -trimpath
tags:
- trimpath
ldflags:
- >-
-X github.com/prometheus/common/version.Version={{ .Version }}
-X github.com/prometheus/common/version.Revision={{ .FullCommit }}
-X github.com/prometheus/common/version.Branch={{ .Branch }}
-X github.com/prometheus/common/version.BuildUser=github-actions
-X github.com/prometheus/common/version.BuildDate={{ .Now.Format "20060102-15:04:05" }}
mod_timestamp: '{{ .CommitTimestamp }}'
-27
View File
@@ -1,27 +0,0 @@
go:
# Whenever the Go version is updated here,
# .github/workflows should also be updated.
version: 1.23
cgo: false
repository:
path: github.com/prometheus-community/windows_exporter
build:
binaries:
- name: windows_exporter
path: ./cmd/windows_exporter
tags:
all:
- trimpath
ldflags: |
-X github.com/prometheus/common/version.Version={{.Version}}
-X github.com/prometheus/common/version.Revision={{.Revision}}
-X github.com/prometheus/common/version.Branch={{.Branch}}
-X github.com/prometheus/common/version.BuildUser={{user}}@{{host}}
-X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}}
tarball:
files:
- LICENSE
crossbuild:
platforms:
- windows/amd64
- windows/arm64
+6 -11
View File
@@ -16,10 +16,10 @@ ALL_OS ?= ltsc2019 ltsc2022
BASE_IMAGE ?= mcr.microsoft.com/windows/nanoserver
.PHONY: build
build: generate windows_exporter.exe
build: windows_exporter.exe
windows_exporter.exe: pkg/**/*.go
promu build -v
windows_exporter.exe: generate
CGO_ENABLED=0 go build -trimpath -tags=trimpath -o $@ ./cmd/windows_exporter
.PHONY: generate
generate:
@@ -38,18 +38,13 @@ lint:
e2e-test: windows_exporter.exe
powershell -NonInteractive -ExecutionPolicy Bypass -File .\tools\end-to-end-test.ps1
.PHONY: promtool
promtool: windows_exporter.exe
pwsh -NonInteractive -ExecutionPolicy Bypass -File .\tools\promtool.ps1
fmt:
gofmt -l -w -s .
crossbuild: generate
# The prometheus/golang-builder image for promu crossbuild doesn't exist
# on Windows, so for now, we'll just build twice
GOARCH=amd64 promu build --prefix=output/amd64
GOARCH=arm64 promu build --prefix=output/arm64
mkdir -p output/amd64 output/arm64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -tags=trimpath -o output/amd64/windows_exporter.exe ./cmd/windows_exporter
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -trimpath -tags=trimpath -o output/arm64/windows_exporter.exe ./cmd/windows_exporter
.PHONY: package
package: crossbuild
+6 -27
View File
@@ -96,13 +96,8 @@ func TestRun(t *testing.T) {
exitCodeCh := make(chan int)
var stdout string
go func() {
stdout = captureOutput(t, func() {
// Simulate the service control manager signaling that we are done.
exitCodeCh <- run(ctx, tc.args)
})
exitCodeCh <- run(ctx, tc.args)
}()
t.Cleanup(func() {
@@ -122,13 +117,13 @@ func TestRun(t *testing.T) {
require.NoError(t, err)
err = waitUntilListening(t, "tcp", uri.Host)
require.NoError(t, err, "LOGS:\n%s", stdout)
require.NoError(t, err)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, tc.metricsEndpoint, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "LOGS:\n%s", stdout)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
@@ -145,24 +140,6 @@ func TestRun(t *testing.T) {
}
}
func captureOutput(tb testing.TB, f func()) string {
tb.Helper()
orig := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
os.Stdout = orig
_ = w.Close()
out, _ := io.ReadAll(r)
return string(out)
}
func waitUntilListening(tb testing.TB, network, address string) error {
tb.Helper()
@@ -173,7 +150,9 @@ func waitUntilListening(tb testing.TB, network, address string) error {
dialer := &net.Dialer{Timeout: 100 * time.Millisecond}
for range 20 {
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
conn, err = dialer.DialContext(tb.Context(), network, address)
if err == nil {
_ = conn.Close()
+20 -12
View File
@@ -85,6 +85,19 @@ func (c *Collector) Close() error {
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.buildDescriptors()
var err error
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](logger.With(slog.String("collector", Name)), pdh.CounterTypeRaw, "Certification Authority", pdh.InstancesAll)
if err != nil {
return fmt.Errorf("failed to create Certification Authority collector: %w", err)
}
return nil
}
func (c *Collector) buildDescriptors() {
c.requestsPerSecond = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "requests_total"),
"Total certificate requests processed",
@@ -163,15 +176,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
[]string{"cert_template"},
nil,
)
var err error
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](logger.With(slog.String("collector", Name)), pdh.CounterTypeRaw, "Certification Authority", pdh.InstancesAll)
if err != nil {
return fmt.Errorf("failed to create Certification Authority collector: %w", err)
}
return nil
}
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
@@ -180,7 +184,13 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
return fmt.Errorf("failed to collect Certification Authority (ADCS) metrics: %w", err)
}
for _, data := range c.perfDataObject {
c.collectMetrics(ch, c.perfDataObject)
return nil
}
func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, rows []perfDataCounterValues) {
for _, data := range rows {
ch <- prometheus.MustNewConstMetric(
c.requestsPerSecond,
prometheus.CounterValue,
@@ -272,6 +282,4 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
data.Name,
)
}
return nil
}
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package adcs
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
func TestCollectMetrics(t *testing.T) {
t.Parallel()
values := perfDataCounterValues{
Name: "WebServer",
RequestsPerSecond: 17,
RequestProcessingTime: 1500,
RetrievalsPerSecond: 19,
RetrievalProcessingTime: 2250,
FailedRequestsPerSecond: 3,
IssuedRequestsPerSecond: 11,
PendingRequestsPerSecond: 2,
RequestCryptographicSigningTime: 500,
RequestPolicyModuleProcessingTime: 750,
ChallengeResponsesPerSecond: 13,
ChallengeResponseProcessingTime: 1250,
SignedCertificateTimestampListsPerSecond: 23,
SignedCertificateTimestampListProcessingTime: 1750,
}
expected := map[string]struct {
metricType dto.MetricType
value float64
}{
"windows_adcs_requests_total": {metricType: dto.MetricType_COUNTER, value: 17},
"windows_adcs_request_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 1.5},
"windows_adcs_retrievals_total": {metricType: dto.MetricType_COUNTER, value: 19},
"windows_adcs_retrievals_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 2.25},
"windows_adcs_failed_requests_total": {metricType: dto.MetricType_COUNTER, value: 3},
"windows_adcs_issued_requests_total": {metricType: dto.MetricType_COUNTER, value: 11},
"windows_adcs_pending_requests_total": {metricType: dto.MetricType_COUNTER, value: 2},
"windows_adcs_request_cryptographic_signing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 0.5},
"windows_adcs_request_policy_module_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 0.75},
"windows_adcs_challenge_responses_total": {metricType: dto.MetricType_COUNTER, value: 13},
"windows_adcs_challenge_response_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 1.25},
"windows_adcs_signed_certificate_timestamp_lists_total": {metricType: dto.MetricType_COUNTER, value: 23},
"windows_adcs_signed_certificate_timestamp_list_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 1.75},
}
for _, tc := range []struct {
name string
rows []perfDataCounterValues
}{
{name: "no templates"},
{name: "zero counters", rows: []perfDataCounterValues{{Name: "ZeroTemplate"}}},
{name: "independent templates", rows: []perfDataCounterValues{values, {Name: "ZeroTemplate"}}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
c := New(nil)
c.buildDescriptors()
registry := prometheus.NewPedanticRegistry()
require.NoError(t, registry.Register(metricCollector{collector: c, rows: tc.rows}))
families, err := registry.Gather()
require.NoError(t, err)
if len(tc.rows) == 0 {
require.Empty(t, families)
return
}
require.Len(t, families, len(expected))
for _, family := range families {
want, ok := expected[family.GetName()]
require.True(t, ok, "unexpected metric %s", family.GetName())
require.Equal(t, want.metricType, family.GetType())
require.Len(t, family.GetMetric(), len(tc.rows))
templates := make(map[string]bool, len(tc.rows))
for _, metric := range family.GetMetric() {
require.Len(t, metric.GetLabel(), 1)
label := metric.GetLabel()[0]
require.Equal(t, "cert_template", label.GetName())
require.False(t, templates[label.GetValue()], "duplicate template sample")
templates[label.GetValue()] = true
expectedValue := 0.0
if label.GetValue() == values.Name {
expectedValue = want.value
} else {
require.Equal(t, "ZeroTemplate", label.GetValue())
}
actual := metric.GetGauge().GetValue()
if want.metricType == dto.MetricType_COUNTER {
actual = metric.GetCounter().GetValue()
}
require.InDelta(t, expectedValue, actual, 0, "%s for template %s", family.GetName(), label.GetValue())
}
}
})
}
}
type metricCollector struct {
collector *Collector
rows []perfDataCounterValues
}
func (c metricCollector) Describe(ch chan<- *prometheus.Desc) {
prometheus.DescribeByCollect(c, ch)
}
func (c metricCollector) Collect(ch chan<- prometheus.Metric) {
c.collector.collectMetrics(ch, c.rows)
}
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/container"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,7 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, container.New, nil)
metrics := testutils.TestCollector(t, container.New, nil)
testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_available", prometheus.Labels{"container": "hostprocess", "hostprocess": "true"})
testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_available", prometheus.Labels{"container": "nanoserver", "hostprocess": "false"})
}
+3 -1
View File
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/dhcp"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, dhcp.New, nil)
metrics := testutils.TestCollector(t, dhcp.New, nil)
testutils.RequireFixtureMetric(t, metrics, dhcp.Name, "windows_dhcp_scope_info", prometheus.Labels{"name": "GitHubActions", "scope": "192.0.2.0/24"})
}
+49 -2
View File
@@ -18,10 +18,13 @@
package file_test
import (
"os"
"path/filepath"
"testing"
"github.com/prometheus-community/windows_exporter/internal/collector/file"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/stretchr/testify/require"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,7 +32,51 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, file.New, &file.Config{
FilePatterns: []string{"*.*"},
path := filepath.Join(t.TempDir(), "test.txt")
content := []byte("windows_exporter test")
require.NoError(t, os.WriteFile(path, content, 0o600))
metrics := testutils.TestCollector(t, file.New, &file.Config{
FilePatterns: []string{path},
})
require.Contains(t, metrics, "windows_file_size_bytes")
require.Len(t, metrics["windows_file_size_bytes"].GetMetric(), 1)
require.InDelta(t, len(content), metrics["windows_file_size_bytes"].GetMetric()[0].GetGauge().GetValue(), 0)
require.Contains(t, metrics, "windows_file_mtime_timestamp_seconds")
require.Positive(t, metrics["windows_file_mtime_timestamp_seconds"].GetMetric()[0].GetGauge().GetValue())
}
func TestCollectorPatterns(t *testing.T) {
t.Parallel()
directory := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(directory, "first.txt"), []byte("first"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(directory, "second.log"), []byte("second"), 0o600))
for _, tc := range []struct {
name string
pattern string
count int
}{
{name: "all files", pattern: "*", count: 2},
{name: "extension", pattern: "*.txt", count: 1},
{name: "case insensitive", pattern: "FIRST.TXT", count: 1},
{name: "missing file", pattern: "missing.txt", count: 0},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
metrics := testutils.TestCollector(t, file.New, &file.Config{
FilePatterns: []string{filepath.Join(directory, tc.pattern)},
})
if tc.count == 0 {
require.Empty(t, metrics)
return
}
require.Contains(t, metrics, "windows_file_size_bytes")
require.Len(t, metrics["windows_file_size_bytes"].GetMetric(), tc.count)
})
}
}
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/fsrmquota"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, fsrmquota.New, nil)
metrics := testutils.TestCollector(t, fsrmquota.New, nil)
testutils.RequireFixtureMetric(t, metrics, fsrmquota.Name, "windows_fsrmquota_size_bytes", prometheus.Labels{"path": `C:\fsrm-ci`})
}
+3 -1
View File
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/hyperv"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, hyperv.New, nil)
metrics := testutils.TestCollector(t, hyperv.New, nil)
testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_dynamic_memory_vm_physical_bytes", prometheus.Labels{"vm": "GitHubActions"})
}
+3 -1
View File
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/iis"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, iis.New, nil)
metrics := testutils.TestCollector(t, iis.New, nil)
testutils.RequireFixtureMetric(t, metrics, iis.Name, "windows_iis_requests_total", prometheus.Labels{"site": "Default Web Site", "method": "get"})
}
@@ -18,12 +18,15 @@
package logical_disk_test
import (
"os"
"regexp"
"testing"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/collector/logical_disk"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/stretchr/testify/require"
)
func BenchmarkCollector(b *testing.B) {
@@ -37,6 +40,46 @@ func BenchmarkCollector(b *testing.B) {
func TestCollector(t *testing.T) {
testutils.TestCollector(t, logical_disk.New, &logical_disk.Config{
VolumeInclude: types.RegExpAny,
CollectorsEnabled: logical_disk.ConfigDefaults.CollectorsEnabled,
VolumeInclude: types.RegExpAny,
})
}
func TestCollectorVolumeFilters(t *testing.T) {
t.Parallel()
systemDrive := os.Getenv("SystemDrive")
require.NotEmpty(t, systemDrive)
matchDrive := regexp.MustCompile("^" + regexp.QuoteMeta(systemDrive) + "$")
for _, tc := range []struct {
name string
config logical_disk.Config
included bool
}{
{name: "include system drive", config: logical_disk.Config{CollectorsEnabled: logical_disk.ConfigDefaults.CollectorsEnabled, VolumeInclude: matchDrive}, included: true},
{name: "exclude system drive", config: logical_disk.Config{CollectorsEnabled: logical_disk.ConfigDefaults.CollectorsEnabled, VolumeInclude: types.RegExpAny, VolumeExclude: matchDrive}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
metrics := testutils.TestCollector(t, logical_disk.New, &tc.config)
if tc.included {
require.Contains(t, metrics, "windows_logical_disk_size_bytes")
require.Len(t, metrics["windows_logical_disk_size_bytes"].GetMetric(), 1)
}
for _, metric := range metrics["windows_logical_disk_size_bytes"].GetMetric() {
for _, label := range metric.GetLabel() {
if label.GetName() == "volume" {
if tc.included {
require.Equal(t, systemDrive, label.GetValue())
} else {
require.NotEqual(t, systemDrive, label.GetValue())
}
}
}
}
})
}
}
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/mscluster"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, mscluster.New, nil)
metrics := testutils.TestCollector(t, mscluster.New, nil)
testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_virtualdisk_size_bytes", prometheus.Labels{"name": "CIVirtualDisk"})
}
@@ -30,7 +30,5 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
t.Skip("Skipping test as it requires WMI data")
testutils.TestCollector(t, netframework.New, nil)
}
-2
View File
@@ -29,7 +29,5 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
t.Skip()
testutils.TestCollector(t, os.New, nil)
}
@@ -119,7 +119,7 @@ func (c *Collector) GetName() string {
}
func (c *Collector) Close() error {
for _, object := range c.config.Objects {
for _, object := range c.objects {
object.collector.Close()
}
@@ -188,6 +188,8 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
},
})
t.Cleanup(func() { require.NoError(t, perfDataCollector.Close()) })
logger := slog.New(slog.DiscardHandler)
err := perfDataCollector.Build(logger, nil)
@@ -203,7 +205,8 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
registry.MustRegister(collectorAdapter{*perfDataCollector})
rw := httptest.NewRecorder()
promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(rw, &http.Request{})
promhttp.HandlerFor(registry, promhttp.HandlerOpts{}).ServeHTTP(rw, &http.Request{})
require.Equal(t, http.StatusOK, rw.Code)
got := rw.Body.String()
require.NotEmpty(t, got)
@@ -212,3 +215,23 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
})
}
}
func TestCollectorClose(t *testing.T) {
t.Parallel()
c := performancecounter.New(&performancecounter.Config{
Objects: []performancecounter.Object{{
Name: "memory", Object: "Memory",
Counters: []performancecounter.Counter{{Name: "Available Bytes", Type: "gauge"}},
}},
})
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil))
t.Cleanup(func() { require.NoError(t, c.Close()) })
metrics := make(chan prometheus.Metric, 10)
require.NoError(t, c.Collect(metrics, 0))
require.NoError(t, c.Close())
// A closed collector must no longer hold usable native counter queries.
require.ErrorIs(t, c.Collect(metrics, 0), pdh.ErrPerformanceCounterNotInitialized)
}
+3 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/collector/printer"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -35,5 +36,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, printer.New, nil)
metrics := testutils.TestCollector(t, printer.New, nil)
testutils.RequireFixtureMetric(t, metrics, printer.Name, "windows_printer_job_count", prometheus.Labels{"printer": "CIPrinter"})
}
+3 -1
View File
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/smb"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, smb.New, nil)
metrics := testutils.TestCollector(t, smb.New, nil)
testutils.RequireFixtureMetric(t, metrics, smb.Name, "windows_smb_server_shares_received_bytes_total", prometheus.Labels{"share": `\\*\CIShare`})
}
@@ -22,6 +22,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/smbclient"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
)
func BenchmarkCollector(b *testing.B) {
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
}
func TestCollector(t *testing.T) {
testutils.TestCollector(t, smbclient.New, nil)
metrics := testutils.TestCollector(t, smbclient.New, nil)
testutils.RequireFixtureMetric(t, metrics, smbclient.Name, "windows_smbclient_data_queue_seconds_total", prometheus.Labels{"share": "CIShare"})
}
+16 -17
View File
@@ -111,17 +111,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger.Info("update collector is in an experimental state! The configuration and metrics may change in future. Please report any issues.")
ctx, cancel := context.WithCancel(context.Background())
initErrCh := make(chan error, 1)
go c.scheduleUpdateStatus(ctx, logger, initErrCh, c.config.Online)
c.ctxCancelFn = cancel
if err := <-initErrCh; err != nil {
return fmt.Errorf("failed to initialize Windows Update collector: %w", err)
}
c.pendingUpdate = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "pending_info"),
"Expose information for a single pending update item",
@@ -150,6 +139,17 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
nil,
)
ctx, cancel := context.WithCancel(context.Background())
initErrCh := make(chan error, 1)
go c.scheduleUpdateStatus(ctx, logger, initErrCh, c.config.Online)
c.ctxCancelFn = cancel
if err := <-initErrCh; err != nil {
return fmt.Errorf("failed to initialize Windows Update collector: %w", err)
}
return nil
}
@@ -279,14 +279,13 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge
c.mu.Lock()
c.metricsBuf = nil
c.mu.Unlock()
continue
} else {
c.mu.Lock()
c.metricsBuf = metricsBuf
c.mu.Unlock()
}
c.mu.Lock()
c.metricsBuf = metricsBuf
c.mu.Unlock()
// Failed searches also observe the interval and cancellation.
select {
case <-time.After(c.config.ScrapeInterval):
case <-ctx.Done():
+3 -3
View File
@@ -104,12 +104,12 @@ func ParseConfigFile(args []string) string {
return configFile
}
if strings.HasSuffix(cliFlag, "-config.file") {
if len(os.Args) <= i+1 {
if cliFlag == "--config.file" || cliFlag == "-config.file" {
if len(args) <= i+1 {
return ""
}
return os.Args[i+1]
return args[i+1]
}
}
+130
View File
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package config_test
import (
"os"
"path/filepath"
"testing"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/config"
"github.com/stretchr/testify/require"
)
func TestParseConfigFile(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
args []string
want string
}{
{name: "absent"},
{name: "long joined", args: []string{"--config.file=test.yaml"}, want: "test.yaml"},
{name: "short joined", args: []string{"-config.file=test.yaml"}, want: "test.yaml"},
{name: "long separate", args: []string{"--config.file", "test.yaml"}, want: "test.yaml"},
{name: "short separate", args: []string{"-config.file", "test.yaml"}, want: "test.yaml"},
{name: "after other flags", args: []string{"--log.level=debug", "--config.file", "test.yaml"}, want: "test.yaml"},
{name: "missing value", args: []string{"--config.file"}},
{name: "empty value", args: []string{"--config.file="}},
{name: "unrelated suffix", args: []string{"--other-config.file", "test.yaml"}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tc.want, config.ParseConfigFile(tc.args))
})
}
}
func TestNewConfigFileResolver(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
contents string
errorText string
}{
{name: "empty"},
{name: "valid", contents: "log:\n level: debug\nweb:\n listen-address: ['127.0.0.1:9182', '[::1]:9182']\n"},
{name: "unknown key", contents: "unknown: value", errorText: "configuration file validation error"},
{name: "unknown collector option", contents: "collector:\n cpu:\n unknown: true\n", errorText: "configuration file validation error"},
{name: "malformed YAML", contents: "log: [", errorText: "configuration file validation error"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "config.yaml")
require.NoError(t, os.WriteFile(path, []byte(tc.contents), 0o600))
resolver, err := config.NewConfigFileResolver(path)
if tc.errorText != "" {
require.ErrorContains(t, err, tc.errorText)
return
}
require.NoError(t, err)
require.NotNil(t, resolver)
})
}
t.Run("missing file", func(t *testing.T) {
t.Parallel()
_, err := config.NewConfigFileResolver(filepath.Join(t.TempDir(), "missing.yaml"))
require.ErrorContains(t, err, "failed to open configuration file")
})
}
func TestParse(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
extraArgs []string
want string
errorText string
}{
{name: "config defaults", want: "debug"},
{name: "CLI overrides config", extraArgs: []string{"--log.level=error"}, want: "error"},
{name: "invalid CLI flag", extraArgs: []string{"--unknown"}, errorText: "failed to bind configuration"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "config.yaml")
require.NoError(t, os.WriteFile(path, []byte("log:\n level: debug\n"), 0o600))
app := kingpin.New("test", "test")
app.Flag("config.file", "config").String()
level := app.Flag("log.level", "level").Default("info").String()
args := append([]string{"--config.file=" + path}, tc.extraArgs...)
err := config.Parse(app, args)
if tc.errorText != "" {
require.ErrorContains(t, err, tc.errorText)
return
}
require.NoError(t, err)
require.Equal(t, tc.want, *level)
})
}
}
+37
View File
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package httphandler_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/prometheus-community/windows_exporter/internal/httphandler"
"github.com/stretchr/testify/require"
)
func TestHealthHandler(t *testing.T) {
t.Parallel()
response := httptest.NewRecorder()
httphandler.NewHealthHandler().ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/health", nil))
require.Equal(t, http.StatusOK, response.Code)
require.Equal(t, "application/json", response.Header().Get("Content-Type"))
require.JSONEq(t, `{"status":"ok"}`, response.Body.String())
}
+181
View File
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package httphandler_test
import (
"errors"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"testing/synctest"
"time"
"github.com/prometheus-community/windows_exporter/internal/httphandler"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/pkg/collector"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
func TestMetricsHTTPHandler(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
query string
options *httphandler.Options
status int
included []string
excluded []string
}{
{name: "default", status: http.StatusOK, included: []string{"windows_test_first 42", "windows_test_second 42", "go_goroutines", "windows_exporter_build_info"}},
{name: "disable exporter metrics", options: &httphandler.Options{DisableExporterMetrics: true, TimeoutMargin: 0.5}, status: http.StatusOK, included: []string{"windows_test_first 42", "windows_exporter_build_info"}, excluded: []string{"go_goroutines", "process_cpu_seconds_total"}},
{name: "filter", query: "?collect[]=first", status: http.StatusOK, included: []string{"windows_test_first 42"}, excluded: []string{"windows_test_second"}},
{name: "multiple collectors", query: "?collect[]=first&collect[]=second", status: http.StatusOK, included: []string{"windows_test_first 42", "windows_test_second 42"}},
{name: "unknown collector", query: "?collect[]=missing", status: http.StatusBadRequest, included: []string{"unknown collector missing"}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
collection := collector.New(collector.Map{
"first": &testCollector{name: "first"},
"second": &testCollector{name: "second"},
})
handler := httphandler.New(slog.New(slog.DiscardHandler), collection, tc.options)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics"+tc.query, nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
require.Equal(t, tc.status, response.Code)
for _, value := range tc.included {
require.Contains(t, response.Body.String(), value)
}
for _, value := range tc.excluded {
require.NotContains(t, response.Body.String(), value)
}
// Filtering one request must not remove collectors from later requests.
if tc.query == "?collect[]=first" {
response = httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil))
require.Equal(t, http.StatusOK, response.Code)
require.Contains(t, response.Body.String(), "windows_test_second 42")
}
})
}
}
func TestMetricsHTTPHandlerCollectorStatus(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
err error
panicValue bool
success string
}{
{name: "success", success: "1"},
{name: "failure", err: errors.New("collector failed"), success: "0"},
{name: "no data", err: types.ErrNoData, success: "1"},
{name: "panic", panicValue: true, success: "0"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
collection := collector.New(collector.Map{"test": &testCollector{name: "test", err: tc.err, panicValue: tc.panicValue}})
handler := httphandler.New(slog.New(slog.DiscardHandler), collection, &httphandler.Options{DisableExporterMetrics: true})
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil))
require.Equal(t, http.StatusOK, response.Code)
require.Contains(t, response.Body.String(), `windows_exporter_collector_success{collector="test"} `+tc.success)
require.Contains(t, response.Body.String(), `windows_exporter_collector_timeout{collector="test"} 0`)
})
}
}
func TestMetricsHTTPHandlerScrapeTimeout(t *testing.T) {
for _, tc := range []struct {
name string
header string
timeout bool
}{
{name: "missing header"},
{name: "invalid header", header: "invalid"},
{name: "zero header", header: "0"},
{name: "sufficient timeout", header: "2"},
{name: "timeout", header: "0.1", timeout: true},
} {
t.Run(tc.name, func(t *testing.T) {
// The collector's delay and deadline use synthetic time.
synctest.Test(t, func(t *testing.T) {
collection := collector.New(collector.Map{"test": &testCollector{name: "test", delay: time.Second}})
handler := httphandler.New(slog.New(slog.DiscardHandler), collection, &httphandler.Options{DisableExporterMetrics: true})
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil)
request.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", tc.header)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
require.Equal(t, http.StatusOK, response.Code)
if tc.timeout {
require.Contains(t, response.Body.String(), `windows_exporter_collector_timeout{collector="test"} 1`)
require.NotContains(t, response.Body.String(), "windows_test_test 42")
// Finish the delayed collector and its drain goroutine before leaving the bubble.
time.Sleep(time.Second)
synctest.Wait()
} else {
require.Contains(t, response.Body.String(), `windows_exporter_collector_timeout{collector="test"} 0`)
require.Contains(t, response.Body.String(), "windows_test_test 42")
}
})
})
}
}
type testCollector struct {
name string
err error
panicValue bool
delay time.Duration
}
func (c *testCollector) GetName() string { return c.name }
func (c *testCollector) Build(_ *slog.Logger, _ *mi.Session) error { return nil }
func (c *testCollector) Close() error { return nil }
func (c *testCollector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
if c.panicValue {
panic("test collector panic")
}
time.Sleep(c.delay)
if c.err != nil {
return c.err
}
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc("windows_test_"+c.name, "Test metric", nil, nil), prometheus.GaugeValue, 42)
return nil
}
+45
View File
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package httphandler_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/prometheus-community/windows_exporter/internal/httphandler"
"github.com/prometheus/common/version"
"github.com/stretchr/testify/require"
)
func TestVersionHandler(t *testing.T) {
t.Parallel()
response := httptest.NewRecorder()
httphandler.NewVersionHandler().ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/version", nil))
require.Equal(t, http.StatusOK, response.Code)
var got map[string]string
require.NoError(t, json.Unmarshal(response.Body.Bytes(), &got))
require.Equal(t, map[string]string{
"version": version.Version, "revision": version.Revision,
"branch": version.Branch, "buildUser": version.BuildUser,
"buildDate": version.BuildDate, "goVersion": version.GoVersion,
}, got)
}
+176 -46
View File
@@ -21,6 +21,8 @@ import (
"errors"
"log/slog"
"os"
"slices"
"strings"
"sync"
"testing"
"time"
@@ -29,8 +31,11 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/update"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/pkg/collector"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
@@ -52,81 +57,206 @@ func FuncBenchmarkCollector[C collector.Collector](b *testing.B, name string, co
metrics := make(chan prometheus.Metric)
go func() {
for {
<-metrics
var wg sync.WaitGroup
wg.Go(func() {
for range metrics {
}
}()
})
b.Cleanup(func() {
close(metrics)
wg.Wait()
assert.NoError(b, collectors.Close())
})
for b.Loop() {
require.NoError(b, c.Collect(metrics, 0))
}
require.NoError(b, collectors.Close())
}
func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, conf *V) {
// TestCollector validates real Windows collector output. CI lists provisioned
// collectors in WINDOWS_EXPORTER_TEST_COLLECTORS so setup failures cannot skip.
func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, conf *V) map[string]*dto.MetricFamily {
t.Helper()
var (
metrics []prometheus.Metric
err error
)
logger := slog.New(slog.DiscardHandler)
c := fn(conf)
ch := make(chan prometheus.Metric, 10000)
required := slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), c.GetName())
miApp, err := mi.ApplicationInitialize()
require.NoError(t, err)
t.Cleanup(func() { assert.NoError(t, miApp.Close()) })
miSession, err := miApp.NewSession(nil)
require.NoError(t, err)
t.Cleanup(func() { assert.NoError(t, miSession.Close()) })
t.Cleanup(func() { assert.NoError(t, c.Close()) })
t.Cleanup(func() {
require.NoError(t, c.Close())
require.NoError(t, miSession.Close())
require.NoError(t, miApp.Close())
})
if err := c.Build(logger, miSession); err != nil {
if !required && unsupportedCollector(err) {
t.Skipf("collector %s is not supported: %v", c.GetName(), err)
}
wg := sync.WaitGroup{}
require.NoError(t, err, "build %s", c.GetName())
}
// PDH rate counters need a second sample after initialization.
time.Sleep(time.Second)
var families map[string]*dto.MetricFamily
for scrape := range 2 {
families = collectMetrics(t, c, required)
t.Logf("%s scrape %d: %d metric families", c.GetName(), scrape+1, len(families))
}
return families
}
func unsupportedCollector(err error) bool {
return errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE) ||
errors.Is(err, mi.MI_RESULT_INVALID_QUERY) ||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoCounter)) ||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) ||
errors.Is(err, pdh.ErrPerformanceCounterNotInitialized) ||
errors.Is(err, pdh.ErrNoData) ||
errors.Is(err, update.ErrUpdateServiceDisabled) ||
errors.Is(err, os.ErrNotExist) ||
errors.Is(err, windows.Errno(2151088411))
}
func collectMetrics(t *testing.T, c collector.Collector, required bool) map[string]*dto.MetricFamily {
t.Helper()
var (
metrics collectedMetrics
collectErr error
)
ch := make(chan prometheus.Metric)
var wg sync.WaitGroup
wg.Go(func() {
for metric := range ch {
metrics = append(metrics, metric)
}
})
err = c.Build(logger, miSession)
// Stop the receiver even if Collect panics or an assertion ends the test.
func() {
defer func() {
close(ch)
wg.Wait()
}()
switch {
case err == nil:
case errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE),
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoCounter)),
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)),
errors.Is(err, update.ErrUpdateServiceDisabled),
errors.Is(err, os.ErrNotExist):
default:
require.NoError(t, err)
err := c.Collect(ch, 30*time.Second)
if errors.Is(err, update.ErrNoUpdates) && required {
deadline := time.Now().Add(time.Minute)
for errors.Is(err, update.ErrNoUpdates) && time.Now().Before(deadline) {
time.Sleep(100 * time.Millisecond)
err = c.Collect(ch, 30*time.Second)
}
}
collectErr = err
}()
// Empty optional instance groups may report no data alongside valid metrics.
// Inspect every joined error so an unrelated failure cannot be hidden.
if noDataOnly(collectErr) && len(metrics) > 0 {
t.Logf("collector %s has empty optional instance groups: %v", c.GetName(), collectErr)
collectErr = nil
}
time.Sleep(1 * time.Second)
err = c.Collect(ch, 0)
switch {
// container collector
case errors.Is(err, windows.Errno(2151088411)),
errors.Is(err, pdh.ErrPerformanceCounterNotInitialized),
errors.Is(err, pdh.ErrNoData),
errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE),
errors.Is(err, mi.MI_RESULT_INVALID_QUERY),
errors.Is(err, update.ErrNoUpdates):
t.Skip("collector not supported on this system")
default:
require.NoError(t, err)
if !required && (unsupportedCollector(collectErr) || errors.Is(collectErr, update.ErrNoUpdates)) {
t.Skipf("collector %s is not supported: %v", c.GetName(), collectErr)
}
close(ch)
require.NoError(t, collectErr, "collect %s", c.GetName())
wg.Wait()
if required {
require.NotEmpty(t, metrics, "provisioned collector %s emitted no metrics", c.GetName())
}
registry := prometheus.NewPedanticRegistry()
require.NoError(t, registry.Register(metrics))
gathered, err := registry.Gather()
require.NoError(t, err, "invalid metrics from %s", c.GetName())
families := make(map[string]*dto.MetricFamily, len(gathered))
for _, family := range gathered {
families[family.GetName()] = family
if strings.HasSuffix(family.GetName(), "_collector_success") {
for _, metric := range family.GetMetric() {
require.InDelta(t, 1, metric.GetGauge().GetValue(), 0, "failed child collector: %s", metric)
}
}
}
return families
}
type collectedMetrics []prometheus.Metric
func (m collectedMetrics) Describe(ch chan<- *prometheus.Desc) {
prometheus.DescribeByCollect(m, ch)
}
func (m collectedMetrics) Collect(ch chan<- prometheus.Metric) {
for _, metric := range m {
ch <- metric
}
}
// RequireFixtureMetric checks a known CI fixture without requiring it on a
// developer's machine. Labels match exactly, ignoring case for Windows names.
func RequireFixtureMetric(t *testing.T, families map[string]*dto.MetricFamily, collectorName, metricName string, labels prometheus.Labels) {
t.Helper()
if !slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), collectorName) {
return
}
require.Contains(t, families, metricName)
for _, metric := range families[metricName].GetMetric() {
matched := true
for name, value := range labels {
found := false
for _, label := range metric.GetLabel() {
if label.GetName() == name && strings.EqualFold(label.GetValue(), value) {
found = true
break
}
}
matched = matched && found
}
if matched {
return
}
}
t.Fatalf("metric %s with fixture labels %v was not emitted; got %s", metricName, labels, families[metricName])
}
func noDataOnly(err error) bool {
if joined, ok := err.(interface{ Unwrap() []error }); ok {
for _, cause := range joined.Unwrap() {
if !noDataOnly(cause) {
return false
}
}
return true
}
if wrapped, ok := err.(interface{ Unwrap() error }); ok {
return noDataOnly(wrapped.Unwrap())
}
return errors.Is(err, pdh.ErrNoData) || errors.Is(err, types.ErrNoData)
}
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package testutils
import (
"errors"
"fmt"
"testing"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/stretchr/testify/require"
)
func TestNoDataOnly(t *testing.T) {
t.Parallel()
failure := errors.New("collector failed")
for _, tc := range []struct {
name string
err error
want bool
}{
{name: "nil"},
{name: "PDH no data", err: pdh.ErrNoData, want: true},
{name: "collector no data", err: types.ErrNoData, want: true},
{name: "wrapped", err: fmt.Errorf("empty instances: %w", pdh.ErrNoData), want: true},
{name: "joined empty groups", err: errors.Join(pdh.ErrNoData, types.ErrNoData), want: true},
{name: "failure", err: failure},
{name: "mixed failures", err: errors.Join(pdh.ErrNoData, failure)},
{name: "nested mixed failures", err: fmt.Errorf("child: %w", errors.Join(pdh.ErrNoData, failure))},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tc.want, noDataOnly(tc.err))
})
}
}
+117
View File
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package collector_test
import (
"errors"
"log/slog"
"testing"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/pkg/collector"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
func TestCollectionConstructors(t *testing.T) {
t.Parallel()
t.Run("config includes every collector", func(t *testing.T) {
t.Parallel()
collection := collector.NewWithConfig(collector.Config{})
_, err := collection.NewHandler(time.Second, slog.New(slog.DiscardHandler), collector.Available())
require.NoError(t, err)
})
t.Run("flags include every collector", func(t *testing.T) {
t.Parallel()
app := kingpin.New("test", "test")
collection := collector.NewWithFlags(app)
_, err := app.Parse(nil)
require.NoError(t, err)
_, err = collection.NewHandler(time.Second, slog.New(slog.DiscardHandler), collector.Available())
require.NoError(t, err)
})
}
func TestCollectionBuildAndClose(t *testing.T) {
t.Parallel()
buildFailure := errors.New("build failed")
closeFailure := errors.New("close failed")
for _, tc := range []struct {
name string
buildErr error
closeErr error
wantBuildErr error
}{
{name: "success"},
{name: "build failure", buildErr: buildFailure, wantBuildErr: buildFailure},
{name: "optional counter missing", buildErr: pdh.ErrNoData},
{name: "close failure", closeErr: closeFailure},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
test := &lifecycleCollector{name: "test", buildErr: tc.buildErr, closeErr: tc.closeErr}
collection := collector.New(collector.Map{"test": test})
err := collection.Build(t.Context(), slog.New(slog.DiscardHandler))
t.Cleanup(func() {
err := collection.Close()
if tc.closeErr != nil {
require.ErrorIs(t, err, tc.closeErr)
} else {
require.NoError(t, err)
}
require.True(t, test.closed)
})
if tc.wantBuildErr != nil {
require.ErrorIs(t, err, tc.wantBuildErr)
} else {
require.NoError(t, err)
}
})
}
}
type lifecycleCollector struct {
name string
buildErr error
closeErr error
closed bool
}
func (c *lifecycleCollector) GetName() string { return c.name }
func (c *lifecycleCollector) Build(_ *slog.Logger, _ *mi.Session) error { return c.buildErr }
func (c *lifecycleCollector) Collect(_ chan<- prometheus.Metric, _ time.Duration) error { return nil }
func (c *lifecycleCollector) Close() error {
c.closed = true
return c.closeErr
}
+91 -45
View File
@@ -15,23 +15,108 @@
"ignorePaths": ["**/workflows/container_description.yml","**/workflows/stale.yml","**/workflows/govulncheck.yml"],
"packageRules": [
{
"matchPackageNames": ["https://github.com/yaml/go-yaml"],
"matchUpdateTypes": ["major"],
"enabled": true,
"matchDatasources": ["golang-version"],
"rangeStrategy": "bump"
},
{
"enabled": true,
"matchDepNames": ["go", "golang"],
"groupName": "go version"
},
{
"enabled": true,
"matchCategories": ["go"],
"matchUpdateTypes": ["digest"]
},
{
"groupName": "golangci-lint",
"matchPackageNames": [
"github.com/golangci/golangci-lint/cmd/golangci-lint",
"golangci/golangci-lint",
"golangci/golangci-lint-action"
],
"addLabels": ["chore"]
},
{
"matchPackageNames": ["gopkg.in/yaml"],
"enabled": false
},
{
"matchManagers": ["gomod"],
"matchDepTypes": ["indirect"],
"major": {"enabled": false},
"enabled": true
},
{
"groupName": "GitHub Actions",
"matchManagers": ["github-actions"],
"matchDatasources": ["!/github-runners$/"],
"semanticCommitScope": "github-actions",
"separateMajorMinor": false,
"addLabels": ["chore"]
},
{
"groupName": "GitHub Runners",
"matchManagers": ["github-actions"],
"matchDatasources": ["github-runners"],
"semanticCommitScope": "github-actions",
"separateMajorMinor": false,
"addLabels": ["chore"]
},
{
"groupName": "moby monorepo",
"matchManagers": ["gomod"],
"matchPackageNames": ["github.com/moby/moby", "github.com/moby/moby/**"]
},
{
"groupName": "golang.org/x",
"matchPackageNames": ["golang.org/x/**"]
},
{
"groupName": "googleapis",
"matchPackageNames": ["google.golang.org/genproto/googleapis/**"]
},
{
"groupName": "Go tools",
"matchFileNames": ["tools/**", "Makefile"],
"matchPackageNames": ["*"],
"addLabels": ["chore"]
},
{
"matchPackageNames": ["golang.org/x/net"],
"sourceUrl": "https://github.com/golang/net"
},
{
"matchPackageNames": ["golang.org/x/oauth2"],
"sourceUrl": "https://github.com/golang/oauth2"
},
{
"matchPackageNames": ["golang.org/x/sys"],
"sourceUrl": "https://github.com/golang/sys"
},
{
"matchPackageNames": ["golang.org/x/text"],
"sourceUrl": "https://github.com/golang/text"
},
{
"enabled": false,
"matchPackageNames": ["https://github.com/yaml/go-yaml"],
"matchUpdateTypes": ["major"]
},
{
"enabled": false,
"matchBaseBranches": ["/^[0-9]\\.[0-9][0-9]$/"],
"matchUpdateTypes": [
"major"
],
"enabled": false
]
},
{
"enabled": false,
"matchBaseBranches": ["/^[0-9]\\.[0-9][0-9]$/"],
"matchManagers": [
"github-actions"
],
"enabled": false
]
},
{
"enabled": false,
@@ -41,45 +126,6 @@
"golangci/golangci-lint",
"golangci/golangci-lint-action"
]
},
{
"groupName": "golangci-lint",
"matchPackageNames": [
"github.com/golangci/golangci-lint/cmd/golangci-lint",
"golangci/golangci-lint",
"golangci/golangci-lint-action"
]
},
{
"matchManagers": [
"gomod"
],
"matchDepTypes": [
"indirect"
],
"enabled": true
},
{
"groupName": "GitHub Actions",
"matchManagers": [
"github-actions"
]
},
{
"groupName": "golang.org/x/",
"matchPackageNames": [
"golang.org/x/*"
]
},
{
"groupName": "Go tools",
"matchFileNames": [
"tools/**",
"Makefile"
],
"matchPackageNames": [
"*"
]
}
],
"postUpdateOptions": [
+33 -77
View File
@@ -1,85 +1,41 @@
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3
# cd to location of script
$script_path = $MyInvocation.MyCommand.Path
$working_dir = Split-Path $script_path
Push-Location $working_dir
$binary = Join-Path $PSScriptRoot '..\windows_exporter.exe'
$logDir = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { $env:TEMP }
$stdout = Join-Path $logDir 'windows_exporter.stdout.log'
$stderr = Join-Path $logDir 'windows_exporter.stderr.log'
$exporter = Start-Process -FilePath $binary -PassThru `
-ArgumentList '--web.listen-address=127.0.0.1:9182' `
-RedirectStandardOutput $stdout -RedirectStandardError $stderr
if (-not (Test-Path -Path '..\windows_exporter.exe')) {
Write-Output "..\windows_exporter.exe not found. Consider running \`go build\` first"
}
try {
$deadline = (Get-Date).AddSeconds(60)
do {
if ($exporter.HasExited) {
throw "windows_exporter exited with code $($exporter.ExitCode)"
}
$temp_dir = Join-Path $env:TEMP $([guid]::newguid()) | ForEach-Object { mkdir $_ }
$client = [System.Net.Sockets.TcpClient]::new()
try {
$client.Connect('127.0.0.1', 9182)
break
} catch {
if ((Get-Date) -ge $deadline) { throw 'windows_exporter did not start within 60 seconds' }
Start-Sleep -Milliseconds 200
} finally {
$client.Dispose()
}
} while ($true)
# Create temporary directory for textfile collector
$textfile_dir = "$($temp_dir)/textfile"
mkdir $textfile_dir | Out-Null
Copy-Item 'e2e-textfile.prom' -Destination "$($textfile_dir)/e2e-textfile.prom"
# Omit dynamic collector information that will change after each run
$skip_re = "^(go_|windows_exporter_build_info|windows_exporter_collector_duration_seconds|windows_exporter_scrape_duration_seconds|process_|windows_textfile_mtime_seconds|windows_cpu|windows_cache|windows_pagefile|windows_logical_disk|windows_physical_disk|windows_memory|windows_net|windows_os|windows_process|windows_service_process|windows_printer|windows_udp|windows_tcp|windows_system|windows_time|windows_session|windows_performancecounter|windows_performancecounter|windows_textfile_mtime_seconds)"
# Start process in background, awaiting HTTP requests.
# Use default collectors, port and address: http://localhost:9182/metrics
$exporter_proc = Start-Process `
-PassThru `
-FilePath ..\windows_exporter.exe `
-ArgumentList "--log.level=debug","--web.disable-exporter-metrics","--collectors.enabled=[defaults],cpu_info,textfile,process,pagefile,performancecounter,scheduled_task,tcp,udp,time,system,service,logical_disk,os,net,memory,cache","--collector.process.include=explorer.exe","--collector.scheduled_task.include=.*GAEvents","--collector.service.include=Themes","--collector.textfile.directories=$($textfile_dir)",@"
--collector.performancecounter.objects="[{\"name\":\"cpu\",\"object\":\"Processor Information\",\"instances\":[\"*\"],\"instance_label\":\"core\",\"counters\":[{\"name\":\"% Processor Time\",\"metric\":\"windows_performancecounter_processor_information_processor_time\",\"labels\":{\"state\":\"active\"}},{\"name\":\"% Idle Time\",\"metric\":\"windows_performancecounter_processor_information_processor_time\",\"labels\":{\"state\":\"idle\"}}]},{\"name\":\"memory\",\"object\":\"Memory\",\"counters\":[{\"name\":\"Cache Faults/sec\",\"type\":\"counter\"}]}]"
"@ `
-WindowStyle Hidden `
-RedirectStandardOutput "$($temp_dir)/windows_exporter.log" `
-RedirectStandardError "$($temp_dir)/windows_exporter_error.log"
# Exporter can take some time to start
for ($i=1; $i -le 1; $i++) {
Start-Sleep 10
$netstat_output = netstat -anp tcp | Select-String 'listening'
if ($netstat_output -like '*:9182*') {
break
# Metric validation belongs in Go tests. This is one binary smoke request.
$response = Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:9182/metrics' -TimeoutSec 30
if ($response.StatusCode -ne 200 -or [string]::IsNullOrWhiteSpace($response.Content)) {
throw 'windows_exporter returned an unsuccessful or empty metrics response'
}
Write-Host "Waiting for exporter to start"
}
try {
$response = Invoke-WebRequest -UseBasicParsing -URI http://127.0.0.1:9182/metrics
} catch {
Write-Host "STDOUT"
Get-Content "$($temp_dir)/windows_exporter.log"
Write-Host "STDERR"
Get-Content "$($temp_dir)/windows_exporter_error.log"
throw $_
}
# Response output must be split and saved as UTF-8.
$response.content -split "[`r`n]"| Select-String -NotMatch $skip_re | Set-Content -Encoding utf8 "$($temp_dir)/e2e-output.txt"
try {
Stop-Process -Id $exporter_proc.Id
} catch {
Write-Host "STDOUT"
Get-Content "$($temp_dir)/windows_exporter.log"
Write-Host "STDERR"
Get-Content "$($temp_dir)/windows_exporter_error.log"
throw $_
}
# Compare the expected and actual output
$output_diff = Compare-Object (Get-Content 'e2e-output.txt' | Where-Object { $_ -ne "" }) (Get-Content "$($temp_dir)/e2e-output.txt" | Where-Object { $_ -ne "" })
# Fail if differences in output are detected
if (-not ($null -eq $output_diff)) {
$output_diff | Format-Table -AutoSize | Out-String -Width 10000
Write-Host "STDOUT"
Get-Content "$($temp_dir)/windows_exporter.log"
Write-Host "----------------------------------------"
Write-Host "STDERR"
Get-Content "$($temp_dir)/windows_exporter_error.log"
(Get-Content "$($temp_dir)/e2e-output.txt") | Set-Content -Encoding utf8 "e2e-output.txt"
exit 1
Write-Host "windows_exporter returned HTTP $($response.StatusCode)"
} finally {
if (-not $exporter.HasExited) { Stop-Process -Id $exporter.Id }
Get-Content $stdout -ErrorAction SilentlyContinue
Get-Content $stderr -ErrorAction SilentlyContinue
}
-141
View File
@@ -1,141 +0,0 @@
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 3
if (-not (Test-Path -Path '.\windows_exporter.exe')) {
Write-Output ".\windows_exporter.exe not found. Consider running \`go build\` first"
}
# Powershell pipes & Get-Content command rather unhelpfully add a carriage return at the end of the string, so
# passing the string as bytes is a messy but necessary workaround for processes that are sensitive to
# line endings, like promtool.
function Start-RawProcess {
param(
# String to pass to $CommandName via STDIN
[Parameter(Mandatory=$true)][String]$InputVar,
# Command to run
[Parameter(Mandatory=$true)][String]$CommandName,
# Arguments provided to $CommandName
[Parameter(Mandatory=$false)][String[]]$CommandArgs
)
# Buffer & initial size of MemoryStream
$BufferSize = 4096
# Convert text to bytes and write to MemoryStream
[byte[]]$InputBytes = [Text.Encoding]::UTF8.GetBytes($InputVar)
$MemStream = New-Object -TypeName System.IO.MemoryStream -ArgumentList $BufferSize
$MemStream.Write($InputBytes, 0, $InputBytes.Length)
[Void]$MemStream.Seek(0, 'Begin')
# Setup stdin\stdout redirection for our process
if ($CommandArgs) {
$StartInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo -Property @{
FileName = $CommandName
UseShellExecute = $false
RedirectStandardInput = $true
RedirectStandardError = $true
Arguments = $CommandArgs
}
} else {
$StartInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo -Property @{
FileName = $CommandName
UseShellExecute = $false
RedirectStandardInput = $true
RedirectStandardError = $true
}
}
# Create new process
$Process = New-Object -TypeName System.Diagnostics.Process
# Assign previously created StartInfo properties
$Process.StartInfo = $StartInfo
# Start process
[void]$Process.Start()
# Pipe data
$Buffer = New-Object -TypeName byte[] -ArgumentList $BufferSize
$StdinStream = $Process.StandardInput.BaseStream
try {
do {
$ReadCount = $MemStream.Read($Buffer, 0, $Buffer.Length)
$StdinStream.Write($Buffer, 0, $ReadCount)
$StdinStream.Flush()
}
while($ReadCount -gt 0)
}
catch
{
throw 'Error streaming buffer to STDIN'
} finally {
# Close streams
$StdinStream.Close()
$MemStream.Close()
}
$Process.WaitForExit()
if ($Process.ExitCode -ne 0) {
Write-Host $Process.StandardError.ReadToEnd()
}
return $Process.ExitCode
}
# cd to location of script
$script_path = $MyInvocation.MyCommand.Path
$working_dir = Split-Path $script_path
Push-Location $working_dir
$temp_dir = Join-Path $env:TEMP $([guid]::newguid()) | ForEach-Object { mkdir $_ }
# Start process in background, awaiting HTTP requests.
# Listen on 9183/TCP, preventing conflicts with 9182/TCP used by end-to-end-test.ps1
# Not an issue when run individually, but will cause failures when run concurrently in CI.
$exporter_proc = Start-Process `
-PassThru `
-FilePath ..\windows_exporter.exe `
-ArgumentList '--web.listen-address="127.0.0.1:9183" --log.level=debug --collectors.enabled=[defaults],cpu_info,textfile,process,scheduled_task'`
-WindowStyle Hidden `
-RedirectStandardOutput "$($temp_dir)/windows_exporter.log" `
-RedirectStandardError "$($temp_dir)/windows_exporter_error.log"
# Exporter can take some time to start
for ($i=1; $i -le 5; $i++) {
Start-Sleep 10
$netstat_output = netstat -anp tcp | Select-String 'listening'
if ($netstat_output -like '*:9183*') {
break
}
Write-Host "Waiting for exporter to start"
}
# Omit metrics from client_golang library; we're not responsible for these
# windows_memory_pool_nonpaged_allocs_total is wrong for years. It's not a gauge, but a counter.
$skip_re = "^([#]?\s*(HELP|TYPE)?\s*go_|windows_memory_pool_nonpaged_allocs_total)"
try {
# Need to remove carriage returns, as promtool expects LF line endings
$output = ((Invoke-WebRequest -UseBasicParsing -URI http://127.0.0.1:9183/metrics).Content) -Split "`r?`n" | Select-String -NotMatch $skip_re | Join-String -Separator "`n"
# $output = (((Invoke-WebRequest -UseBasicParsing -URI http://127.0.0.1:9183/metrics).Content) -Split "`r?`n" | Select-String -NotMatch $skip_re) -join "`n"
# Join the split lines back to a single String (with LF line endings!)
$output = $output -Join "`n"
Stop-Process -Id $exporter_proc.Id
} catch {
Write-Host "STDOUT"
Get-Content "$($temp_dir)/windows_exporter.log"
Write-Host "STDERR"
Get-Content "$($temp_dir)/windows_exporter_error.log"
throw $_
}
$ExitCode = Start-RawProcess -InputVar $output -CommandName promtool.exe -CommandArgs @("check metrics")
if ($ExitCode -ne 0) {
Write-Host "OUTPUT"
Write-Host $output
Write-Host "Promtool command returned exit code $($ExitCode). See output for details."
EXIT 1
}