diff --git a/.ddev/config.toml b/.ddev/config.toml index 5bfba9b7f9..89ada397f9 100644 --- a/.ddev/config.toml +++ b/.ddev/config.toml @@ -7,6 +7,7 @@ storm = "Storm" [overrides.metrics-prefix] aerospike_enterprise = "aerospike." snmpwalk = "snmpwalk." +stonebranch = "stonebranch." warpstream = "warpstream." [overrides.is-integration] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e1a4e0a4f2..db6621e2e8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -220,6 +220,7 @@ code-coverage.datadog.yml @DataDog/agent-integr /statsig/ @marcos-statsig support@statsig.com @DataDog/ecosystems-review /statsig_rum/ @crstatsig support@statig.com /steadybit/ @bripkens support@steadybit.com @DataDog/ecosystems-review +/stonebranch/ @len06080 len-maurice.seemann@stonebranch.com /storm/ @platinummonkey /stormforge/ @bradbeam @tperol support@stormforge.io @DataDog/ecosystems-review /superwise/ @oryan-omer support@superwise.ai @DataDog/ecosystems-review diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index 0660e34791..fad247cfb0 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -1412,6 +1412,25 @@ jobs: test-py3: ${{ inputs.test-py3 }} setup-env-vars: "${{ inputs.setup-env-vars }}" secrets: inherit + j712c9df: + uses: DataDog/integrations-core/.github/workflows/test-target.yml@5f6b58dfb609ea0afb37762612dbb6e2c37d2933 + with: + job-name: stonebranch + target: stonebranch + 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 }} + setup-env-vars: "${{ inputs.setup-env-vars }}" + secrets: inherit j5870f9b: uses: DataDog/integrations-core/.github/workflows/test-target.yml@5f6b58dfb609ea0afb37762612dbb6e2c37d2933 with: diff --git a/stonebranch/CHANGELOG.md b/stonebranch/CHANGELOG.md new file mode 100644 index 0000000000..7c3b41dc44 --- /dev/null +++ b/stonebranch/CHANGELOG.md @@ -0,0 +1,6 @@ +# CHANGELOG - Stonebranch + +## 1.0.0 / 2025-12-15 + +***Added:*** +* Initial Release diff --git a/stonebranch/README.md b/stonebranch/README.md new file mode 100644 index 0000000000..5b2a96d836 --- /dev/null +++ b/stonebranch/README.md @@ -0,0 +1,148 @@ +# Agent Check: Stonebranch + +## Overview + +Stonebranch Universal Automation Center (UAC) is an enterprise workload automation platform used to orchestrate, schedule, and monitor complex workflows across distributed systems, applications, and infrastructure. + +The Stonebranch integration provides visibility into UAC by scraping metrics exposed by the Universal Controller Prometheus endpoint and surfacing them in Datadog. This enables operations, SRE, and platform teams to monitor automation health, execution activity, and system capacity from a single observability platform. + +The integration automatically collects a curated set of core UC metrics and supports optional metric groups for deeper JVM, process, and license-level insights. Metrics are enriched with labels emitted by the UC, enabling Datadog users to build workflow-, agent-, and controller-centric views and dashboards. + +The integration collects the following Datadog telemetry types: +- **Metrics**: Task executions, workflow outcomes, agent and OMS status, controller health, database pools, and license usage. + +With this integration, teams can proactively detect automation failures, capacity constraints, and infrastructure issues impacting workload execution. + + +## Setup + +The Stonebranch check is not included in the [Datadog Agent][1] package and must be installed separately. + +### Installation + +Install the Stonebranch integration using the Datadog Agent integration command. + +1. Run the following command to install the integration: +```shell + datadog-agent integration install -t datadog-stonebranch== +``` + +### Configuration + +1. Edit the `stonebranch.d/conf.yaml` file in the `conf.d/` directory of your Datadog Agent configuration. + + The Universal Controller exposes metrics via its Prometheus endpoint and the Agent must be able to authenticate to it. + + Minimal configuration example: +```yaml + instances: + - openmetrics_endpoint: https://stonebranch.controller/uc/resources/metrics + + auth_type: basic + username: + password: + + metric_groups: + - jvm + - process + + tags: + - env:prod + - service:stonebranch +``` + + * Default metrics are always collected. + * Optional metric groups can be enabled using `metric_groups`. + * Individual metrics can be excluded using `exclude_metric_names`. + + See the sample `stonebranch.d/conf.yaml.example` file for the full list of available configuration options and check the [Stonebranch documentation][2] for the complete list of available metrics. + +2. Restart the Datadog Agent. + + +### Validation + +Run the Agent status command and verify that `stonebranch` appears under the **Checks** section: +```shell +datadog-agent status +``` + + +## Metric Groups (Opt-in) + +The Stonebranch integration always collects a curated set of core Universal Automation Center (UAC) metrics by default. These metrics provide visibility into task execution, workflows, controller health, agents, OMS connectivity, and overall system status. + +Additional metrics are organized into **opt-in metric groups**. These groups allow you to selectively enable deeper visibility without collecting unnecessary metrics by default. + +Metric groups are enabled using the `metric_groups` option in the instance configuration. + +### Available Metric Groups + +The following metric groups are supported: + +* **jvm** + Collects JVM-level metrics for the Universal Controller, including thread counts and memory usage. + +* **process** + Collects operating system process metrics such as CPU usage, resident memory, and open file descriptors for controller processes. + +* **license_details** + Collects detailed license consumption metrics, including agent counts, cluster nodes, task definitions, and monthly execution usage. + +### Enabling Metric Groups + +To enable one or more metric groups, add the `metric_groups` option to your instance configuration: +```yaml +instances: + - openmetrics_endpoint: https://stonebranch.controller/uc/resources/metrics + + auth_type: basic + username: + password: + + metric_groups: + - jvm + - process +``` + +Metric groups are additive. When enabled, the metrics in the selected groups are collected in addition to the default Stonebranch metrics. + +### Excluding Individual Metrics + +If you want to exclude specific metrics after defaults and metric groups have been expanded, use the `exclude_metric_names` option: +```yaml +exclude_metric_names: + - uc_build_info +``` + +This option removes metrics by exact Prometheus metric name and is applied after all default and group-based metrics have been selected. + +### Notes + +* Default Stonebranch metrics are collected unless explicitly excluded using `exclude_metric_names`. +* Metric groups are optional and disabled unless explicitly configured. +* Advanced OpenMetrics filtering options (such as regex-based exclusions and label-based filtering) are available through standard OpenMetrics configuration options. See the sample configuration file for details. + + +## Data Collected + +### Metrics + +The integration collects a curated set of Stonebranch metrics by default, including task execution counts, workflow status, agent and OMS health, and controller-level statistics. + +Optional metric groups provide additional coverage: + +* JVM metrics +* Process-level resource metrics +* License consumption and capacity metrics + +See `metadata.csv` for the complete list of metrics exposed by this integration. + +## Support + +Need help? Contact [Stonebranch support][3]. + + +[1]: https://github.com/DataDog/integrations-extras +[2]: https://stonebranchdocs.atlassian.net/wiki/spaces/UC79/pages/1614446871/Universal+Controller+-+Provided+Metrics +[3]: https://www.stonebranch.com/contact \ No newline at end of file diff --git a/stonebranch/assets/configuration/spec.yaml b/stonebranch/assets/configuration/spec.yaml new file mode 100644 index 0000000000..6c3a6a8e3a --- /dev/null +++ b/stonebranch/assets/configuration/spec.yaml @@ -0,0 +1,38 @@ +name: Stonebranch +files: + - name: stonebranch.yaml + options: + - template: init_config + options: + - template: init_config/openmetrics + + - template: instances + options: + - template: instances/openmetrics + + - name: metric_groups + description: | + Opt-in metric groups to scrape in addition to the default Stonebranch metrics. + Allowed values: `jvm`, `process`, `license_details`. + value: + type: array + items: + type: string + example: + - jvm + - process + compact_example: true + enabled: false + + - name: exclude_metric_names + description: | + Metrics to exclude by exact Prometheus metric name. + (This is applied after defaults and metric groups are expanded.) + value: + type: array + items: + type: string + example: + - uc_build_info + compact_example: true + enabled: false diff --git a/stonebranch/assets/dashboards/stonebranch_overview.json b/stonebranch/assets/dashboards/stonebranch_overview.json new file mode 100644 index 0000000000..64a7c39821 --- /dev/null +++ b/stonebranch/assets/dashboards/stonebranch_overview.json @@ -0,0 +1,636 @@ +{ + "title": "Stonebranch Overview", + "description": "Monitors Stonebranch Universal Automation Center task execution, infrastructure health, and license consumption.", + "widgets": [ + { + "id": 1, + "definition": { + "type": "group", + "title": "About Stonebranch", + "layout_type": "ordered", + "widgets": [ + { + "id": 2, + "definition": { + "type": "note", + "content": "## Stonebranch Universal Automation Center\n\nStonebranch UAC is an enterprise workload automation platform. This dashboard provides visibility into task execution outcomes, agent and OMS connectivity, and license utilization.\n\n**Useful links:**\n- [Stonebranch Documentation](https://docs.stonebranch.com)\n- [Datadog Integration Setup](https://docs.datadoghq.com)", + "background_color": "white", + "font_size": "14", + "text_align": "left", + "show_tick": false, + "tick_pos": "50%", + "tick_edge": "left" + } + } + ] + } + }, + { + "id": 10, + "definition": { + "type": "group", + "title": "Overview", + "layout_type": "ordered", + "widgets": [ + { + "id": 11, + "definition": { + "type": "query_value", + "title": "Active Task Instances", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "sum:stonebranch.uc_task_instance.active{$endpoint}", + "aggregator": "last" + } + ], + "autoscale": true, + "precision": 0, + "timeseries_background": { + "type": "bars" + } + } + }, + { + "id": 12, + "definition": { + "type": "query_value", + "title": "OMS Servers Connected", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "sum:stonebranch.uc_oms_server.status{$endpoint}", + "aggregator": "last", + "conditional_formats": [ + { + "comparator": "<", + "value": 1, + "palette": "red_on_white" + }, + { + "comparator": ">=", + "value": 1, + "palette": "green_on_white" + } + ] + } + ], + "autoscale": false, + "precision": 0, + "timeseries_background": { + "type": "area" + } + } + }, + { + "id": 13, + "definition": { + "type": "query_value", + "title": "Tasks Launched (Last Hour)", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "sum:stonebranch.uc_task_instance.launch.total.count{$endpoint}.as_count()", + "aggregator": "sum" + } + ], + "autoscale": true, + "precision": 0, + "timeseries_background": { + "type": "bars" + } + } + }, + { + "id": 14, + "definition": { + "type": "monitor_summary", + "title": "Stonebranch Monitors", + "title_size": "16", + "title_align": "left", + "display_format": "countsAndList", + "color_preference": "text", + "hide_zero_counts": true, + "show_last_triggered": false, + "query": "tag:integration:stonebranch", + "sort": "status,asc", + "count": 50, + "start": 0 + } + } + ] + } + }, + { + "id": 20, + "definition": { + "type": "group", + "title": "Task Execution", + "layout_type": "ordered", + "widgets": [ + { + "id": 21, + "definition": { + "type": "timeseries", + "title": "Task Execution History by Status", + "title_size": "16", + "title_align": "left", + "show_legend": true, + "legend_layout": "auto", + "legend_columns": ["avg", "sum"], + "requests": [ + { + "q": "sum:stonebranch.uc_history.total.count{$endpoint} by {task_instance_status}.as_count()", + "display_type": "bars", + "style": { + "palette": "dog_classic", + "line_type": "solid", + "line_width": "normal" + } + } + ], + "yaxis": { + "scale": "linear", + "include_zero": true + } + } + }, + { + "id": 22, + "definition": { + "type": "sunburst", + "title": "Task Outcomes Distribution", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "sum:stonebranch.uc_history.total.count{$endpoint} by {task_instance_status}.as_count()", + "style": { + "palette": "dog_classic" + } + } + ], + "legend": { + "type": "table" + } + } + }, + { + "id": 23, + "definition": { + "type": "timeseries", + "title": "Late Starts and Late Finishes", + "title_size": "16", + "title_align": "left", + "show_legend": true, + "legend_layout": "auto", + "legend_columns": ["avg", "sum"], + "requests": [ + { + "q": "sum:stonebranch.uc_task_instance.late_start.total.count{$endpoint}.as_count()", + "display_type": "bars", + "style": { + "palette": "warm", + "line_type": "solid", + "line_width": "normal" + }, + "metadata": [ + { + "expression": "sum:stonebranch.uc_task_instance.late_start.total.count{$endpoint}.as_count()", + "alias": "Late Starts" + } + ] + }, + { + "q": "sum:stonebranch.uc_task_instance.late_finish.total.count{$endpoint}.as_count()", + "display_type": "bars", + "style": { + "palette": "orange", + "line_type": "solid", + "line_width": "normal" + }, + "metadata": [ + { + "expression": "sum:stonebranch.uc_task_instance.late_finish.total.count{$endpoint}.as_count()", + "alias": "Late Finishes" + } + ] + } + ], + "yaxis": { + "scale": "linear", + "include_zero": true + } + } + }, + { + "id": 24, + "definition": { + "type": "timeseries", + "title": "Active Task Instances", + "title_size": "16", + "title_align": "left", + "show_legend": true, + "legend_layout": "auto", + "legend_columns": ["avg", "max"], + "requests": [ + { + "q": "sum:stonebranch.uc_task_instance.active{$endpoint} by {task_instance_status}", + "display_type": "area", + "style": { + "palette": "dog_classic", + "line_type": "solid", + "line_width": "normal" + } + } + ], + "yaxis": { + "scale": "linear", + "include_zero": true + } + } + } + ] + } + }, + { + "id": 30, + "definition": { + "type": "group", + "title": "License Consumption", + "layout_type": "ordered", + "widgets": [ + { + "id": 31, + "definition": { + "type": "query_value", + "title": "Distributed Agents (Used / Max)", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "avg:stonebranch.uc_license.agents_distributed.used{$endpoint} / avg:stonebranch.uc_license.agents_distributed.max{$endpoint} * 100", + "aggregator": "last", + "conditional_formats": [ + { + "comparator": ">=", + "value": 95, + "palette": "red_on_white" + }, + { + "comparator": ">=", + "value": 80, + "palette": "yellow_on_white" + }, + { + "comparator": "<", + "value": 80, + "palette": "green_on_white" + } + ] + } + ], + "autoscale": false, + "custom_unit": "%", + "precision": 1, + "timeseries_background": { + "type": "area" + } + } + }, + { + "id": 32, + "definition": { + "type": "query_value", + "title": "z/OS Agents (Used / Max)", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "avg:stonebranch.uc_license.agents_zos.used{$endpoint} / avg:stonebranch.uc_license.agents_zos.max{$endpoint} * 100", + "aggregator": "last", + "conditional_formats": [ + { + "comparator": ">=", + "value": 95, + "palette": "red_on_white" + }, + { + "comparator": ">=", + "value": 80, + "palette": "yellow_on_white" + }, + { + "comparator": "<", + "value": 80, + "palette": "green_on_white" + } + ] + } + ], + "autoscale": false, + "custom_unit": "%", + "precision": 1, + "timeseries_background": { + "type": "area" + } + } + }, + { + "id": 33, + "definition": { + "type": "query_value", + "title": "Cluster Nodes (Used / Max)", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "avg:stonebranch.uc_license.cluster_nodes.used{$endpoint} / avg:stonebranch.uc_license.cluster_nodes.max{$endpoint} * 100", + "aggregator": "last", + "conditional_formats": [ + { + "comparator": ">=", + "value": 95, + "palette": "red_on_white" + }, + { + "comparator": ">=", + "value": 80, + "palette": "yellow_on_white" + }, + { + "comparator": "<", + "value": 80, + "palette": "green_on_white" + } + ] + } + ], + "autoscale": false, + "custom_unit": "%", + "precision": 1, + "timeseries_background": { + "type": "area" + } + } + }, + { + "id": 34, + "definition": { + "type": "query_value", + "title": "Monthly Executions (Used / Max)", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "avg:stonebranch.uc_license.monthly_executions.used{$endpoint} / avg:stonebranch.uc_license.monthly_executions.max{$endpoint} * 100", + "aggregator": "last", + "conditional_formats": [ + { + "comparator": ">=", + "value": 95, + "palette": "red_on_white" + }, + { + "comparator": ">=", + "value": 85, + "palette": "yellow_on_white" + }, + { + "comparator": "<", + "value": 85, + "palette": "green_on_white" + } + ] + } + ], + "autoscale": false, + "custom_unit": "%", + "precision": 1, + "timeseries_background": { + "type": "area" + } + } + }, + { + "id": 35, + "definition": { + "type": "query_value", + "title": "Task Definitions (Used / Max)", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "avg:stonebranch.uc_license.task_definitions.used{$endpoint} / avg:stonebranch.uc_license.task_definitions.max{$endpoint} * 100", + "aggregator": "last", + "conditional_formats": [ + { + "comparator": ">=", + "value": 95, + "palette": "red_on_white" + }, + { + "comparator": ">=", + "value": 80, + "palette": "yellow_on_white" + }, + { + "comparator": "<", + "value": 80, + "palette": "green_on_white" + } + ] + } + ], + "autoscale": false, + "custom_unit": "%", + "precision": 1, + "timeseries_background": { + "type": "area" + } + } + }, + { + "id": 36, + "definition": { + "type": "timeseries", + "title": "Monthly Executions Over Time", + "title_size": "16", + "title_align": "left", + "show_legend": true, + "legend_layout": "auto", + "legend_columns": ["avg", "max"], + "requests": [ + { + "q": "avg:stonebranch.uc_license.monthly_executions.used{$endpoint}", + "display_type": "line", + "style": { + "palette": "cool", + "line_type": "solid", + "line_width": "normal" + }, + "metadata": [ + { + "expression": "avg:stonebranch.uc_license.monthly_executions.used{$endpoint}", + "alias": "Used" + } + ] + }, + { + "q": "avg:stonebranch.uc_license.monthly_executions.max{$endpoint}", + "display_type": "line", + "style": { + "palette": "grey", + "line_type": "dashed", + "line_width": "normal" + }, + "metadata": [ + { + "expression": "avg:stonebranch.uc_license.monthly_executions.max{$endpoint}", + "alias": "Licensed Max" + } + ] + } + ], + "yaxis": { + "scale": "linear", + "include_zero": true + } + } + } + ] + } + }, + { + "id": 40, + "definition": { + "type": "group", + "title": "Infrastructure Health", + "layout_type": "ordered", + "widgets": [ + { + "id": 41, + "definition": { + "type": "timeseries", + "title": "OMS Server Status by Device", + "title_size": "16", + "title_align": "left", + "show_legend": true, + "legend_layout": "auto", + "legend_columns": ["avg", "min"], + "requests": [ + { + "q": "avg:stonebranch.uc_oms_server.status{$endpoint} by {device}", + "display_type": "line", + "style": { + "palette": "dog_classic", + "line_type": "solid", + "line_width": "thick" + } + } + ], + "yaxis": { + "scale": "linear", + "include_zero": true, + "min": "0", + "max": "2" + } + } + }, + { + "id": 42, + "definition": { + "type": "timeseries", + "title": "Database Connection Pool (Active vs Max)", + "title_size": "16", + "title_align": "left", + "show_legend": true, + "legend_layout": "auto", + "legend_columns": ["avg", "max"], + "requests": [ + { + "q": "avg:stonebranch.uc_database_connection_pool.active{$endpoint} by {pool}", + "display_type": "bars", + "style": { + "palette": "cool", + "line_type": "solid", + "line_width": "normal" + }, + "metadata": [ + { + "expression": "avg:stonebranch.uc_database_connection_pool.active{$endpoint} by {pool}", + "alias": "Active" + } + ] + }, + { + "q": "avg:stonebranch.uc_database_connection_pool.max{$endpoint} by {pool}", + "display_type": "line", + "style": { + "palette": "red", + "line_type": "dashed", + "line_width": "normal" + }, + "metadata": [ + { + "expression": "avg:stonebranch.uc_database_connection_pool.max{$endpoint} by {pool}", + "alias": "Max" + } + ] + } + ], + "yaxis": { + "scale": "linear", + "include_zero": true + } + } + }, + { + "id": 43, + "definition": { + "type": "toplist", + "title": "Agent Status", + "title_size": "16", + "title_align": "left", + "requests": [ + { + "q": "top(avg:stonebranch.uc_agent.status{$endpoint} by {agent_id}, 25, 'last', 'asc')", + "style": { + "palette": "dog_classic" + }, + "conditional_formats": [ + { + "comparator": "<", + "value": 1, + "palette": "red_on_white" + }, + { + "comparator": ">=", + "value": 1, + "palette": "green_on_white" + } + ] + } + ] + } + } + ] + } + } + ], + "template_variables": [ + { + "name": "endpoint", + "prefix": "endpoint", + "available_values": [], + "default": "*" + } + ], + "layout_type": "ordered", + "is_read_only": false, + "notify_list": [], + "reflow_type": "fixed" +} diff --git a/stonebranch/assets/monitors/agent_offline.json b/stonebranch/assets/monitors/agent_offline.json new file mode 100644 index 0000000000..85d0682834 --- /dev/null +++ b/stonebranch/assets/monitors/agent_offline.json @@ -0,0 +1,25 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch agent is offline", + "tags": ["integration:stonebranch"], + "description": "Alerts when a Stonebranch Universal Agent reports an offline status. Offline agents cannot accept or execute tasks.", + "definition": { + "name": "[Stonebranch] Agent {{agent_id.name}} is offline on {{endpoint.name}}", + "type": "query alert", + "query": "min(last_5m):avg:stonebranch.uc_agent.status{*} by {endpoint,agent_id} < 1", + "message": "Stonebranch Universal Agent {{agent_id.name}} on {{endpoint.name}} is reporting offline status.\n\nVerify the agent process is running and it can connect to the OMS server.\n\n@slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 1 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/db_pool_exhaustion.json b/stonebranch/assets/monitors/db_pool_exhaustion.json new file mode 100644 index 0000000000..0c604998eb --- /dev/null +++ b/stonebranch/assets/monitors/db_pool_exhaustion.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch database connection pool utilization is high", + "tags": ["integration:stonebranch"], + "description": "Alerts when the database connection pool utilization approaches its maximum capacity, which can cause task execution delays or failures.", + "definition": { + "name": "[Stonebranch] DB connection pool {{pool.name}} is near capacity on {{endpoint.name}}", + "type": "query alert", + "query": "max(last_5m):avg:stonebranch.uc_database_connection_pool.active{*} by {endpoint,pool} / avg:stonebranch.uc_database_connection_pool.max{*} by {endpoint,pool} > 0.95", + "message": "The Stonebranch database connection pool {{pool.name}} on {{endpoint.name}} is near or at capacity.\n\nActive connections are {{value}} of the maximum. Consider increasing the pool size or investigating long-running transactions.\n\n@pagerduty @slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 0.95, + "warning": 0.8 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/license_cluster_nodes.json b/stonebranch/assets/monitors/license_cluster_nodes.json new file mode 100644 index 0000000000..e9bd08ad1f --- /dev/null +++ b/stonebranch/assets/monitors/license_cluster_nodes.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch cluster node license utilization is high", + "tags": ["integration:stonebranch"], + "description": "Alerts when cluster node license consumption approaches the licensed maximum, which may prevent adding additional controller nodes.", + "definition": { + "name": "[Stonebranch] Cluster node license utilization is high on {{endpoint.name}}", + "type": "query alert", + "query": "max(last_15m):avg:stonebranch.uc_license.cluster_nodes.used{*} by {endpoint} / avg:stonebranch.uc_license.cluster_nodes.max{*} by {endpoint} > 0.95", + "message": "Stonebranch cluster node license utilization on {{endpoint.name}} has exceeded the threshold.\n\nCurrent utilization is {{value}}. Contact Stonebranch if additional cluster nodes are needed.\n\n@slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 0.95, + "warning": 0.8 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/license_distributed_agents.json b/stonebranch/assets/monitors/license_distributed_agents.json new file mode 100644 index 0000000000..48c7760ec5 --- /dev/null +++ b/stonebranch/assets/monitors/license_distributed_agents.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch distributed agent license utilization is high", + "tags": ["integration:stonebranch"], + "description": "Alerts when distributed agent license consumption approaches the licensed maximum, which may block new agent registrations.", + "definition": { + "name": "[Stonebranch] Distributed agent license utilization is high on {{endpoint.name}}", + "type": "query alert", + "query": "max(last_15m):avg:stonebranch.uc_license.agents_distributed.used{*} by {endpoint} / avg:stonebranch.uc_license.agents_distributed.max{*} by {endpoint} > 0.95", + "message": "Stonebranch distributed agent license utilization on {{endpoint.name}} has exceeded the threshold.\n\nCurrent utilization is {{value}}. Review active agents and consider decommissioning unused ones or upgrading the license.\n\n@slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 0.95, + "warning": 0.8 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/license_monthly_executions.json b/stonebranch/assets/monitors/license_monthly_executions.json new file mode 100644 index 0000000000..380c25ab46 --- /dev/null +++ b/stonebranch/assets/monitors/license_monthly_executions.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch monthly task execution license utilization is high", + "tags": ["integration:stonebranch"], + "description": "Alerts when monthly task execution license consumption approaches the licensed maximum. Reaching the limit may prevent new task executions.", + "definition": { + "name": "[Stonebranch] Monthly execution license utilization is high on {{endpoint.name}}", + "type": "query alert", + "query": "max(last_15m):avg:stonebranch.uc_license.monthly_executions.used{*} by {endpoint} / avg:stonebranch.uc_license.monthly_executions.max{*} by {endpoint} > 0.95", + "message": "Stonebranch monthly execution license utilization on {{endpoint.name}} has exceeded the threshold.\n\nCurrent utilization is {{value}}. The license resets at the start of the next billing month. Contact Stonebranch for a temporary extension if needed.\n\n@pagerduty @slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 0.95, + "warning": 0.85 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/license_task_definitions.json b/stonebranch/assets/monitors/license_task_definitions.json new file mode 100644 index 0000000000..7b35bb55d7 --- /dev/null +++ b/stonebranch/assets/monitors/license_task_definitions.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch task definition license utilization is high", + "tags": ["integration:stonebranch"], + "description": "Alerts when task definition license consumption approaches the licensed maximum, which may prevent creating new task definitions.", + "definition": { + "name": "[Stonebranch] Task definition license utilization is high on {{endpoint.name}}", + "type": "query alert", + "query": "max(last_15m):avg:stonebranch.uc_license.task_definitions.used{*} by {endpoint} / avg:stonebranch.uc_license.task_definitions.max{*} by {endpoint} > 0.95", + "message": "Stonebranch task definition license utilization on {{endpoint.name}} has exceeded the threshold.\n\nCurrent utilization is {{value}}. Archive or delete unused task definitions or contact Stonebranch to increase the licensed limit.\n\n@slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 0.95, + "warning": 0.8 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/license_zos_agents.json b/stonebranch/assets/monitors/license_zos_agents.json new file mode 100644 index 0000000000..f8bdb088b9 --- /dev/null +++ b/stonebranch/assets/monitors/license_zos_agents.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch z/OS agent license utilization is high", + "tags": ["integration:stonebranch"], + "description": "Alerts when z/OS agent license consumption approaches the licensed maximum.", + "definition": { + "name": "[Stonebranch] z/OS agent license utilization is high on {{endpoint.name}}", + "type": "query alert", + "query": "max(last_15m):avg:stonebranch.uc_license.agents_zos.used{*} by {endpoint} / avg:stonebranch.uc_license.agents_zos.max{*} by {endpoint} > 0.95", + "message": "Stonebranch z/OS agent license utilization on {{endpoint.name}} has exceeded the threshold.\n\nCurrent utilization is {{value}}. Review active z/OS agents or contact Stonebranch to adjust your license.\n\n@slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 0.95, + "warning": 0.8 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/oms_disconnected.json b/stonebranch/assets/monitors/oms_disconnected.json new file mode 100644 index 0000000000..050f250679 --- /dev/null +++ b/stonebranch/assets/monitors/oms_disconnected.json @@ -0,0 +1,25 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch OMS server is disconnected", + "tags": ["integration:stonebranch"], + "description": "Alerts when a Stonebranch OMS server reports a disconnected status. A disconnected OMS will prevent the Universal Controller from dispatching jobs to agents.", + "definition": { + "name": "[Stonebranch] OMS server disconnected on {{endpoint.name}}", + "type": "query alert", + "query": "min(last_5m):avg:stonebranch.uc_oms_server.status{*} by {endpoint,device} < 1", + "message": "The Stonebranch OMS server {{device.name}} is reporting a disconnected status on endpoint {{endpoint.name}}.\n\nCheck the OMS server process and network connectivity between the Universal Controller and OMS.\n\n@pagerduty @slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 1 + }, + "notify_no_data": false, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/monitors/uc_node_down.json b/stonebranch/assets/monitors/uc_node_down.json new file mode 100644 index 0000000000..e9323500cd --- /dev/null +++ b/stonebranch/assets/monitors/uc_node_down.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "created_at": "2025-12-15", + "last_updated_at": "2025-12-15", + "title": "Stonebranch Universal Controller node is not reporting", + "tags": ["integration:stonebranch"], + "description": "Alerts when a Stonebranch Universal Controller node stops reporting its build info metric, indicating the node may be down or unreachable.", + "definition": { + "name": "[Stonebranch] UC node not reporting on {{endpoint.name}}", + "type": "query alert", + "query": "min(last_5m):avg:stonebranch.uc_build.info{*} by {endpoint} < 1", + "message": "The Stonebranch Universal Controller node at {{endpoint.name}} has stopped reporting.\n\nVerify the UC process is running and the Prometheus metrics endpoint is accessible.\n\n@pagerduty @slack-alerts", + "tags": ["integration:stonebranch"], + "options": { + "thresholds": { + "critical": 1 + }, + "notify_no_data": true, + "no_data_timeframe": 10, + "require_full_window": false, + "include_tags": true, + "new_group_delay": 60, + "renotify_interval": 0 + } + } +} diff --git a/stonebranch/assets/service_checks.json b/stonebranch/assets/service_checks.json new file mode 100644 index 0000000000..07db0415e6 --- /dev/null +++ b/stonebranch/assets/service_checks.json @@ -0,0 +1,11 @@ +[ + { + "agent_version": "7.47.0", + "integration": "stonebranch", + "check": "stonebranch.openmetrics.health", + "statuses": ["ok", "critical"], + "groups": ["endpoint"], + "name": "Stonebranch OpenMetrics endpoint health", + "description": "Returns CRITICAL if the agent cannot reach the Stonebranch Universal Controller Prometheus metrics endpoint; otherwise returns OK." + } +] diff --git a/stonebranch/datadog_checks/stonebranch/__about__.py b/stonebranch/datadog_checks/stonebranch/__about__.py new file mode 100644 index 0000000000..1f356cc57b --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/__about__.py @@ -0,0 +1 @@ +__version__ = '1.0.0' diff --git a/stonebranch/datadog_checks/stonebranch/__init__.py b/stonebranch/datadog_checks/stonebranch/__init__.py new file mode 100644 index 0000000000..4eddad6494 --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/__init__.py @@ -0,0 +1,4 @@ +from .__about__ import __version__ +from .check import StonebranchCheck + +__all__ = ['__version__', 'StonebranchCheck'] diff --git a/stonebranch/datadog_checks/stonebranch/check.py b/stonebranch/datadog_checks/stonebranch/check.py new file mode 100644 index 0000000000..c9cbd3f17b --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/check.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import Any + +from datadog_checks.base import ConfigurationError, OpenMetricsBaseCheckV2 + +from .metrics import ADDITIONAL_METRICS, DEFAULT_METRICS + + +class StonebranchCheck(OpenMetricsBaseCheckV2): + __NAMESPACE__ = "stonebranch" + DEFAULT_METRIC_LIMIT = 0 + + def __init__(self, name, init_config, instances): + super().__init__(name, init_config, instances) + + self.instance.setdefault("tls_verify", True) + + def get_default_config(self) -> dict[str, Any]: + """ + OpenMetricsBaseCheckV2 builds its scraper configuration from this method. + Returning a fully-populated config here is the most reliable way to ensure: + - our computed metrics allowlist is applied + - our namespace is enforced + - our endpoint is validated + """ + endpoint = self.instance.get("openmetrics_endpoint") + if not endpoint: + raise ConfigurationError("`openmetrics_endpoint` must be set") + + user_metrics = self.instance.get("metrics") + if user_metrics: + metrics = self._coerce_metrics(user_metrics) + else: + metrics: list[dict[str, str]] = list(DEFAULT_METRICS) + + groups = self.instance.get("metric_groups") or [] + for group in groups: + if group not in ADDITIONAL_METRICS: + raise ConfigurationError(f"invalid group in metric_groups: {group}") + metrics.extend(ADDITIONAL_METRICS[group]) + + excludes = set(self.instance.get("exclude_metric_names") or []) + if excludes: + metrics = [m for m in metrics if next(iter(m.keys())) not in excludes] + + config: dict[str, Any] = dict(self.instance) + config.update( + { + "openmetrics_endpoint": endpoint, + "namespace": self.__NAMESPACE__, + "metrics": metrics, + "metadata_label_map": { + "build": "build", + "build_date": "build_date", + "release": "release", + }, + } + ) + return config + + @staticmethod + def _coerce_metrics(value: Any) -> list[dict[str, str]]: + """ + Accept either: + - list[str] (metric names) -> convert to OpenMetrics mapping format + - list[dict[str, str]] (OpenMetrics v2 mappings) -> pass through + """ + if isinstance(value, list) and all(isinstance(x, str) for x in value): + return [{n: n} for n in value] + if isinstance(value, list) and all(isinstance(x, dict) for x in value): + return value + + raise ConfigurationError( + "`metrics` must be a list of metric names (strings) or a list of OpenMetrics mappings (dicts)" + ) diff --git a/stonebranch/datadog_checks/stonebranch/config_models/__init__.py b/stonebranch/datadog_checks/stonebranch/config_models/__init__.py new file mode 100644 index 0000000000..5c2bf5c9f4 --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/config_models/__init__.py @@ -0,0 +1,20 @@ +# 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/stonebranch/datadog_checks/stonebranch/config_models/defaults.py b/stonebranch/datadog_checks/stonebranch/config_models/defaults.py new file mode 100644 index 0000000000..d100f53aed --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/config_models/defaults.py @@ -0,0 +1,132 @@ +# 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 shared_skip_proxy(): + return False + + +def shared_timeout(): + return 10 + + +def instance_allow_redirects(): + return True + + +def instance_auth_type(): + return 'basic' + + +def instance_cache_metric_wildcards(): + return True + + +def instance_cache_shared_labels(): + return True + + +def instance_collect_counters_with_distributions(): + return False + + +def instance_collect_histogram_buckets(): + return True + + +def instance_disable_generic_tags(): + return False + + +def instance_empty_default_hostname(): + return False + + +def instance_enable_health_service_check(): + return True + + +def instance_enable_legacy_tags_normalization(): + return True + + +def instance_histogram_buckets_as_distributions(): + return False + + +def instance_ignore_connection_errors(): + return False + + +def instance_kerberos_auth(): + return 'disabled' + + +def instance_kerberos_delegate(): + return False + + +def instance_kerberos_force_initiate(): + return False + + +def instance_log_requests(): + return False + + +def instance_min_collection_interval(): + return 15 + + +def instance_non_cumulative_histogram_buckets(): + return False + + +def instance_persist_connections(): + return False + + +def instance_request_size(): + return 16 + + +def instance_skip_proxy(): + return False + + +def instance_tag_by_endpoint(): + return True + + +def instance_telemetry(): + return False + + +def instance_timeout(): + return 10 + + +def instance_tls_ignore_warning(): + return False + + +def instance_tls_use_host_header(): + return False + + +def instance_tls_verify(): + return True + + +def instance_use_latest_spec(): + return False + + +def instance_use_legacy_auth_encoding(): + return True + + +def instance_use_process_start_time(): + return False diff --git a/stonebranch/datadog_checks/stonebranch/config_models/instance.py b/stonebranch/datadog_checks/stonebranch/config_models/instance.py new file mode 100644 index 0000000000..8bb2849370 --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/config_models/instance.py @@ -0,0 +1,182 @@ +# 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 types import MappingProxyType +from typing import Any, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from typing_extensions import Literal + +from datadog_checks.base.utils.functions import identity +from datadog_checks.base.utils.models import validation + +from . import defaults, validators + + +SECURE_FIELD_NAMES = frozenset( + ['auth_token', 'kerberos_cache', 'kerberos_keytab', 'tls_ca_cert', 'tls_cert', 'tls_private_key'] +) + + +class AuthToken(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + frozen=True, + ) + reader: Optional[MappingProxyType[str, Any]] = None + writer: Optional[MappingProxyType[str, Any]] = None + + +class ExtraMetrics(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + extra='allow', + frozen=True, + ) + name: Optional[str] = None + type: Optional[str] = None + + +class MetricPatterns(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + frozen=True, + ) + exclude: Optional[tuple[str, ...]] = None + include: Optional[tuple[str, ...]] = None + + +class Metrics(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + extra='allow', + frozen=True, + ) + name: Optional[str] = None + type: Optional[str] = None + + +class Proxy(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + frozen=True, + ) + http: Optional[str] = None + https: Optional[str] = None + no_proxy: Optional[tuple[str, ...]] = None + + +class ShareLabels(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + frozen=True, + ) + labels: Optional[tuple[str, ...]] = None + match: Optional[tuple[str, ...]] = None + + +class InstanceConfig(BaseModel): + model_config = ConfigDict( + validate_default=True, + arbitrary_types_allowed=True, + frozen=True, + ) + allow_redirects: Optional[bool] = None + auth_token: Optional[AuthToken] = None + auth_type: Optional[str] = None + aws_host: Optional[str] = None + aws_region: Optional[str] = None + aws_service: Optional[str] = None + cache_metric_wildcards: Optional[bool] = None + cache_shared_labels: Optional[bool] = None + collect_counters_with_distributions: Optional[bool] = None + collect_histogram_buckets: Optional[bool] = None + connect_timeout: Optional[float] = None + disable_generic_tags: Optional[bool] = None + empty_default_hostname: Optional[bool] = None + enable_health_service_check: Optional[bool] = None + enable_legacy_tags_normalization: Optional[bool] = None + exclude_labels: Optional[tuple[str, ...]] = None + exclude_metric_names: Optional[tuple[str, ...]] = None + exclude_metrics: Optional[tuple[str, ...]] = None + exclude_metrics_by_labels: Optional[MappingProxyType[str, Union[bool, tuple[str, ...]]]] = None + extra_headers: Optional[MappingProxyType[str, Any]] = None + extra_metrics: Optional[tuple[Union[str, MappingProxyType[str, Union[str, ExtraMetrics]]], ...]] = None + headers: Optional[MappingProxyType[str, Any]] = None + histogram_buckets_as_distributions: Optional[bool] = None + hostname_format: Optional[str] = None + hostname_label: Optional[str] = None + ignore_connection_errors: Optional[bool] = None + ignore_tags: Optional[tuple[str, ...]] = None + include_labels: Optional[tuple[str, ...]] = None + kerberos_auth: Optional[Literal['required', 'optional', 'disabled']] = None + kerberos_cache: Optional[str] = None + kerberos_delegate: Optional[bool] = None + kerberos_force_initiate: Optional[bool] = None + kerberos_hostname: Optional[str] = None + kerberos_keytab: Optional[str] = None + kerberos_principal: Optional[str] = None + log_requests: Optional[bool] = None + metric_groups: Optional[tuple[str, ...]] = None + metric_patterns: Optional[MetricPatterns] = None + metrics: Optional[tuple[Union[str, MappingProxyType[str, Union[str, Metrics]]], ...]] = None + min_collection_interval: Optional[float] = None + namespace: Optional[str] = Field(None, pattern='\\w*') + non_cumulative_histogram_buckets: Optional[bool] = None + ntlm_domain: Optional[str] = None + openmetrics_endpoint: str + password: Optional[str] = None + persist_connections: Optional[bool] = None + proxy: Optional[Proxy] = None + raw_line_filters: Optional[tuple[str, ...]] = None + raw_metric_prefix: Optional[str] = None + read_timeout: Optional[float] = None + rename_labels: Optional[MappingProxyType[str, Any]] = None + request_size: Optional[float] = None + service: Optional[str] = None + share_labels: Optional[MappingProxyType[str, Union[bool, ShareLabels]]] = None + skip_proxy: Optional[bool] = None + tag_by_endpoint: Optional[bool] = None + tags: Optional[tuple[str, ...]] = None + telemetry: Optional[bool] = None + timeout: Optional[float] = None + tls_ca_cert: Optional[str] = None + tls_cert: Optional[str] = None + tls_ciphers: Optional[tuple[str, ...]] = None + tls_ignore_warning: Optional[bool] = None + tls_private_key: Optional[str] = None + tls_protocols_allowed: Optional[tuple[str, ...]] = None + tls_use_host_header: Optional[bool] = None + tls_verify: Optional[bool] = None + use_latest_spec: Optional[bool] = None + use_legacy_auth_encoding: Optional[bool] = None + use_process_start_time: Optional[bool] = None + username: Optional[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) + + if info.field_name in SECURE_FIELD_NAMES: + validation.security.check_field_trusted_provider( + info.field_name, value, info.context.get('security_config') + ) + 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/stonebranch/datadog_checks/stonebranch/config_models/shared.py b/stonebranch/datadog_checks/stonebranch/config_models/shared.py new file mode 100644 index 0000000000..da933d6d8a --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/config_models/shared.py @@ -0,0 +1,56 @@ +# 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 Proxy(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + frozen=True, + ) + http: Optional[str] = None + https: Optional[str] = None + no_proxy: Optional[tuple[str, ...]] = None + + +class SharedConfig(BaseModel): + model_config = ConfigDict( + validate_default=True, + arbitrary_types_allowed=True, + frozen=True, + ) + proxy: Optional[Proxy] = None + service: Optional[str] = None + skip_proxy: Optional[bool] = None + timeout: Optional[float] = 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) + else: + value = getattr(defaults, f'shared_{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_shared', identity)(model)) diff --git a/stonebranch/datadog_checks/stonebranch/config_models/validators.py b/stonebranch/datadog_checks/stonebranch/config_models/validators.py new file mode 100644 index 0000000000..39523e4f92 --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/config_models/validators.py @@ -0,0 +1,9 @@ +# 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/stonebranch/datadog_checks/stonebranch/data/conf.yaml.example b/stonebranch/datadog_checks/stonebranch/data/conf.yaml.example new file mode 100644 index 0000000000..a5aab806e0 --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/data/conf.yaml.example @@ -0,0 +1,657 @@ +## All options defined here are available to all instances. +# +init_config: + + ## @param proxy - mapping - optional + ## Set HTTP or HTTPS proxies for all instances. Use the `no_proxy` list + ## to specify hosts that must bypass proxies. + ## + ## The SOCKS protocol is also supported like so: + ## + ## socks5://user:pass@host:port + ## + ## Using the scheme `socks5` causes the DNS resolution to happen on the + ## client, rather than on the proxy server. This is in line with `curl`, + ## which uses the scheme to decide whether to do the DNS resolution on + ## the client or proxy. If you want to resolve the domains on the proxy + ## server, use `socks5h` as the scheme. + # + # proxy: + # http: http://: + # https: https://: + # no_proxy: + # - + # - + + ## @param skip_proxy - boolean - optional - default: false + ## If set to `true`, this makes the check bypass any proxy + ## settings enabled and attempt to reach services directly. + # + # skip_proxy: false + + ## @param timeout - number - optional - default: 10 + ## The timeout for connecting to services. + # + # timeout: 10 + + ## @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 openmetrics_endpoint - string - required + ## The URL exposing metrics in the OpenMetrics format. + # + - openmetrics_endpoint: + + ## @param raw_metric_prefix - string - optional + ## A prefix that is removed from all exposed metric names, if present. + ## All configuration options will use the prefix-less name. + # + # raw_metric_prefix: _ + + ## @param extra_metrics - (list of string or mapping) - optional + ## This list defines metrics to collect from the `openmetrics_endpoint`, in addition to + ## what the check collects by default. If the check already collects a metric, then + ## metric definitions here take precedence. Metrics may be defined in 3 ways: + ## + ## 1. If the item is a string, then it represents the exposed metric name, and + ## the sent metric name will be identical. For example: + ## ``` + ## extra_metrics: + ## - + ## - + ## ``` + ## 2. If the item is a mapping, then the keys represent the exposed metric names. + ## + ## 1. If a value is a string, then it represents the sent metric name. For example: + ## ``` + ## extra_metrics: + ## - : + ## - : + ## ``` + ## 2. If a value is a mapping, then it must have a `name` and/or `type` key. + ## The `name` represents the sent metric name, and the `type` represents how + ## the metric should be handled, overriding any type information the endpoint + ## may provide. For example: + ## ``` + ## extra_metrics: + ## - : + ## name: + ## type: + ## - : + ## name: + ## type: + ## ``` + ## The supported native types are `gauge`, `counter`, `histogram`, and `summary`. + ## + ## Note: To collect counter metrics with names ending in `_total`, specify the metric name without the `_total` + ## suffix. For example, to collect the counter metric `promhttp_metric_handler_requests_total`, specify + ## `promhttp_metric_handler_requests`. This submits to Datadog the metric name appended with `.count`. + ## For more information, see: + ## https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#suffixes + ## + ## Regular expressions may be used to match the exposed metric names, for example: + ## ``` + ## extra_metrics: + ## - ^network_(ingress|egress)_.+ + ## - .+: + ## type: gauge + ## ``` + # + # extra_metrics: + # - + # - : + # - : + # name: + # type: + + ## @param exclude_metrics - list of strings - optional + ## A list of metrics to exclude, with each entry being either + ## the exact metric name or a regular expression. + ## + ## In order to exclude all metrics but the ones matching a specific filter, + ## you can use a negative lookahead regex like: + ## - ^(?!foo).*$ + # + # exclude_metrics: [] + + ## @param exclude_metrics_by_labels - mapping - optional + ## A mapping of labels to exclude metrics with matching label name and their corresponding metric values. To match + ## all values of a label, set it to `true`. + ## + ## Note: Label filtering happens before `rename_labels`. + ## + ## For example, the following configuration instructs the check to exclude all metrics with + ## a label `worker` or a label `pid` with the value of either `23` or `42`. + ## + ## exclude_metrics_by_labels: + ## worker: true + ## pid: + ## - '23' + ## - '42' + # + # exclude_metrics_by_labels: {} + + ## @param exclude_labels - list of strings - optional + ## A list of labels to exclude, useful for high cardinality values like timestamps or UUIDs. + ## May be used in conjunction with `include_labels`. + ## Labels defined in `exclude_labels` will take precedence in case of overlap. + ## + ## Note: Label filtering happens before `rename_labels`. + # + # exclude_labels: [] + + ## @param include_labels - list of strings - optional + ## A list of labels to include. May be used in conjunction with `exclude_labels`. + ## Labels defined in `exclude_labels` will take precedence in case of overlap. + ## + ## Note: Label filtering happens before `rename_labels`. + # + # include_labels: [] + + ## @param rename_labels - mapping - optional + ## A mapping of label names to their new names. + # + # rename_labels: + # : + # : + + ## @param enable_health_service_check - boolean - optional - default: true + ## Whether or not to send a service check named `.openmetrics.health` which reports + ## the health of the `openmetrics_endpoint`. + # + # enable_health_service_check: true + + ## @param ignore_connection_errors - boolean - optional - default: false + ## Whether or not to ignore connection errors when scraping `openmetrics_endpoint`. + # + # ignore_connection_errors: false + + ## @param hostname_label - string - optional + ## Override the hostname for every metric submission with the value of one of its labels. + # + # hostname_label: + + ## @param hostname_format - string - optional + ## When `hostname_label` is set, this instructs the check how to format the values. The string + ## `` is replaced by the value of the label defined by `hostname_label`. + # + # hostname_format: + + ## @param collect_histogram_buckets - boolean - optional - default: true + ## Whether or not to send histogram buckets. + # + # collect_histogram_buckets: true + + ## @param non_cumulative_histogram_buckets - boolean - optional - default: false + ## Whether or not histogram buckets are non-cumulative and to come with a `lower_bound` tag. + # + # non_cumulative_histogram_buckets: false + + ## @param histogram_buckets_as_distributions - boolean - optional - default: false + ## Whether or not to send histogram buckets as Datadog distribution metrics. This implicitly + ## enables the `collect_histogram_buckets` and `non_cumulative_histogram_buckets` options. + ## + ## Learn more about distribution metrics: + ## https://docs.datadoghq.com/developers/metrics/types/?tab=distribution#metric-types + # + # histogram_buckets_as_distributions: false + + ## @param collect_counters_with_distributions - boolean - optional - default: false + ## Whether or not to also collect the observation counter metrics ending in `.sum` and `.count` + ## when sending histogram buckets as Datadog distribution metrics. This implicitly enables the + ## `histogram_buckets_as_distributions` option. + # + # collect_counters_with_distributions: false + + ## @param use_process_start_time - boolean - optional - default: false + ## Whether to enable a heuristic for reporting counter values on the first scrape. When true, + ## the first time an endpoint is scraped, check `process_start_time_seconds` to decide whether zero + ## initial value can be assumed for counters. This requires keeping metrics in memory until the entire + ## response is received. + # + # use_process_start_time: false + + ## @param share_labels - mapping - optional + ## This mapping allows for the sharing of labels across multiple metrics. The keys represent the + ## exposed metrics from which to share labels, and the values are mappings that configure the + ## sharing behavior. Each mapping must have at least one of the following keys: + ## + ## - labels - This is a list of labels to share. All labels are shared if this is not set. + ## - match - This is a list of labels to match on other metrics as a condition for sharing. + ## - values - This is a list of allowed values as a condition for sharing. + ## + ## To unconditionally share all labels of a metric, set it to `true`. + ## + ## For example, the following configuration instructs the check to apply all labels from `metric_a` + ## to all other metrics, the `node` label from `metric_b` to only those metrics that have a `pod` + ## label value that matches the `pod` label value of `metric_b`, and all labels from `metric_c` + ## to all other metrics if their value is equal to `23` or `42`. + # + # share_labels: + # metric_a: true + # metric_b: + # labels: + # - node + # match: + # - pod + # metric_c: + # values: + # - 23 + # - 42 + + ## @param cache_shared_labels - boolean - optional - default: true + ## When `share_labels` is set, it instructs the check to cache labels collected from the first payload + ## for improved performance. + ## + ## Set this to `false` to compute label sharing for every payload at the risk of potentially increased memory usage. + # + # cache_shared_labels: true + + ## @param raw_line_filters - list of strings - optional + ## A list of regular expressions used to exclude lines read from the `openmetrics_endpoint` + ## from being parsed. + # + # raw_line_filters: [] + + ## @param cache_metric_wildcards - boolean - optional - default: true + ## Whether or not to cache data from metrics that are defined by regular expressions rather + ## than the full metric name. + # + # cache_metric_wildcards: true + + ## @param telemetry - boolean - optional - default: false + ## Whether or not to submit metrics prefixed by `.telemetry.` for debugging purposes. + # + # telemetry: false + + ## @param ignore_tags - list of strings - optional + ## A list of regular expressions used to ignore tags added by Autodiscovery and entries in the `tags` option. + # + # ignore_tags: + # - + # - + # - + + ## @param proxy - mapping - optional + ## This overrides the `proxy` setting in `init_config`. + ## + ## Set HTTP or HTTPS proxies for this instance. Use the `no_proxy` list + ## to specify hosts that must bypass proxies. + ## + ## The SOCKS protocol is also supported, for example: + ## + ## socks5://user:pass@host:port + ## + ## Using the scheme `socks5` causes the DNS resolution to happen on the + ## client, rather than on the proxy server. This is in line with `curl`, + ## which uses the scheme to decide whether to do the DNS resolution on + ## the client or proxy. If you want to resolve the domains on the proxy + ## server, use `socks5h` as the scheme. + # + # proxy: + # http: http://: + # https: https://: + # no_proxy: + # - + # - + + ## @param skip_proxy - boolean - optional - default: false + ## This overrides the `skip_proxy` setting in `init_config`. + ## + ## If set to `true`, this makes the check bypass any proxy + ## settings enabled and attempt to reach services directly. + # + # skip_proxy: false + + ## @param auth_type - string - optional - default: basic + ## The type of authentication to use. The available types (and related options) are: + ## ``` + ## - basic + ## |__ username + ## |__ password + ## |__ use_legacy_auth_encoding + ## - digest + ## |__ username + ## |__ password + ## - ntlm + ## |__ ntlm_domain + ## |__ password + ## - kerberos + ## |__ kerberos_auth + ## |__ kerberos_cache + ## |__ kerberos_delegate + ## |__ kerberos_force_initiate + ## |__ kerberos_hostname + ## |__ kerberos_keytab + ## |__ kerberos_principal + ## - aws + ## |__ aws_region + ## |__ aws_host + ## |__ aws_service + ## ``` + ## The `aws` auth type relies on boto3 to automatically gather AWS credentials, for example: from `.aws/credentials`. + ## Details: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#configuring-credentials + # + # auth_type: basic + + ## @param use_legacy_auth_encoding - boolean - optional - default: true + ## When `auth_type` is set to `basic`, this determines whether to encode as `latin1` rather than `utf-8`. + # + # use_legacy_auth_encoding: true + + ## @param username - string - optional + ## The username to use if services are behind basic or digest auth. + # + # username: + + ## @param password - string - optional + ## The password to use if services are behind basic or NTLM auth. + # + # password: + + ## @param ntlm_domain - string - optional + ## If your services use NTLM authentication, specify + ## the domain used in the check. For NTLM Auth, append + ## the username to domain, not as the `username` parameter. + # + # ntlm_domain: \ + + ## @param kerberos_auth - string - optional - default: disabled + ## If your services use Kerberos authentication, you can specify the Kerberos + ## strategy to use between: + ## + ## - required + ## - optional + ## - disabled + ## + ## See https://github.com/requests/requests-kerberos#mutual-authentication + # + # kerberos_auth: disabled + + ## @param kerberos_cache - string - optional + ## Sets the KRB5CCNAME environment variable. + ## It should point to a credential cache with a valid TGT. + # + # kerberos_cache: + + ## @param kerberos_delegate - boolean - optional - default: false + ## Set to `true` to enable Kerberos delegation of credentials to a server that requests delegation. + ## + ## See https://github.com/requests/requests-kerberos#delegation + # + # kerberos_delegate: false + + ## @param kerberos_force_initiate - boolean - optional - default: false + ## Set to `true` to preemptively initiate the Kerberos GSS exchange and + ## present a Kerberos ticket on the initial request (and all subsequent). + ## + ## See https://github.com/requests/requests-kerberos#preemptive-authentication + # + # kerberos_force_initiate: false + + ## @param kerberos_hostname - string - optional + ## Override the hostname used for the Kerberos GSS exchange if its DNS name doesn't + ## match its Kerberos hostname, for example: behind a content switch or load balancer. + ## + ## See https://github.com/requests/requests-kerberos#hostname-override + # + # kerberos_hostname: + + ## @param kerberos_principal - string - optional + ## Set an explicit principal, to force Kerberos to look for a + ## matching credential cache for the named user. + ## + ## See https://github.com/requests/requests-kerberos#explicit-principal + # + # kerberos_principal: + + ## @param kerberos_keytab - string - optional + ## Set the path to your Kerberos key tab file. + # + # kerberos_keytab: + + ## @param auth_token - mapping - optional + ## This allows for the use of authentication information from dynamic sources. + ## Both a reader and writer must be configured. + ## + ## The available readers are: + ## + ## - type: file + ## path (required): The absolute path for the file to read from. + ## pattern: A regular expression pattern with a single capture group used to find the + ## token rather than using the entire file, for example: Your secret is (.+) + ## - type: oauth + ## url (required): The token endpoint. + ## client_id (required): The client identifier. + ## client_secret (required): The client secret. + ## basic_auth: Whether the provider expects credentials to be transmitted in + ## an HTTP Basic Auth header. The default is: false + ## options: Mapping of additional options to pass to the provider, such as the audience + ## or the scope. For example: + ## options: + ## audience: https://example.com + ## scope: read:example + ## + ## The available writers are: + ## + ## - type: header + ## name (required): The name of the field, for example: Authorization + ## value: The template value, for example `Bearer `. The default is: + ## placeholder: The substring in `value` to replace with the token, defaults to: + # + # auth_token: + # reader: + # type: + # : + # : + # writer: + # type: + # : + # : + + ## @param aws_region - string - optional + ## If your services require AWS Signature Version 4 signing, set the region. + ## + ## See https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + # + # aws_region: + + ## @param aws_host - string - optional + ## If your services require AWS Signature Version 4 signing, set the host. + ## This only needs the hostname and does not require the protocol (HTTP, HTTPS, and more). + ## For example, if connecting to https://us-east-1.amazonaws.com/, set `aws_host` to `us-east-1.amazonaws.com`. + ## + ## Note: This setting is not necessary for official integrations. + ## + ## See https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + # + # aws_host: + + ## @param aws_service - string - optional + ## If your services require AWS Signature Version 4 signing, set the service code. For a list + ## of available service codes, see https://docs.aws.amazon.com/general/latest/gr/rande.html + ## + ## Note: This setting is not necessary for official integrations. + ## + ## See https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + # + # aws_service: + + ## @param tls_verify - boolean - optional - default: true + ## Instructs the check to validate the TLS certificate of services. + # + # tls_verify: true + + ## @param tls_use_host_header - boolean - optional - default: false + ## If a `Host` header is set, this enables its use for SNI (matching against the TLS certificate CN or SAN). + # + # tls_use_host_header: false + + ## @param tls_ignore_warning - boolean - optional - default: false + ## If `tls_verify` is disabled, security warnings are logged by the check. + ## Disable those by setting `tls_ignore_warning` to true. + # + # tls_ignore_warning: false + + ## @param tls_cert - string - optional + ## The path to a single file in PEM format containing a certificate as well as any + ## number of CA certificates needed to establish the certificate's authenticity for + ## use when connecting to services. It may also contain an unencrypted private key to use. + # + # tls_cert: + + ## @param tls_private_key - string - optional + ## The unencrypted private key to use for `tls_cert` when connecting to services. This is + ## required if `tls_cert` is set and it does not already contain a private key. + # + # tls_private_key: + + ## @param tls_ca_cert - string - optional + ## The path to a file of concatenated CA certificates in PEM format or a directory + ## containing several CA certificates in PEM format. If a directory, the directory + ## must have been processed using the `openssl rehash` command. See: + ## https://www.openssl.org/docs/man3.2/man1/c_rehash.html + # + # tls_ca_cert: + + ## @param tls_protocols_allowed - list of strings - optional + ## The expected versions of TLS/SSL when fetching intermediate certificates. + ## Only `SSLv3`, `TLSv1.2`, `TLSv1.3` are allowed by default. The possible values are: + ## SSLv3 + ## TLSv1 + ## TLSv1.1 + ## TLSv1.2 + ## TLSv1.3 + # + # tls_protocols_allowed: + # - SSLv3 + # - TLSv1.2 + # - TLSv1.3 + + ## @param tls_ciphers - list of strings - optional + ## The list of ciphers suites to use when connecting to an endpoint. If not specified, + ## `ALL` ciphers are used. For list of ciphers see: + ## https://www.openssl.org/docs/man1.0.2/man1/ciphers.html + # + # tls_ciphers: + # - TLS_AES_256_GCM_SHA384 + # - TLS_CHACHA20_POLY1305_SHA256 + # - TLS_AES_128_GCM_SHA256 + + ## @param headers - mapping - optional + ## The headers parameter allows you to send specific headers with every request. + ## You can use it for explicitly specifying the host header or adding headers for + ## authorization purposes. + ## + ## This overrides any default headers. + # + # headers: + # Host: + # X-Auth-Token: + + ## @param extra_headers - mapping - optional + ## Additional headers to send with every request. + # + # extra_headers: + # Host: + # X-Auth-Token: + + ## @param timeout - number - optional - default: 10 + ## The timeout for accessing services. + ## + ## This overrides the `timeout` setting in `init_config`. + # + # timeout: 10 + + ## @param connect_timeout - number - optional + ## The connect timeout for accessing services. Defaults to `timeout`. + # + # connect_timeout: + + ## @param read_timeout - number - optional + ## The read timeout for accessing services. Defaults to `timeout`. + # + # read_timeout: + + ## @param request_size - number - optional - default: 16 + ## The number of kibibytes (KiB) to read from streaming HTTP responses at a time. + # + # request_size: 16 + + ## @param log_requests - boolean - optional - default: false + ## Whether or not to debug log the HTTP(S) requests made, including the method and URL. + # + # log_requests: false + + ## @param persist_connections - boolean - optional - default: false + ## Whether or not to persist cookies and use connection pooling for improved performance. + # + # persist_connections: false + + ## @param allow_redirects - boolean - optional - default: true + ## Whether or not to allow URL redirection. + # + # allow_redirects: 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: + # - + + ## @param metric_groups - list of strings - optional + ## Opt-in metric groups to scrape in addition to the default Stonebranch metrics. + ## Allowed values: `jvm`, `process`, `license_details`. + # + # metric_groups: + # - "jvm" + # - "process" + + ## @param exclude_metric_names - list of strings - optional + ## Metrics to exclude by exact Prometheus metric name. + ## (This is applied after defaults and metric groups are expanded.) + # + # exclude_metric_names: + # - "uc_build_info" diff --git a/stonebranch/datadog_checks/stonebranch/metrics.py b/stonebranch/datadog_checks/stonebranch/metrics.py new file mode 100644 index 0000000000..b237805129 --- /dev/null +++ b/stonebranch/datadog_checks/stonebranch/metrics.py @@ -0,0 +1,81 @@ +# metrics.py + +# Curated: UC essentials only +DEFAULT_METRICS = [ + {"uc_history": "uc_history.total"}, + {"uc_task_instance_active": "uc_task_instance.active"}, + {"uc_task_instance_launch": "uc_task_instance.launch.total"}, + {"uc_task_instance_late_start": "uc_task_instance.late_start.total"}, + {"uc_task_instance_late_finish": "uc_task_instance.late_finish.total"}, + {"uc_task_instance_early_finish": "uc_task_instance.early_finish.total"}, + {"uc_task_instance_duration_seconds": "uc_task_instance.duration.seconds"}, + {"uc_universal_event": "uc_universal_event.total"}, + {"uc_agent_status": "uc_agent.status"}, + {"uc_build_info": "uc_build.info"}, + {"uc_database_connection_pool_active": "uc_database_connection_pool.active"}, + {"uc_database_connection_pool_idle": "uc_database_connection_pool.idle"}, + {"uc_database_connection_pool_idle_min": "uc_database_connection_pool.idle_min"}, + {"uc_database_connection_pool_idle_max": "uc_database_connection_pool.idle_max"}, + {"uc_database_connection_pool_allocated": "uc_database_connection_pool.allocated"}, + {"uc_database_connection_pool_max": "uc_database_connection_pool.max"}, + {"uc_oms_server_last_connected_time_seconds": "uc_oms_server.last_connected_time.seconds"}, + {"uc_oms_server_status": "uc_oms_server.status"}, + {"uc_oms_server_session_status": "uc_oms_server.session.status"}, + {"uc_monthly_executions": "uc_monthly_executions"}, +] + +# Opt-in groups +ADDITIONAL_METRICS = { + "jvm": [ + {"jvm_buffer_pool_capacity_bytes": "jvm_buffer_pool_capacity_bytes"}, + {"jvm_buffer_pool_used_buffers": "jvm_buffer_pool_used_buffers"}, + {"jvm_buffer_pool_used_bytes": "jvm_buffer_pool_used_bytes"}, + {"jvm_classes_currently_loaded": "jvm_classes_currently_loaded"}, + {"jvm_classes_loaded": "jvm_classes_loaded_total"}, + {"jvm_classes_unloaded": "jvm_classes_unloaded_total"}, + {"jvm_compilation_time_seconds_total": "jvm_compilation_time_seconds_total"}, + {"jvm_gc_collection_seconds": "jvm_gc_collection_seconds"}, + {"jvm_memory_committed_bytes": "jvm_memory_committed_bytes"}, + {"jvm_memory_init_bytes": "jvm_memory_init_bytes"}, + {"jvm_memory_max_bytes": "jvm_memory_max_bytes"}, + {"jvm_memory_objects_pending_finalization": "jvm_memory_objects_pending_finalization"}, + {"jvm_memory_pool_allocated_bytes": "jvm_memory_pool_allocated_bytes_total"}, + {"jvm_memory_pool_collection_committed_bytes": "jvm_memory_pool_collection_committed_bytes"}, + {"jvm_memory_pool_collection_init_bytes": "jvm_memory_pool_collection_init_bytes"}, + {"jvm_memory_pool_collection_max_bytes": "jvm_memory_pool_collection_max_bytes"}, + {"jvm_memory_pool_collection_used_bytes": "jvm_memory_pool_collection_used_bytes"}, + {"jvm_memory_pool_committed_bytes": "jvm_memory_pool_committed_bytes"}, + {"jvm_memory_pool_init_bytes": "jvm_memory_pool_init_bytes"}, + {"jvm_memory_pool_max_bytes": "jvm_memory_pool_max_bytes"}, + {"jvm_memory_pool_used_bytes": "jvm_memory_pool_used_bytes"}, + {"jvm_memory_used_bytes": "jvm_memory_used_bytes"}, + {"jvm_runtime_info": "jvm_runtime_info"}, + {"jvm_threads_current": "jvm_threads_current"}, + {"jvm_threads_daemon": "jvm_threads_daemon"}, + {"jvm_threads_deadlocked": "jvm_threads_deadlocked"}, + {"jvm_threads_deadlocked_monitor": "jvm_threads_deadlocked_monitor"}, + {"jvm_threads_peak": "jvm_threads_peak"}, + {"jvm_threads_started": "jvm_threads_started_total"}, + {"jvm_threads_state": "jvm_threads_state"}, + ], + "process": [ + {"process_cpu_seconds": "process_cpu_seconds_total"}, + {"process_max_fds": "process_max_fds"}, + {"process_open_fds": "process_open_fds"}, + {"process_resident_memory_bytes": "process_resident_memory_bytes"}, + {"process_start_time_seconds": "process_start_time_seconds"}, + {"process_virtual_memory_bytes": "process_virtual_memory_bytes"}, + ], + "license_details": [ + {"uc_license_agents_distributed_used": "uc_license.agents_distributed.used"}, + {"uc_license_agents_distributed_max": "uc_license.agents_distributed.max"}, + {"uc_license_agents_zos_used": "uc_license.agents_zos.used"}, + {"uc_license_agents_zos_max": "uc_license.agents_zos.max"}, + {"uc_license_cluster_nodes_used": "uc_license.cluster_nodes.used"}, + {"uc_license_cluster_nodes_max": "uc_license.cluster_nodes.max"}, + {"uc_license_monthly_executions_used": "uc_license.monthly_executions.used"}, + {"uc_license_monthly_executions_max": "uc_license.monthly_executions.max"}, + {"uc_license_task_definitions_used": "uc_license.task_definitions.used"}, + {"uc_license_task_definitions_max": "uc_license.task_definitions.max"}, + ], +} diff --git a/stonebranch/hatch.toml b/stonebranch/hatch.toml new file mode 100644 index 0000000000..c85c5f07a7 --- /dev/null +++ b/stonebranch/hatch.toml @@ -0,0 +1,4 @@ +[env.collectors.datadog-checks] + +[[envs.default.matrix]] +python = ["3.12"] diff --git a/stonebranch/metadata.csv b/stonebranch/metadata.csv new file mode 100644 index 0000000000..afd8ae8c07 --- /dev/null +++ b/stonebranch/metadata.csv @@ -0,0 +1,68 @@ +metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name,curated_metric +stonebranch.jvm_classes_currently_loaded,gauge,,,,The number of classes that are currently loaded in the JVM,0,stonebranch,jvm classes loaded, +stonebranch.jvm_buffer_pool_capacity_bytes,gauge,,byte,,Bytes capacity of a given JVM buffer pool.,0,stonebranch,jvm buffer pool capacity, +stonebranch.jvm_buffer_pool_used_buffers,gauge,,buffer,,Used buffers of a given JVM buffer pool.,0,stonebranch,jvm buffer pool used buffers, +stonebranch.jvm_buffer_pool_used_bytes,gauge,,byte,,Used bytes of a given JVM buffer pool.,0,stonebranch,jvm buffer pool used bytes, +stonebranch.jvm_classes_loaded_total.count,count,,,,The total number of classes that have been loaded since the JVM has started execution,1,stonebranch,jvm classes loaded total, +stonebranch.jvm_classes_unloaded_total.count,count,,,,The total number of classes that have been unloaded since the JVM has started execution,0,stonebranch,jvm classes unloaded total, +stonebranch.jvm_compilation_time_seconds_total.count,count,,second,,The total time in seconds taken for HotSpot compilation,0,stonebranch,jvm compilation time, +stonebranch.jvm_gc_collection_seconds,gauge,,second,,Time spent in a given JVM garbage collector in seconds.,0,stonebranch,jvm gc collection time, +stonebranch.jvm_memory_committed_bytes,gauge,,byte,,Committed (bytes) of a given JVM memory area.,0,stonebranch,jvm memory committed, +stonebranch.jvm_memory_init_bytes,gauge,,byte,,Initial bytes of a given JVM memory area.,0,stonebranch,jvm memory init, +stonebranch.jvm_memory_max_bytes,gauge,,byte,,Max (bytes) of a given JVM memory area.,0,stonebranch,jvm memory max, +stonebranch.jvm_memory_objects_pending_finalization,gauge,,object,,The number of objects waiting in the finalizer queue.,0,stonebranch,pending finalization, +stonebranch.jvm_memory_pool_allocated_bytes_total.count,count,,byte,,"Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously.",0,stonebranch,jvm pool allocated total, +stonebranch.jvm_memory_pool_collection_committed_bytes,gauge,,byte,,Committed after last collection bytes of a given JVM memory pool.,0,stonebranch,jvm pool committed post-gc, +stonebranch.jvm_memory_pool_collection_init_bytes,gauge,,byte,,Initial after last collection bytes of a given JVM memory pool.,0,stonebranch,jvm pool init post-gc, +stonebranch.jvm_memory_pool_collection_max_bytes,gauge,,byte,,Max bytes after last collection of a given JVM memory pool.,0,stonebranch,jvm pool max post-gc, +stonebranch.jvm_memory_pool_collection_used_bytes,gauge,,byte,,Used bytes after last collection of a given JVM memory pool.,0,stonebranch,jvm pool used post-gc, +stonebranch.jvm_memory_pool_committed_bytes,gauge,,byte,,Committed bytes of a given JVM memory pool.,0,stonebranch,jvm pool committed, +stonebranch.jvm_memory_pool_init_bytes,gauge,,byte,,Initial bytes of a given JVM memory pool.,0,stonebranch,jvm pool init, +stonebranch.jvm_memory_pool_max_bytes,gauge,,byte,,Max bytes of a given JVM memory pool.,0,stonebranch,jvm pool max, +stonebranch.jvm_memory_pool_used_bytes,gauge,,byte,,Used bytes of a given JVM memory pool.,0,stonebranch,jvm pool used, +stonebranch.jvm_memory_used_bytes,gauge,,byte,,Used bytes of a given JVM memory area.,0,stonebranch,jvm memory used, +stonebranch.jvm_runtime_info,gauge,,entry,,JVM runtime info,0,stonebranch,jvm runtime info, +stonebranch.jvm_threads_current,gauge,,thread,,Current thread count of a JVM,0,stonebranch,jvm threads current, +stonebranch.jvm_threads_daemon,gauge,,thread,,Daemon thread count of a JVM,0,stonebranch,jvm threads daemon, +stonebranch.jvm_threads_deadlocked,gauge,,thread,,Cycles of JVM-threads that are in deadlock waiting to acquire object monitors or ownable synchronizers,0,stonebranch,jvm threads deadlocked, +stonebranch.jvm_threads_deadlocked_monitor,gauge,,thread,,Cycles of JVM-threads that are in deadlock waiting to acquire object monitors,0,stonebranch,jvm deadlocked monitors, +stonebranch.jvm_threads_peak,gauge,,thread,,Peak thread count of a JVM,0,stonebranch,jvm threads peak, +stonebranch.jvm_threads_started_total.count,count,,thread,,Started thread count of a JVM,1,stonebranch,jvm threads started, +stonebranch.jvm_threads_state,gauge,,thread,,Current count of threads by state,0,stonebranch,jvm threads by state, +stonebranch.process_cpu_seconds_total.count,count,,second,,Total user and system CPU time spent in seconds.,0,stonebranch,process cpu time, +stonebranch.process_max_fds,gauge,,file,,Maximum number of open file descriptors.,0,stonebranch,process max fds, +stonebranch.process_open_fds,gauge,,file,,Number of open file descriptors.,0,stonebranch,process open fds, +stonebranch.process_resident_memory_bytes,gauge,,byte,,Resident memory size in bytes.,0,stonebranch,process rss, +stonebranch.process_start_time_seconds,gauge,,second,,Start time of the process since unix epoch in seconds.,0,stonebranch,process start time, +stonebranch.process_virtual_memory_bytes,gauge,,byte,,Virtual memory size in bytes.,0,stonebranch,process vmem, +stonebranch.uc_agent.status,gauge,,entry,,"Agent status. (0=Offline, 1=Active, -1=Unknown)",0,stonebranch,uc agent status, +stonebranch.uc_build.info,gauge,,entry,,Universal Controller build information.,0,stonebranch,uc build info, +stonebranch.uc_database_connection_pool.active,gauge,,connection,,Number of connections currently borrowed from a given pool.,0,stonebranch,db pool active, +stonebranch.uc_database_connection_pool.allocated,gauge,,connection,,"Number of connections currently allocated by a given pool (borrowed + idle).",0,stonebranch,db pool allocated, +stonebranch.uc_database_connection_pool.idle,gauge,,connection,,Number of connections currently idle in a given pool.,0,stonebranch,db pool idle, +stonebranch.uc_database_connection_pool.idle_max,gauge,,connection,,Maximum number of idle connections to maintain in a given pool.,0,stonebranch,db pool idle max, +stonebranch.uc_database_connection_pool.idle_min,gauge,,connection,,Target for the minimum number of idle connections to maintain in a given pool.,0,stonebranch,db pool idle min, +stonebranch.uc_database_connection_pool.max,gauge,,connection,,Maximum number of connections that can be allocated by a given pool at a given time.,0,stonebranch,db pool max, +stonebranch.uc_history.total.count,count,,,,"Total number of task instances that completed with an end status of In Doubt, Start Failure, Confirmation Required, Cancelled, Failed, Skipped, Finished, or Success.",1,stonebranch,uc history total, +stonebranch.uc_license.agents_distributed.max,gauge,,,,Number of distributed agents allowed.,1,stonebranch,licensed dist agents max, +stonebranch.uc_license.agents_distributed.used,gauge,,,,Number of distributed agents used.,0,stonebranch,licensed dist agents used, +stonebranch.uc_license.agents_zos.max,gauge,,,,Number of z/OS agents allowed.,1,stonebranch,licensed zos agents max, +stonebranch.uc_license.agents_zos.used,gauge,,,,Number of z/OS agents used.,0,stonebranch,licensed zos agents used, +stonebranch.uc_license.cluster_nodes.max,gauge,,node,,Number of cluster nodes allowed.,1,stonebranch,licensed cluster nodes max, +stonebranch.uc_license.cluster_nodes.used,gauge,,node,,Number of cluster nodes used.,0,stonebranch,licensed cluster nodes used, +stonebranch.uc_license.monthly_executions.max,gauge,,execution,,Licensed monthly task executions allowed.,1,stonebranch,licensed monthly exec max, +stonebranch.uc_license.monthly_executions.used,gauge,,execution,,Licensed monthly task executions used.,0,stonebranch,licensed monthly exec used, +stonebranch.uc_license.task_definitions.max,gauge,,,,Number of task definitions allowed.,1,stonebranch,licensed task defs max, +stonebranch.uc_license.task_definitions.used,gauge,,,,Number of task definitions used.,0,stonebranch,licensed task defs used, +stonebranch.uc_monthly_executions,gauge,,execution,,Total number of completed task instance executions for the current month by given type and status.,1,stonebranch,monthly executions, +stonebranch.uc_oms_server.last_connected_time.seconds,gauge,,second,,OMS Server last connected time as seconds since Unix epoch.,0,stonebranch,oms last connected, +stonebranch.uc_oms_server.session.status,gauge,,entry,,"OMS Server session status. (0=None, 1=Impaired, 2=Operational, -1=Unknown)",0,stonebranch,oms session status, +stonebranch.uc_oms_server.status,gauge,,entry,,"OMS Server status. (0=Disconnected, 1=Connected, -1=Unknown)",0,stonebranch,oms status, +stonebranch.uc_task_instance.active,gauge,,,,Number of instances currently active by a given status.,0,stonebranch,active task instances, +stonebranch.uc_task_instance.duration.seconds,gauge,,second,,Task instance duration in seconds.,0,stonebranch,task duration, +stonebranch.uc_task_instance.early_finish.total.count,count,,,,Total number of task instances that finished early.,1,stonebranch,early finishes, +stonebranch.uc_task_instance.late_finish.total.count,count,,,,Total number of task instances that finished late.,0,stonebranch,late finishes, +stonebranch.uc_task_instance.late_start.total.count,count,,,,Total number of task instances that started late.,0,stonebranch,late starts, +stonebranch.uc_task_instance.launch.total.count,count,,,,Total number of task instances launched.,1,stonebranch,task launches, +stonebranch.uc_universal_event.total.count,count,,,,Total number of universal events triggered.,0,stonebranch,universal events, + diff --git a/stonebranch/pyproject.toml b/stonebranch/pyproject.toml new file mode 100644 index 0000000000..42234cda4d --- /dev/null +++ b/stonebranch/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = [ + "hatchling>=0.13.0", +] +build-backend = "hatchling.build" + +[project] +name = "datadog-stonebranch" +description = "The stonebranch check" +readme = "README.md" +license = "BSD-3-Clause" +requires-python = ">=3.12" +keywords = [ + "datadog", + "datadog agent", + "datadog check", + "stonebranch", +] +authors = [ + { name = "StonebranchPM", email = "len-maurice.seemann@stonebranch.com" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Private :: Do Not Upload", + "Programming Language :: Python :: 3.12", + "Topic :: System :: Monitoring", +] +dependencies = [ + "datadog-checks-base>=37.0.0", +] +dynamic = [ + "version", +] + +[project.optional-dependencies] +deps = [] + +[project.urls] +Source = "https://github.com/DataDog/integrations-extras" + +[tool.hatch.version] +path = "datadog_checks/stonebranch/__about__.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/datadog_checks", + "/tests", + "/manifest.json", +] + +[tool.hatch.build.targets.wheel] +include = [ + "/datadog_checks/stonebranch", +] +dev-mode-dirs = [ + ".", +] diff --git a/stonebranch/tests/__init__.py b/stonebranch/tests/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/stonebranch/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/stonebranch/tests/conftest.py b/stonebranch/tests/conftest.py new file mode 100644 index 0000000000..1bc61d0306 --- /dev/null +++ b/stonebranch/tests/conftest.py @@ -0,0 +1,26 @@ +import os + +import pytest + +from datadog_checks.dev import docker_run, get_here +from datadog_checks.dev.conditions import CheckDockerLogs, CheckEndpoints + +HERE = get_here() +COMPOSE_FILE = os.path.join(HERE, 'docker', 'docker-compose.yaml') + +INSTANCE = {'openmetrics_endpoint': 'http://localhost:8080/metrics'} + + +@pytest.fixture(scope='session') +def dd_environment(): + conditions = [ + CheckDockerLogs(identifier='caddy', patterns=['server running']), + CheckEndpoints(INSTANCE['openmetrics_endpoint']), + ] + with docker_run(COMPOSE_FILE, conditions=conditions): + yield INSTANCE + + +@pytest.fixture +def instance(): + return INSTANCE diff --git a/stonebranch/tests/docker/Caddyfile b/stonebranch/tests/docker/Caddyfile new file mode 100644 index 0000000000..34d99bcf64 --- /dev/null +++ b/stonebranch/tests/docker/Caddyfile @@ -0,0 +1,4 @@ +:8080 { + root * /metrics + file_server +} diff --git a/stonebranch/tests/docker/docker-compose.yaml b/stonebranch/tests/docker/docker-compose.yaml new file mode 100644 index 0000000000..e06f5b105e --- /dev/null +++ b/stonebranch/tests/docker/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + + caddy: + image: caddy:2.7 + container_name: caddy + ports: + - "8080:8080" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + - ../fixtures/metrics.txt:/metrics/metrics diff --git a/stonebranch/tests/fixtures/metrics.txt b/stonebranch/tests/fixtures/metrics.txt new file mode 100644 index 0000000000..9055658f68 --- /dev/null +++ b/stonebranch/tests/fixtures/metrics.txt @@ -0,0 +1,83 @@ +# HELP uc_agent_status Agent status. (0=Offline, 1=Active, -1=Unknown) +# TYPE uc_agent_status gauge +uc_agent_status{agent_id="AGNT0001"} 1.0 +uc_agent_status{agent_id="AGNT0002"} 0.0 + +# HELP uc_build_info Universal Controller build information. +# TYPE uc_build_info gauge +uc_build_info{build="build.108",build_date="09-26-2025_0545",release="7.9.0.0"} 1 + +# HELP uc_database_connection_pool_active Number of connections currently borrowed from a given pool. +# TYPE uc_database_connection_pool_active gauge +uc_database_connection_pool_active{db_type="MySQL",pool="Client"} 2.0 + +# HELP uc_database_connection_pool_idle Number of connections currently idle in a given pool. +# TYPE uc_database_connection_pool_idle gauge +uc_database_connection_pool_idle{db_type="MySQL",pool="Client"} 8.0 + +# HELP uc_database_connection_pool_idle_min Target for the minimum number of idle connections to maintain in a given pool. +# TYPE uc_database_connection_pool_idle_min gauge +uc_database_connection_pool_idle_min{db_type="MySQL",pool="Client"} 2.0 + +# HELP uc_database_connection_pool_idle_max Maximum number of idle connections to maintain in a given pool. +# TYPE uc_database_connection_pool_idle_max gauge +uc_database_connection_pool_idle_max{db_type="MySQL",pool="Client"} 10.0 + +# HELP uc_database_connection_pool_allocated Number of connections currently allocated by a given pool. +# TYPE uc_database_connection_pool_allocated gauge +uc_database_connection_pool_allocated{db_type="MySQL",pool="Client"} 10.0 + +# HELP uc_database_connection_pool_max Maximum number of connections that can be allocated by a given pool. +# TYPE uc_database_connection_pool_max gauge +uc_database_connection_pool_max{db_type="MySQL",pool="Client"} 50.0 + +# HELP uc_history_total Total number of task instances that completed with an end status. +# TYPE uc_history_total counter +uc_history_total{task_instance_status="Success"} 1250.0 +uc_history_total{task_instance_status="Failed"} 12.0 +uc_history_total{task_instance_status="Skipped"} 3.0 + +# HELP uc_monthly_executions Total completed task instance executions for the current month. +# TYPE uc_monthly_executions gauge +uc_monthly_executions{task_type="Linux",task_instance_status="Success"} 432.0 + +# HELP uc_oms_server_status OMS Server status. (0=Disconnected, 1=Connected, -1=Unknown) +# TYPE uc_oms_server_status gauge +uc_oms_server_status{oms_server_name="OMS-PRIMARY"} 1.0 + +# HELP uc_oms_server_session_status OMS Server session status. +# TYPE uc_oms_server_session_status gauge +uc_oms_server_session_status{oms_server_name="OMS-PRIMARY"} 2.0 + +# HELP uc_oms_server_last_connected_time_seconds OMS Server last connected time as seconds since Unix epoch. +# TYPE uc_oms_server_last_connected_time_seconds gauge +uc_oms_server_last_connected_time_seconds{oms_server_name="OMS-PRIMARY"} 1.748e+09 + +# HELP uc_task_instance_active Number of instances currently active by a given status. +# TYPE uc_task_instance_active gauge +uc_task_instance_active{task_instance_status="Running"} 5.0 +uc_task_instance_active{task_instance_status="Waiting"} 2.0 + +# HELP uc_task_instance_launch_total Total number of task instances launched. +# TYPE uc_task_instance_launch_total counter +uc_task_instance_launch_total 2048.0 + +# HELP uc_task_instance_late_start_total Total number of task instances that started late. +# TYPE uc_task_instance_late_start_total counter +uc_task_instance_late_start_total 4.0 + +# HELP uc_task_instance_late_finish_total Total number of task instances that finished late. +# TYPE uc_task_instance_late_finish_total counter +uc_task_instance_late_finish_total 2.0 + +# HELP uc_task_instance_early_finish_total Total number of task instances that finished early. +# TYPE uc_task_instance_early_finish_total counter +uc_task_instance_early_finish_total 1.0 + +# HELP uc_task_instance_duration_seconds Task instance duration in seconds. +# TYPE uc_task_instance_duration_seconds gauge +uc_task_instance_duration_seconds{task_name="daily-etl",task_type="Linux"} 42.5 + +# HELP uc_universal_event_total Total number of universal events triggered. +# TYPE uc_universal_event_total counter +uc_universal_event_total{event_name="job-complete"} 128.0 diff --git a/stonebranch/tests/test_e2e.py b/stonebranch/tests/test_e2e.py new file mode 100644 index 0000000000..def3ba819b --- /dev/null +++ b/stonebranch/tests/test_e2e.py @@ -0,0 +1,6 @@ +import pytest + + +@pytest.mark.e2e +def test_e2e(dd_agent_check): + pass diff --git a/stonebranch/tests/test_unit.py b/stonebranch/tests/test_unit.py new file mode 100644 index 0000000000..51ce4d22ec --- /dev/null +++ b/stonebranch/tests/test_unit.py @@ -0,0 +1,149 @@ +import io + +import requests + +from datadog_checks.dev.utils import get_metadata_metrics +from datadog_checks.stonebranch import StonebranchCheck + +PROM_TEXT = """\ +# HELP uc_agent_status Agent status. (0=Offline, 1=Active, -1=Unknown) +# TYPE uc_agent_status gauge +uc_agent_status{agent_id="AGNT0006"} 1.0 +uc_agent_status{agent_id="AGNT0007"} 0.0 + +# HELP uc_build_info Universal Controller build information. +# TYPE uc_build_info gauge +uc_build_info{build="build.108",build_date="09-26-2025_0545",release="7.9.0.0"} 1 + +# HELP uc_database_connection_pool_allocated Number of connections currently allocated by a given pool. +# TYPE uc_database_connection_pool_allocated gauge +uc_database_connection_pool_allocated{db_type="MySQL",pool="Client"} 5.0 +""" + +PROM_TEXT_JVM = """\ +# HELP jvm_threads_current Current thread count of a JVM +# TYPE jvm_threads_current gauge +jvm_threads_current 42.0 + +# HELP jvm_threads_peak Peak thread count of a JVM +# TYPE jvm_threads_peak gauge +jvm_threads_peak 60.0 + +# HELP jvm_memory_used_bytes Used bytes of a given JVM memory area. +# TYPE jvm_memory_used_bytes gauge +jvm_memory_used_bytes{area="heap"} 123456789.0 + +# HELP jvm_memory_committed_bytes Committed (bytes) of a given JVM memory area. +# TYPE jvm_memory_committed_bytes gauge +jvm_memory_committed_bytes{area="heap"} 256000000.0 + +# HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds. +# TYPE jvm_gc_collection_seconds gauge +jvm_gc_collection_seconds{gc="G1 Young Generation"} 0.123 +""" + + +def make_streaming_response(url: str, text: str) -> requests.Response: + r = requests.Response() + r.status_code = 200 + r.url = url + r.headers["Content-Type"] = "text/plain; version=0.0.4; charset=utf-8" + r.encoding = "utf-8" + + r.raw = io.BytesIO(text.encode("utf-8")) + + r.request = requests.Request("GET", url).prepare() + return r + + +def test_openmetrics_basic_auth_and_labels(aggregator, dd_run_check, mocker): + url = "http://test.local/metrics" + instance = { + "openmetrics_endpoint": url, + "metrics": [ + {"uc_agent_status": "uc_agent.status"}, + {"uc_build_info": "uc_build.info"}, + {"uc_database_connection_pool_allocated": "uc_database_connection_pool.allocated"}, + ], + "auth_type": "basic", + "username": "user1", + "password": "pass1", + "tags": ["environment:test"], + } + + mocked_request = mocker.patch( + "requests.sessions.Session.request", + autospec=True, + return_value=make_streaming_response(url, PROM_TEXT), + ) + + check = StonebranchCheck("stonebranch", {}, [instance]) + dd_run_check(check) + + endpoint_tag = f"endpoint:{url}" + base_tags = ["environment:test", endpoint_tag] + + aggregator.assert_metric( + "stonebranch.uc_agent.status", + value=1.0, + tags=base_tags + ["agent_id:AGNT0006"], + ) + aggregator.assert_metric( + "stonebranch.uc_agent.status", + value=0.0, + tags=base_tags + ["agent_id:AGNT0007"], + ) + aggregator.assert_metric( + "stonebranch.uc_build.info", + value=1.0, + tags=base_tags + ["build:build.108", "build_date:09-26-2025_0545", "release:7.9.0.0"], + ) + aggregator.assert_metric( + "stonebranch.uc_database_connection_pool.allocated", + value=5.0, + tags=base_tags + ["db_type:MySQL", "pool:Client"], + ) + + _, kwargs = mocked_request.call_args + assert kwargs.get("auth") is not None + + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_metric_groups_jvm(aggregator, dd_run_check, mocker): + url = "http://test.local/metrics" + instance = { + "openmetrics_endpoint": url, + "metric_groups": ["jvm"], + "tags": ["environment:test"], + } + + mocker.patch( + "requests.sessions.Session.request", + autospec=True, + return_value=make_streaming_response(url, PROM_TEXT_JVM), + ) + + check = StonebranchCheck("stonebranch", {}, [instance]) + dd_run_check(check) + + endpoint_tag = f"endpoint:{url}" + base_tags = ["environment:test", endpoint_tag] + + aggregator.assert_metric("stonebranch.jvm_threads_current", value=42.0, tags=base_tags) + aggregator.assert_metric("stonebranch.jvm_threads_peak", value=60.0, tags=base_tags) + aggregator.assert_metric( + "stonebranch.jvm_memory_used_bytes", + value=123456789.0, + tags=base_tags + ["area:heap"], + ) + aggregator.assert_metric( + "stonebranch.jvm_memory_committed_bytes", + value=256000000.0, + tags=base_tags + ["area:heap"], + ) + aggregator.assert_metric( + "stonebranch.jvm_gc_collection_seconds", + tags=base_tags + ["gc:G1 Young Generation"], + )