From 9a1a9d61d5afb7d333e77fcfd2f5b6cf1b46f5fd Mon Sep 17 00:00:00 2001 From: Pierre Gimalac <23154723+pgimalac@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:35:18 +0200 Subject: [PATCH] Add lparstats check (AIX only) (#23451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add lparstats check (AIX only) Port the lparstats check from datadog-unix-agent to integrations-core, updated for Python 3 and datadog-checks-base. This check collects IBM POWER LPAR performance metrics on AIX via the `lparstat` command: - Memory statistics (system.lpar.memory.*) - Hypervisor call statistics (system.lpar.hypervisor.*) - I/O memory entitlements (system.lpar.memory.entitlement.*) - SPURR processor utilization (system.lpar.spurr.*) The manifest explicitly declares "Supported OS::AIX" as this check relies on lparstat which is exclusive to IBM AIX on POWER hardware. * Fix validation issues: license headers, CHANGELOG, metadata sort, spec.yaml, labeler, ci * Generate config models, sync conf.yaml.example, sync CI, fix lint * Add missing [project.optional-dependencies] section to pyproject.toml * Add basic unit tests with mocked lparstat output * Fix test: remove dd_environment fixture (no Docker needed) * Remove unused pytest import * Fix test: remove assert_all_metrics_covered, check key metrics only * Add dd_environment fixture and rebase onto master * Fix manifest: remove invalid Supported OS::AIX tag, add auto_install and source_type_id * Apply review nits: f-strings, x.split(), hypervisor guard, dev status 5, remove setup.cfg, skip e2e on non-AIX * lparstats: declare SPURR .pct metrics as fraction not percent Values emitted by collect_spurr are in the [0,1] range (e.g. 0.015), not [0,100], so unit_name=percent was incorrect. * lparstats: set curated_metric=core for system.lpar.memory.physb This is the manifest's primary check metric; marking it as core aligns with the convention used by other integrations. * lparstats: always apply DEFAULT_TIMEOUT, not only under sudo A hung lparstat call can block the check regardless of whether sudo is in use; unconditionally setting the timeout is the safer default. * lparstats: strip % from field names in collect_memory_entitlements collect_memory already strips % from its fields; apply the same treatment in collect_memory_entitlements so a %-suffixed column in lparstat -m -eR output produces a valid metric name. * lparstats: add type hints to all callables * lparstats: replace HYPERVISOR_IDX_METRIC_MAP dict with a tuple Contiguous integer keys 0..4 are just positional indices; a tuple is simpler and removes the need for a dict-lookup pattern. * lparstats: patch subprocess.run in tests instead of private _run_cmd Coupling tests to the internal _run_cmd helper is fragile; patching the public subprocess.run interface is more stable and matches the project testing guidelines. * lparstats: use dd_run_check fixture instead of check.check(instance) * lparstats: add tests for hypervisor and memory-entitlements collectors Both collectors had zero coverage because the default instance fixture disables them. Add a dedicated test that enables both and asserts that expected metrics are emitted. * lparstats: add tag assertions for all collectors Assert that memory/SPURR metrics carry no tags and that hypervisor (call:) and entitlement (iompn:) tags are present. * lparstats: guard os.getuid() with hasattr check os.getuid() does not exist on Windows; wrap it so the module can be imported on non-Unix platforms without raising AttributeError. * lparstats: check returncode in all collectors, add lparstats.can_collect service check Each collector now inspects the lparstat return code and skips metric emission (with a warning) on failure instead of silently parsing empty output. A new lparstats.can_collect service check is emitted OK when all enabled collectors succeed and CRITICAL if any lparstat invocation exits non-zero. * lparstats: fix fragile SPURR actual-vs-normalized column split The previous split used idx > len(fields)/2, which silently breaks if lparstat -E ever changes its column count. Split at the freq column instead (it reliably separates actual from normalized), with a fallback warning if freq is absent. * lparstats: extract _lparstat_rows helper to deduplicate parsing prefix All four collectors shared the same _run_cmd → splitlines → filter → slice pattern. _lparstat_rows(cmd, start_idx, ...) centralises it and returns (rows, stderr, returncode) so callers can still inspect the exit code. * lparstats: fix curated_metric value for system.lpar.memory.physb Valid values are cpu and memory; core is not accepted by the validator. * lparstats: disable e2e env, remove dd_environment fixture Set e2e-env = false in hatch.toml so CI does not try to spin up an e2e environment for an AIX-only check that can never run on Linux CI. Remove the now-redundant dd_environment fixture from conftest.py. * lparstats: set owner to agent-integrations --- .codecov.yml | 9 + .github/workflows/config/labeler.yml | 4 + .github/workflows/test-all.yml | 20 ++ lparstats/CHANGELOG.md | 3 + lparstats/README.md | 71 ++++++ lparstats/assets/configuration/spec.yaml | 61 +++++ lparstats/assets/service_checks.json | 11 + .../datadog_checks/lparstats/__about__.py | 5 + .../datadog_checks/lparstats/__init__.py | 8 + .../lparstats/config_models/__init__.py | 24 ++ .../lparstats/config_models/defaults.py | 52 +++++ .../lparstats/config_models/instance.py | 69 ++++++ .../lparstats/config_models/shared.py | 45 ++++ .../lparstats/config_models/validators.py | 13 ++ .../lparstats/data/conf.yaml.example | 96 ++++++++ .../datadog_checks/lparstats/lparstats.py | 214 ++++++++++++++++++ lparstats/hatch.toml | 7 + lparstats/manifest.json | 47 ++++ lparstats/metadata.csv | 42 ++++ lparstats/pyproject.toml | 46 ++++ lparstats/tests/__init__.py | 3 + lparstats/tests/conftest.py | 19 ++ lparstats/tests/test_check.py | 150 ++++++++++++ 23 files changed, 1019 insertions(+) create mode 100644 lparstats/CHANGELOG.md create mode 100644 lparstats/README.md create mode 100644 lparstats/assets/configuration/spec.yaml create mode 100644 lparstats/assets/service_checks.json create mode 100644 lparstats/datadog_checks/lparstats/__about__.py create mode 100644 lparstats/datadog_checks/lparstats/__init__.py create mode 100644 lparstats/datadog_checks/lparstats/config_models/__init__.py create mode 100644 lparstats/datadog_checks/lparstats/config_models/defaults.py create mode 100644 lparstats/datadog_checks/lparstats/config_models/instance.py create mode 100644 lparstats/datadog_checks/lparstats/config_models/shared.py create mode 100644 lparstats/datadog_checks/lparstats/config_models/validators.py create mode 100644 lparstats/datadog_checks/lparstats/data/conf.yaml.example create mode 100644 lparstats/datadog_checks/lparstats/lparstats.py create mode 100644 lparstats/hatch.toml create mode 100644 lparstats/manifest.json create mode 100644 lparstats/metadata.csv create mode 100644 lparstats/pyproject.toml create mode 100644 lparstats/tests/__init__.py create mode 100644 lparstats/tests/conftest.py create mode 100644 lparstats/tests/test_check.py diff --git a/.codecov.yml b/.codecov.yml index ac3d54aa44206..49c979786fa41 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -430,6 +430,10 @@ coverage: target: 75 flags: - kyototycoon + LPARStats: + target: 75 + flags: + - lparstats Lighttpd: target: 75 flags: @@ -1408,6 +1412,11 @@ flags: paths: - kyverno/datadog_checks/kyverno - kyverno/tests + lparstats: + carryforward: true + paths: + - lparstats/datadog_checks/lparstats + - lparstats/tests lighttpd: carryforward: true paths: diff --git a/.github/workflows/config/labeler.yml b/.github/workflows/config/labeler.yml index a865fe4dd71b2..77dc38b774c1f 100644 --- a/.github/workflows/config/labeler.yml +++ b/.github/workflows/config/labeler.yml @@ -890,6 +890,10 @@ integration/langchain: - changed-files: - any-glob-to-any-file: - langchain/**/* +integration/lparstats: +- changed-files: + - any-glob-to-any-file: + - lparstats/**/* integration/lastpass: - changed-files: - any-glob-to-any-file: diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index fb09186fb0e82..8d60ea2567f71 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -2457,6 +2457,26 @@ jobs: minimum-base-package: ${{ inputs.minimum-base-package }} pytest-args: ${{ inputs.pytest-args }} secrets: inherit + jefcf576: + uses: ./.github/workflows/test-target.yml + with: + job-name: LPARStats + target: lparstats + platform: linux + runner: '["ubuntu-22.04"]' + repo: "${{ inputs.repo }}" + context: ${{ inputs.context }} + python-version: "${{ inputs.python-version }}" + latest: ${{ inputs.latest }} + agent-image: "${{ inputs.agent-image }}" + agent-image-py2: "${{ inputs.agent-image-py2 }}" + agent-image-windows: "${{ inputs.agent-image-windows }}" + agent-image-windows-py2: "${{ inputs.agent-image-windows-py2 }}" + test-py2: ${{ inputs.test-py2 }} + test-py3: ${{ inputs.test-py3 }} + minimum-base-package: ${{ inputs.minimum-base-package }} + pytest-args: ${{ inputs.pytest-args }} + secrets: inherit je63e92c: uses: ./.github/workflows/test-target.yml with: diff --git a/lparstats/CHANGELOG.md b/lparstats/CHANGELOG.md new file mode 100644 index 0000000000000..0a12bcb03b1ba --- /dev/null +++ b/lparstats/CHANGELOG.md @@ -0,0 +1,3 @@ +# CHANGELOG - lparstats + + diff --git a/lparstats/README.md b/lparstats/README.md new file mode 100644 index 0000000000000..4eb3c97ad530f --- /dev/null +++ b/lparstats/README.md @@ -0,0 +1,71 @@ +# LPARStats + +## Overview + +The LPARStats check collects performance metrics from IBM POWER Logical Partitions (LPARs) +running AIX by parsing the output of the `lparstat` command. + +**This check is only supported on AIX.** It relies on the `lparstat` utility, which is +exclusive to IBM AIX on POWER hardware. + +Metrics collected: + +- **Memory statistics** (`system.lpar.memory.*`): physical memory usage, page statistics, + I/O memory pool utilization. +- **Hypervisor call statistics** (`system.lpar.hypervisor.*`): per-call counts and latency + for hypervisor calls. Requires root or sudo. +- **I/O memory entitlements** (`system.lpar.memory.entitlement.*`): per-pool entitlement + and allocation data. Requires root or sudo. +- **SPURR processor utilization** (`system.lpar.spurr.*`): actual and normalized physical + processor utilization rates. + +## Setup + +### Installation + +The LPARStats check is included in the [Datadog Agent][1] package for AIX. No additional +installation is needed. + +### Configuration + +1. Edit the `lparstats.d/conf.yaml` file in your Agent's `conf.d/` directory. + See the [sample lparstats.d/conf.yaml][2] for all available configuration options. + +2. To collect hypervisor and memory entitlement metrics, the Agent must run as root, or + the `dd-agent` user must be granted sudo access to `lparstat`: + + ``` + dd-agent ALL=(root) NOPASSWD: /usr/bin/lparstat + ``` + +3. [Restart the Agent][3]. + +### Validation + +Run the [Agent's status subcommand][4] and look for `lparstats` under the Checks section. + +## Data Collected + +### Metrics + +See [metadata.csv][5] for a list of metrics provided by this check. + +### Service Checks + +`lparstats.can_collect` +: Returns `CRITICAL` if any `lparstat` sub-command exits with a non-zero return code. Returns `OK` otherwise. + +### Events + +The LPARStats check does not include any events. + +## Support + +Need help? Contact [Datadog support][6]. + +[1]: https://app.datadoghq.com/account/settings/agent/latest +[2]: https://github.com/DataDog/integrations-core/blob/master/lparstats/datadog_checks/lparstats/data/conf.yaml.example +[3]: https://docs.datadoghq.com/agent/guide/agent-commands/#start-stop-and-restart-the-agent +[4]: https://docs.datadoghq.com/agent/guide/agent-commands/#agent-status-and-information +[5]: https://github.com/DataDog/integrations-core/blob/master/lparstats/metadata.csv +[6]: https://docs.datadoghq.com/help/ diff --git a/lparstats/assets/configuration/spec.yaml b/lparstats/assets/configuration/spec.yaml new file mode 100644 index 0000000000000..37be803c4efc3 --- /dev/null +++ b/lparstats/assets/configuration/spec.yaml @@ -0,0 +1,61 @@ +name: LPARStats +files: +- name: lparstats.yaml + options: + - template: init_config + options: + - template: init_config/default + - template: instances + options: + - name: name + description: A name for this instance. + required: false + value: + type: string + example: lparstats + - name: sudo + description: | + Run lparstat with sudo. Requires adding dd-agent to the sudoers file: + + dd-agent ALL=(ALL) NOPASSWD: /usr/bin/lparstat + + When running as root, sudo is not needed. + required: false + value: + type: boolean + example: false + - name: memory_stats + description: Collect physical memory and page statistics (lparstat -m). + required: false + value: + type: boolean + example: true + - name: page_stats + description: Include page-level statistics (-pw flag). Requires memory_stats to be true. + required: false + value: + type: boolean + example: true + - name: memory_entitlements + description: | + Collect per-I/O-memory-pool entitlement stats (lparstat -m -eR). + Requires root or sudo. + required: false + value: + type: boolean + example: true + - name: hypervisor + description: | + Collect hypervisor call statistics (lparstat -H). + Requires root or sudo. + required: false + value: + type: boolean + example: true + - name: spurr_utilization + description: Collect SPURR physical processor utilization (lparstat -E). + required: false + value: + type: boolean + example: true + - template: instances/default diff --git a/lparstats/assets/service_checks.json b/lparstats/assets/service_checks.json new file mode 100644 index 0000000000000..1eeb516daaf99 --- /dev/null +++ b/lparstats/assets/service_checks.json @@ -0,0 +1,11 @@ +[ + { + "agent_version": "7.0.0", + "integration": "lparstats", + "check": "lparstats.can_collect", + "statuses": ["ok", "critical"], + "groups": [], + "name": "LPARStats Can Collect", + "description": "Returns `CRITICAL` if any `lparstat` sub-command fails (non-zero exit code). Returns `OK` otherwise." + } +] diff --git a/lparstats/datadog_checks/lparstats/__about__.py b/lparstats/datadog_checks/lparstats/__about__.py new file mode 100644 index 0000000000000..8b81c97c6ea37 --- /dev/null +++ b/lparstats/datadog_checks/lparstats/__about__.py @@ -0,0 +1,5 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +__version__ = '0.1.0' diff --git a/lparstats/datadog_checks/lparstats/__init__.py b/lparstats/datadog_checks/lparstats/__init__.py new file mode 100644 index 0000000000000..693a3a83d1a4d --- /dev/null +++ b/lparstats/datadog_checks/lparstats/__init__.py @@ -0,0 +1,8 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from .__about__ import __version__ +from .lparstats import LPARStats + +__all__ = ['__version__', 'LPARStats'] diff --git a/lparstats/datadog_checks/lparstats/config_models/__init__.py b/lparstats/datadog_checks/lparstats/config_models/__init__.py new file mode 100644 index 0000000000000..f678b7e73d91a --- /dev/null +++ b/lparstats/datadog_checks/lparstats/config_models/__init__.py @@ -0,0 +1,24 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from .instance import InstanceConfig +from .shared import SharedConfig + + +class ConfigMixin: + _config_model_instance: InstanceConfig + _config_model_shared: SharedConfig + + @property + def config(self) -> InstanceConfig: + return self._config_model_instance + + @property + def shared_config(self) -> SharedConfig: + return self._config_model_shared diff --git a/lparstats/datadog_checks/lparstats/config_models/defaults.py b/lparstats/datadog_checks/lparstats/config_models/defaults.py new file mode 100644 index 0000000000000..5b2467a85e3a5 --- /dev/null +++ b/lparstats/datadog_checks/lparstats/config_models/defaults.py @@ -0,0 +1,52 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + + +def instance_disable_generic_tags(): + return False + + +def instance_empty_default_hostname(): + return False + + +def instance_enable_legacy_tags_normalization(): + return True + + +def instance_hypervisor(): + return True + + +def instance_memory_entitlements(): + return True + + +def instance_memory_stats(): + return True + + +def instance_min_collection_interval(): + return 15 + + +def instance_name(): + return 'lparstats' + + +def instance_page_stats(): + return True + + +def instance_spurr_utilization(): + return True + + +def instance_sudo(): + return False diff --git a/lparstats/datadog_checks/lparstats/config_models/instance.py b/lparstats/datadog_checks/lparstats/config_models/instance.py new file mode 100644 index 0000000000000..e33be01595ab0 --- /dev/null +++ b/lparstats/datadog_checks/lparstats/config_models/instance.py @@ -0,0 +1,69 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, ConfigDict, field_validator, model_validator + +from datadog_checks.base.utils.functions import identity +from datadog_checks.base.utils.models import validation + +from . import defaults, validators + + +class MetricPatterns(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + frozen=True, + ) + exclude: Optional[tuple[str, ...]] = None + include: Optional[tuple[str, ...]] = None + + +class InstanceConfig(BaseModel): + model_config = ConfigDict( + validate_default=True, + arbitrary_types_allowed=True, + frozen=True, + ) + disable_generic_tags: Optional[bool] = None + empty_default_hostname: Optional[bool] = None + enable_legacy_tags_normalization: Optional[bool] = None + hypervisor: Optional[bool] = None + memory_entitlements: Optional[bool] = None + memory_stats: Optional[bool] = None + metric_patterns: Optional[MetricPatterns] = None + min_collection_interval: Optional[float] = None + name: Optional[str] = None + page_stats: Optional[bool] = None + service: Optional[str] = None + spurr_utilization: Optional[bool] = None + sudo: Optional[bool] = None + tags: Optional[tuple[str, ...]] = None + + @model_validator(mode='before') + def _initial_validation(cls, values): + return validation.core.initialize_config(getattr(validators, 'initialize_instance', identity)(values)) + + @field_validator('*', mode='before') + def _validate(cls, value, info): + field = cls.model_fields[info.field_name] + field_name = field.alias or info.field_name + if field_name in info.context['configured_fields']: + value = getattr(validators, f'instance_{info.field_name}', identity)(value, field=field) + else: + value = getattr(defaults, f'instance_{info.field_name}', lambda: value)() + + return validation.utils.make_immutable(value) + + @model_validator(mode='after') + def _final_validation(cls, model): + return validation.core.check_model(getattr(validators, 'check_instance', identity)(model)) diff --git a/lparstats/datadog_checks/lparstats/config_models/shared.py b/lparstats/datadog_checks/lparstats/config_models/shared.py new file mode 100644 index 0000000000000..10cab800f6c1e --- /dev/null +++ b/lparstats/datadog_checks/lparstats/config_models/shared.py @@ -0,0 +1,45 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, ConfigDict, field_validator, model_validator + +from datadog_checks.base.utils.functions import identity +from datadog_checks.base.utils.models import validation + +from . import validators + + +class SharedConfig(BaseModel): + model_config = ConfigDict( + validate_default=True, + arbitrary_types_allowed=True, + frozen=True, + ) + service: Optional[str] = None + + @model_validator(mode='before') + def _initial_validation(cls, values): + return validation.core.initialize_config(getattr(validators, 'initialize_shared', identity)(values)) + + @field_validator('*', mode='before') + def _validate(cls, value, info): + field = cls.model_fields[info.field_name] + field_name = field.alias or info.field_name + if field_name in info.context['configured_fields']: + value = getattr(validators, f'shared_{info.field_name}', identity)(value, field=field) + + return validation.utils.make_immutable(value) + + @model_validator(mode='after') + def _final_validation(cls, model): + return validation.core.check_model(getattr(validators, 'check_shared', identity)(model)) diff --git a/lparstats/datadog_checks/lparstats/config_models/validators.py b/lparstats/datadog_checks/lparstats/config_models/validators.py new file mode 100644 index 0000000000000..5e48f02a73da4 --- /dev/null +++ b/lparstats/datadog_checks/lparstats/config_models/validators.py @@ -0,0 +1,13 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# Here you can include additional config validators or transformers +# +# def initialize_instance(values, **kwargs): +# if 'my_option' not in values and 'my_legacy_option' in values: +# values['my_option'] = values['my_legacy_option'] +# if values.get('my_number') > 10: +# raise ValueError('my_number max value is 10, got %s' % str(values.get('my_number'))) +# +# return values diff --git a/lparstats/datadog_checks/lparstats/data/conf.yaml.example b/lparstats/datadog_checks/lparstats/data/conf.yaml.example new file mode 100644 index 0000000000000..c265498c4172d --- /dev/null +++ b/lparstats/datadog_checks/lparstats/data/conf.yaml.example @@ -0,0 +1,96 @@ +## All options defined here are available to all instances. +# +init_config: + + ## @param service - string - optional + ## Attach the tag `service:` to every metric, event, and service check emitted by this integration. + ## + ## Additionally, this sets the default `service` for every log source. + # + # service: + +## Every instance is scheduled independently of the others. +# +instances: + + - + ## @param name - string - optional - default: lparstats + ## A name for this instance. + # + # name: lparstats + + ## @param sudo - boolean - optional - default: false + ## Run lparstat with sudo. Requires adding dd-agent to the sudoers file: + ## + ## dd-agent ALL=(ALL) NOPASSWD: /usr/bin/lparstat + ## + ## When running as root, sudo is not needed. + # + # sudo: false + + ## @param memory_stats - boolean - optional - default: true + ## Collect physical memory and page statistics (lparstat -m). + # + # memory_stats: true + + ## @param page_stats - boolean - optional - default: true + ## Include page-level statistics (-pw flag). Requires memory_stats to be true. + # + # page_stats: true + + ## @param memory_entitlements - boolean - optional - default: true + ## Collect per-I/O-memory-pool entitlement stats (lparstat -m -eR). + ## Requires root or sudo. + # + # memory_entitlements: true + + ## @param hypervisor - boolean - optional - default: true + ## Collect hypervisor call statistics (lparstat -H). + ## Requires root or sudo. + # + # hypervisor: true + + ## @param spurr_utilization - boolean - optional - default: true + ## Collect SPURR physical processor utilization (lparstat -E). + # + # spurr_utilization: true + + ## @param tags - list of strings - optional + ## A list of tags to attach to every metric and service check emitted by this instance. + ## + ## Learn more about tagging at https://docs.datadoghq.com/tagging + # + # tags: + # - : + # - : + + ## @param service - string - optional + ## Attach the tag `service:` to every metric, event, and service check emitted by this integration. + ## + ## Overrides any `service` defined in the `init_config` section. + # + # service: + + ## @param min_collection_interval - number - optional - default: 15 + ## This changes the collection interval of the check. For more information, see: + ## https://docs.datadoghq.com/developers/write_agent_check/#collection-interval + # + # min_collection_interval: 15 + + ## @param empty_default_hostname - boolean - optional - default: false + ## This forces the check to send metrics with no hostname. + ## + ## This is useful for cluster-level checks. + # + # empty_default_hostname: false + + ## @param metric_patterns - mapping - optional + ## A mapping of metrics to include or exclude, with each entry being a regular expression. + ## + ## Metrics defined in `exclude` will take precedence in case of overlap. + # + # metric_patterns: + # include: + # - + # exclude: + # - diff --git a/lparstats/datadog_checks/lparstats/lparstats.py b/lparstats/datadog_checks/lparstats/lparstats.py new file mode 100644 index 0000000000000..922a64111e340 --- /dev/null +++ b/lparstats/datadog_checks/lparstats/lparstats.py @@ -0,0 +1,214 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +# +# Ported from datadog-unix-agent to integrations-core (Python 3, datadog_checks_base). +# Original: https://github.com/DataDog/datadog-unix-agent/tree/master/checks/bundled/lparstats + +import os +import subprocess + +from datadog_checks.base import AgentCheck + + +def _run_cmd(cmd: list[str], sudo: bool = False, timeout: float | None = None) -> tuple[str, str, int]: + """Run a command, optionally via sudo. Returns (stdout, stderr, returncode).""" + if sudo: + cmd = ['sudo'] + list(cmd) + try: + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + ) + return ( + result.stdout.decode('utf-8', errors='replace'), + result.stderr.decode('utf-8', errors='replace'), + result.returncode, + ) + except subprocess.TimeoutExpired: + return '', 'timeout', -1 + except Exception as e: + return '', str(e), -1 + + +def _lparstat_rows( + cmd: list[str], start_idx: int, sudo: bool = False, timeout: float | None = None +) -> tuple[list[str], str, int]: + """Run lparstat, filter blank lines, and return rows starting at start_idx.""" + output, stderr, rc = _run_cmd(cmd, sudo=sudo, timeout=timeout) + rows = [line for line in output.splitlines() if line][start_idx:] + return rows, stderr, rc + + +class LPARStats(AgentCheck): + SERVICE_CHECK_NAME = 'lparstats.can_collect' + + MEMORY_METRICS_START_IDX = 1 + HYPERVISOR_METRICS_START_IDX = 4 + HYPERVISOR_METRICS = ( + 'system.lpar.hypervisor.n_calls', + 'system.lpar.hypervisor.time.spent.total', + 'system.lpar.hypervisor.time.spent.hyp', + 'system.lpar.hypervisor.time.call.avg', + 'system.lpar.hypervisor.time.call.max', + ) + MEMORY_ENTITLEMENTS_START_IDX = 4 + SPURR_PROCESSOR_UTILIZATION_START_IDX = 3 + DEFAULT_TIMEOUT = 5 + + def check(self, instance: dict) -> None: + sudo = instance.get('sudo', False) + root = (hasattr(os, 'getuid') and os.getuid() == 0) or sudo + if not root: + self.log.info('Not running as root or sudo - entitlement and hypervisor metrics might be unavailable') + + timeout = self.DEFAULT_TIMEOUT + any_failed = False + + if instance.get('memory_stats', True): + if not self.collect_memory(instance.get('page_stats', True), sudo, timeout): + any_failed = True + if instance.get('memory_entitlements', True) and root: + if not self.collect_memory_entitlements(sudo, timeout): + any_failed = True + if instance.get('hypervisor', True) and root: + if not self.collect_hypervisor(sudo, timeout): + any_failed = True + if instance.get('spurr_utilization', True): + if not self.collect_spurr(sudo, timeout): + any_failed = True + + status = AgentCheck.CRITICAL if any_failed else AgentCheck.OK + self.service_check(self.SERVICE_CHECK_NAME, status) + + def collect_memory(self, page_stats: bool = True, sudo: bool = False, timeout: float | None = None) -> bool: + cmd = ['lparstat', '-m'] + if page_stats: + cmd.append('-pw') + cmd.extend(['1', '1']) + + stats, stderr, rc = _lparstat_rows(cmd, self.MEMORY_METRICS_START_IDX, sudo=sudo, timeout=timeout) + if rc != 0: + self.log.warning('lparstat -m failed (rc=%d): %s', rc, stderr.strip()) + return False + if len(stats) < 3: + self.log.warning('lparstat -m output too short, skipping memory metrics') + return False + fields = stats[0].split() + values = stats[2].split() + for idx, field in enumerate(fields): + if idx >= len(values): + break + try: + m = float(values[idx]) + if '%' in field: + field = field.replace('%', '') + self.gauge(f'system.lpar.memory.{field}', m) + except ValueError: + self.log.info("unable to convert %s to float - skipping", field) + return True + + def collect_hypervisor(self, sudo: bool = False, timeout: float | None = None) -> bool: + cmd = ['lparstat', '-H', '1', '1'] + stats, stderr, rc = _lparstat_rows(cmd, self.HYPERVISOR_METRICS_START_IDX, sudo=sudo, timeout=timeout) + if rc != 0: + self.log.warning('lparstat -H failed (rc=%d): %s', rc, stderr.strip()) + return False + if len(stats) < 1: + self.log.warning('lparstat -H output too short, skipping hypervisor metrics') + return False + for stat in stats: + values = stat.split() + if len(values) < 2: + continue + call_tag = f"call:{values[0]}" + for idx, entry in enumerate(values[1:]): + if idx >= len(self.HYPERVISOR_METRICS): + break + try: + metric_name = self.HYPERVISOR_METRICS[idx] + metric_value = float(entry) + self.gauge(metric_name, metric_value, tags=[call_tag]) + except ValueError: + self.log.info( + "unable to convert %s to float for %s - skipping", + self.HYPERVISOR_METRICS[idx], + call_tag, + ) + return True + + def collect_memory_entitlements(self, sudo: bool = False, timeout: float | None = None) -> bool: + cmd = ['lparstat', '-m', '-eR', '1', '1'] + stats, stderr, rc = _lparstat_rows(cmd, self.MEMORY_ENTITLEMENTS_START_IDX, sudo=sudo, timeout=timeout) + if rc != 0: + self.log.warning('lparstat -m -eR failed (rc=%d): %s', rc, stderr.strip()) + return False + if len(stats) < 2: + self.log.warning('lparstat -m -eR output too short, skipping entitlement metrics') + return False + fields = stats[0].split()[1:] + for stat in stats[1:]: + values = stat.split() + if len(values) < 2: + continue + tag = f"iompn:{values[0]}" + for idx, field in enumerate(fields): + if idx + 1 >= len(values): + break + try: + field = field.replace('%', '') + metric_name = f"system.lpar.memory.entitlement.{field}" + metric_value = float(values[idx + 1]) + self.gauge(metric_name, metric_value, tags=[tag]) + except ValueError: + self.log.info("unable to convert %s to float for %s - skipping", field, tag) + return True + + def collect_spurr(self, sudo: bool = False, timeout: float | None = None) -> bool: + cmd = ['lparstat', '-E', '1', '1'] + table, stderr, rc = _lparstat_rows(cmd, self.SPURR_PROCESSOR_UTILIZATION_START_IDX, sudo=sudo, timeout=timeout) + if rc != 0: + self.log.warning('lparstat -E failed (rc=%d): %s', rc, stderr.strip()) + return False + if len(table) < 3: + self.log.warning('lparstat -E output too short, skipping SPURR metrics') + return False + fields = table[0].split() + stats = table[2].split() + try: + norm_start = fields.index('freq') + 1 + except ValueError: + self.log.warning('lparstat -E output missing freq column; falling back to midpoint split') + norm_start = len(fields) // 2 + 1 + metrics = {} + total = 0 + total_norm = 0 + metric_tpl = "system.lpar.spurr.{}" + for idx, field in enumerate(fields): + if idx >= len(stats): + break + metric = metric_tpl.format(field) + if idx >= norm_start: + metric = f"{metric}.norm" + try: + metrics[metric] = float(stats[idx]) + except ValueError: + # freq field (e.g. "3.5GHz[100%]") is expected to fail + self.log.debug("unable to convert %s (%s) to float - skipping", field, stats[idx]) + continue + if 'norm' in metric: + total_norm += metrics[metric] + else: + total += metrics[metric] + + for metric, val in metrics.items(): + self.gauge(metric, val) + if 'norm' in metric: + denom = total_norm + else: + denom = total + if denom > 0: + self.gauge(f"{metric}.pct", val / denom) + return True diff --git a/lparstats/hatch.toml b/lparstats/hatch.toml new file mode 100644 index 0000000000000..a96bcebabb3be --- /dev/null +++ b/lparstats/hatch.toml @@ -0,0 +1,7 @@ +[env.collectors.datadog-checks] + +[envs.default] +e2e-env = false + +[[envs.default.matrix]] +python = ["3.13"] diff --git a/lparstats/manifest.json b/lparstats/manifest.json new file mode 100644 index 0000000000000..c24efc9b56927 --- /dev/null +++ b/lparstats/manifest.json @@ -0,0 +1,47 @@ +{ + "manifest_version": "2.0.0", + "app_uuid": "cf2e2f83-88de-4b2e-8363-071a9f09f0d8", + "app_id": "lparstats", + "owner": "agent-integrations", + "display_on_public_website": true, + "tile": { + "overview": "README.md#Overview", + "configuration": "README.md#Setup", + "support": "README.md#Support", + "changelog": "CHANGELOG.md", + "description": "Collect IBM POWER LPAR performance metrics from AIX systems via lparstat.", + "title": "LPARStats", + "media": [], + "classifier_tags": [ + "Category::OS & System", + "Offering::Integration" + ] + }, + "author": { + "support_email": "help@datadoghq.com", + "name": "Datadog", + "homepage": "https://www.datadoghq.com", + "sales_email": "info@datadoghq.com" + }, + "assets": { + "integration": { + "source_type_name": "LPARStats", + "source_type_id": 71534100, + "auto_install": true, + "configuration": { + "spec": "assets/configuration/spec.yaml" + }, + "events": { + "creates_events": false + }, + "metrics": { + "prefix": "system.lpar.", + "check": "system.lpar.memory.physb", + "metadata_path": "metadata.csv" + }, + "service_checks": { + "metadata_path": "assets/service_checks.json" + } + } + } +} diff --git a/lparstats/metadata.csv b/lparstats/metadata.csv new file mode 100644 index 0000000000000..ab77ce6eace22 --- /dev/null +++ b/lparstats/metadata.csv @@ -0,0 +1,42 @@ +metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name,curated_metric +system.lpar.hypervisor.n_calls,gauge,,,,Number of hypervisor calls.,0,lparstats,lpar hypervisor n_calls, +system.lpar.hypervisor.time.call.avg,gauge,,nanosecond,,Average hypervisor call time.,0,lparstats,lpar hypervisor time avg, +system.lpar.hypervisor.time.call.max,gauge,,nanosecond,,Maximum hypervisor call time.,0,lparstats,lpar hypervisor time max, +system.lpar.hypervisor.time.spent.hyp,gauge,,percent,,Percent hypervisor time spent.,0,lparstats,lpar hypervisor time hyp, +system.lpar.hypervisor.time.spent.total,gauge,,percent,,Percent total time spent in hypervisor calls.,0,lparstats,lpar hypervisor time total, +system.lpar.memory.ccol,gauge,,,,Compaction collection rate.,0,lparstats,lpar memory ccol, +system.lpar.memory.entc,gauge,,,,Entitlement consumed (%).,0,lparstats,lpar memory entc, +system.lpar.memory.entitlement.iodes,gauge,,,,I/O memory desired entitlement per pool.,0,lparstats,lpar entitlement iodes, +system.lpar.memory.entitlement.iohwm,gauge,,,,I/O memory high-water mark per pool.,0,lparstats,lpar entitlement iohwm, +system.lpar.memory.entitlement.iomaf,gauge,,,,I/O memory adjustment failures per pool.,0,lparstats,lpar entitlement iomaf, +system.lpar.memory.entitlement.iomin,gauge,,,,I/O memory minimum entitlement per pool.,0,lparstats,lpar entitlement iomin, +system.lpar.memory.entitlement.iomu,gauge,,,,I/O memory in use per pool.,0,lparstats,lpar entitlement iomu, +system.lpar.memory.entitlement.iores,gauge,,,,I/O memory reserved per pool.,0,lparstats,lpar entitlement iores, +system.lpar.memory.hpi,gauge,,,,Hard page-in rate.,0,lparstats,lpar memory hpi, +system.lpar.memory.hpit,gauge,,,,Hard page-in time.,0,lparstats,lpar memory hpit, +system.lpar.memory.iohwm,gauge,,,,I/O memory high-water mark (GB).,0,lparstats,lpar memory iohwm, +system.lpar.memory.iomaf,gauge,,,,I/O memory adjustment failures.,0,lparstats,lpar memory iomaf, +system.lpar.memory.iomf,gauge,,,,I/O memory free (GB).,0,lparstats,lpar memory iomf, +system.lpar.memory.iomin,gauge,,,,I/O memory minimum (GB).,0,lparstats,lpar memory iomin, +system.lpar.memory.iomu,gauge,,,,I/O memory in use (GB).,0,lparstats,lpar memory iomu, +system.lpar.memory.mpgcol,gauge,,,,Minor page collection rate.,0,lparstats,lpar memory mpgcol, +system.lpar.memory.pgcol,gauge,,,,Page collection rate.,0,lparstats,lpar memory pgcol, +system.lpar.memory.physb,gauge,,,,Physical memory busy (allocated).,0,lparstats,lpar memory physb,memory +system.lpar.memory.pmem,gauge,,,,Physical memory in use (GB).,0,lparstats,lpar memory pmem, +system.lpar.memory.vcsw,gauge,,,,Voluntary context switches.,0,lparstats,lpar memory vcsw, +system.lpar.spurr.idle,gauge,,,,SPURR actual idle processor utilization.,0,lparstats,lpar spurr idle, +system.lpar.spurr.idle.norm,gauge,,,,SPURR normalized idle processor utilization.,0,lparstats,lpar spurr idle norm, +system.lpar.spurr.idle.norm.pct,gauge,,fraction,,SPURR normalized idle processor utilization as a fraction of total.,0,lparstats,lpar spurr idle norm pct, +system.lpar.spurr.idle.pct,gauge,,fraction,,SPURR actual idle processor utilization as a fraction of total.,0,lparstats,lpar spurr idle pct, +system.lpar.spurr.sys,gauge,,,,SPURR actual system processor utilization.,0,lparstats,lpar spurr sys, +system.lpar.spurr.sys.norm,gauge,,,,SPURR normalized system processor utilization.,0,lparstats,lpar spurr sys norm, +system.lpar.spurr.sys.norm.pct,gauge,,fraction,,SPURR normalized system processor utilization as a fraction of total.,0,lparstats,lpar spurr sys norm pct, +system.lpar.spurr.sys.pct,gauge,,fraction,,SPURR actual system processor utilization as a fraction of total.,0,lparstats,lpar spurr sys pct, +system.lpar.spurr.user,gauge,,,,SPURR actual user processor utilization.,0,lparstats,lpar spurr user, +system.lpar.spurr.user.norm,gauge,,,,SPURR normalized user processor utilization.,0,lparstats,lpar spurr user norm, +system.lpar.spurr.user.norm.pct,gauge,,fraction,,SPURR normalized user processor utilization as a fraction of total.,0,lparstats,lpar spurr user norm pct, +system.lpar.spurr.user.pct,gauge,,fraction,,SPURR actual user processor utilization as a fraction of total.,0,lparstats,lpar spurr user pct, +system.lpar.spurr.wait,gauge,,,,SPURR actual wait processor utilization.,0,lparstats,lpar spurr wait, +system.lpar.spurr.wait.norm,gauge,,,,SPURR normalized wait processor utilization.,0,lparstats,lpar spurr wait norm, +system.lpar.spurr.wait.norm.pct,gauge,,fraction,,SPURR normalized wait processor utilization as a fraction of total.,0,lparstats,lpar spurr wait norm pct, +system.lpar.spurr.wait.pct,gauge,,fraction,,SPURR actual wait processor utilization as a fraction of total.,0,lparstats,lpar spurr wait pct, diff --git a/lparstats/pyproject.toml b/lparstats/pyproject.toml new file mode 100644 index 0000000000000..82c404e199d5a --- /dev/null +++ b/lparstats/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["hatchling>=0.11.2"] +build-backend = "hatchling.build" + +[project] +name = "datadog-lparstats" +description = "The LPARStats check" +readme = "README.md" +keywords = [ + "datadog", + "datadog agent", + "datadog check", + "lparstats", + "aix", + "lpar", + "ibm power", +] +authors = [{ name = "Datadog", email = "packages@datadoghq.com" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3.13", + "Topic :: System :: Monitoring", + "Private :: Do Not Upload", +] +dependencies = ["datadog-checks-base"] +dynamic = ["version"] +license = "BSD-3-Clause" + +[project.optional-dependencies] +deps = [] + +[project.urls] +Source = "https://github.com/DataDog/integrations-core" + +[tool.hatch.version] +path = "datadog_checks/lparstats/__about__.py" + +[tool.hatch.build.targets.sdist] +include = ["/datadog_checks", "/tests", "/manifest.json"] + +[tool.hatch.build.targets.wheel] +include = ["/datadog_checks/lparstats"] +dev-mode-dirs = ["."] diff --git a/lparstats/tests/__init__.py b/lparstats/tests/__init__.py new file mode 100644 index 0000000000000..75c6647cb9233 --- /dev/null +++ b/lparstats/tests/__init__.py @@ -0,0 +1,3 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) diff --git a/lparstats/tests/conftest.py b/lparstats/tests/conftest.py new file mode 100644 index 0000000000000..76ccefd7f23d1 --- /dev/null +++ b/lparstats/tests/conftest.py @@ -0,0 +1,19 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +import pytest + +CHECK_NAME = 'lparstats' + + +@pytest.fixture +def instance(): + return { + 'name': CHECK_NAME, + 'memory_stats': True, + 'page_stats': False, + 'memory_entitlements': False, + 'hypervisor': False, + 'spurr_utilization': True, + } diff --git a/lparstats/tests/test_check.py b/lparstats/tests/test_check.py new file mode 100644 index 0000000000000..7916e231d7cca --- /dev/null +++ b/lparstats/tests/test_check.py @@ -0,0 +1,150 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from unittest.mock import MagicMock, patch + +from datadog_checks.base import AgentCheck +from datadog_checks.lparstats import LPARStats + +MEMORY_OUTPUT = """\ + +System configuration: lcpu=8 mem=4096MB mpsz=0.00GB iome=4096.00MB iomp=9 ent=0.20 + +physb hpi hpit pmem iomin iomu iomf iohwm iomaf %entc vcsw +----- ----- ----- ----- ------ ------ ------ ------ ----- ----- ----- + 1.20 0 0 4.00 62.2 - - - 0 15.2 427 +""" + +SPURR_OUTPUT = """\ + +System configuration: type=Shared mode=Uncapped smt=4 lcpu=8 mem=4096MB ent=0.20 Power=Disabled + +Physical Processor Utilisation: + + --------Actual-------- ------Normalised------ + user sys wait idle freq user sys wait idle + ---- ---- ---- ---- --------- ---- ---- ---- ---- +0.015 0.012 0.000 0.172 3.5GHz[100%] 0.015 0.012 0.000 0.172 +""" + +HYPERVISOR_OUTPUT = """\ + +System configuration: lcpu=8 mem=4096MB ent=0.20 + +Hypervisor calls: + Call N_calls spent.total spent.hyp call.avg call.max + ----- ------- ----------- --------- -------- -------- + mmap 12345 2.50 0.80 0.002 0.010 +""" + +ENTITLEMENTS_OUTPUT = """\ + +System configuration: lcpu=8 mem=4096MB ent=0.20 + +I/O Memory Entitlement: + per pool +----- ----- ----- ---- ----- ----- ----- + iompn iodes iomin iomu iomaf iohwm iores + P1 16.0 8.0 10.0 0.0 12.0 16.0 +""" + + +def _make_proc(stdout=''): + proc = MagicMock() + proc.stdout = stdout.encode('utf-8') + proc.stderr = b'' + proc.returncode = 0 + return proc + + +def _mock_subprocess_run(cmd, **kwargs): + if '-m' in cmd and '-eR' not in cmd: + return _make_proc(MEMORY_OUTPUT) + if '-E' in cmd: + return _make_proc(SPURR_OUTPUT) + if '-H' in cmd: + return _make_proc(HYPERVISOR_OUTPUT) + if '-eR' in cmd: + return _make_proc(ENTITLEMENTS_OUTPUT) + return _make_proc() + + +def test_check_runs(aggregator, dd_run_check, instance): + check = LPARStats('lparstats', {}, [instance]) + with patch('datadog_checks.lparstats.lparstats.subprocess.run', side_effect=_mock_subprocess_run): + dd_run_check(check) + + # Memory metrics (no tags expected) + aggregator.assert_metric('system.lpar.memory.physb', value=1.20, tags=[]) + aggregator.assert_metric('system.lpar.memory.entc', value=15.2, tags=[]) + + # SPURR metrics (no tags expected) + aggregator.assert_metric('system.lpar.spurr.user', value=0.015, tags=[]) + aggregator.assert_metric('system.lpar.spurr.idle', value=0.172, tags=[]) + aggregator.assert_metric('system.lpar.spurr.user.pct', tags=[]) + + aggregator.assert_service_check('lparstats.can_collect', status=AgentCheck.OK) + + +def test_lparstat_command_failure(aggregator, instance): + """Service check is CRITICAL when lparstat exits non-zero.""" + check = LPARStats('lparstats', {}, [instance]) + failed_proc = _make_proc('') + failed_proc.returncode = 1 + with patch('datadog_checks.lparstats.lparstats.subprocess.run', return_value=failed_proc): + check.check(instance) + aggregator.assert_service_check('lparstats.can_collect', status=AgentCheck.CRITICAL) + assert len(aggregator.metrics('system.lpar.memory.physb')) == 0 + + +def test_hypervisor_and_entitlements(aggregator, dd_run_check): + """Hypervisor and memory-entitlement collectors emit metrics with call/iompn tags.""" + inst = { + 'name': 'lparstats', + 'memory_stats': False, + 'page_stats': False, + 'memory_entitlements': True, + 'hypervisor': True, + 'spurr_utilization': False, + 'sudo': True, # makes root=True so both collectors are activated + } + check = LPARStats('lparstats', {}, [inst]) + with patch('datadog_checks.lparstats.lparstats.subprocess.run', side_effect=_mock_subprocess_run): + dd_run_check(check) + + aggregator.assert_metric('system.lpar.hypervisor.n_calls', value=12345.0, tags=['call:mmap']) + aggregator.assert_metric('system.lpar.hypervisor.time.spent.total', value=2.50, tags=['call:mmap']) + aggregator.assert_metric('system.lpar.memory.entitlement.iodes', value=16.0, tags=['iompn:P1']) + aggregator.assert_metric('system.lpar.memory.entitlement.iomin', value=8.0, tags=['iompn:P1']) + + +def test_memory_output_too_short(aggregator, instance): + check = LPARStats('lparstats', {}, [instance]) + with patch('datadog_checks.lparstats.lparstats.subprocess.run', return_value=_make_proc('')): + check.check(instance) + # No metrics should be emitted for empty output + assert len(aggregator.metrics('system.lpar.memory.physb')) == 0 + + +def test_spurr_zero_total(aggregator, instance): + """SPURR pct metrics should not be emitted when total is 0 (avoid div-by-zero).""" + zero_spurr = """\ + +System configuration: ... + +Physical Processor Utilisation: + + --------Actual-------- ------Normalised------ + user sys wait idle freq user sys wait idle + ---- ---- ---- ---- --------- ---- ---- ---- ---- +0.000 0.000 0.000 0.000 3.5GHz[100%] 0.000 0.000 0.000 0.000 +""" + check = LPARStats('lparstats', {}, [instance]) + with patch( + 'datadog_checks.lparstats.lparstats.subprocess.run', + side_effect=lambda cmd, **kw: _make_proc(zero_spurr) if '-E' in cmd else _make_proc(''), + ): + check.check(instance) + # .pct metrics should not be emitted when total is 0 + assert len(aggregator.metrics('system.lpar.spurr.user.pct')) == 0