diff --git a/.codecov.yml b/.codecov.yml index 11dbd831509dd..104d575c11304 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -794,6 +794,10 @@ coverage: target: 75 flags: - kyverno + n8n: + target: 75 + flags: + - n8n nvidia_nim: target: 75 flags: @@ -1431,6 +1435,11 @@ flags: paths: - mysql/datadog_checks/mysql - mysql/tests + n8n: + carryforward: true + paths: + - n8n/datadog_checks/n8n + - n8n/tests nagios: carryforward: true paths: diff --git a/.github/workflows/config/labeler.yml b/.github/workflows/config/labeler.yml index edef8d55a056d..e86afa7765065 100644 --- a/.github/workflows/config/labeler.yml +++ b/.github/workflows/config/labeler.yml @@ -89,6 +89,8 @@ integration/azure_active_directory: - azure_active_directory/**/* integration/azure_iot_edge: - azure_iot_edge/**/* +integration/barracuda_secure_edge: +- barracuda_secure_edge/**/* integration/bentoml: - bentoml/**/* integration/beyondtrust_identity_security_insights: @@ -157,6 +159,8 @@ integration/cloud_foundry_api: - cloud_foundry_api/**/* integration/cloudera: - cloudera/**/* +integration/cloudgen_firewall: +- cloudgen_firewall/**/* integration/cockroachdb: - cockroachdb/**/* integration/cofense_triage: @@ -359,10 +363,10 @@ integration/kafka: - kafka/**/* integration/kafka_consumer: - kafka_consumer/**/* -integration/karpenter: -- karpenter/**/* integration/kandji: - kandji/**/* +integration/karpenter: +- karpenter/**/* integration/keda: - keda/**/* integration/keeper: @@ -467,6 +471,8 @@ integration/mux: - mux/**/* integration/mysql: - mysql/**/* +integration/n8n: +- n8n/**/* integration/nagios: - nagios/**/* integration/network: @@ -781,10 +787,6 @@ integration/zerofox_cloud_platform: - zerofox_cloud_platform/**/* integration/zk: - zk/**/* -integration/barracuda_secure_edge: -- barracuda_secure_edge/**/* -integration/cloudgen_firewall: -- cloudgen_firewall/**/* integration/zscaler_private_access: - zscaler_private_access/**/* qa/skip-qa: diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index d300257e42635..e4f672a866fba 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -2604,6 +2604,26 @@ jobs: - py3.13-percona-8.0.42 - py3.13-percona-8.4 fail-fast: false + j636396f: + uses: ./.github/workflows/test-target.yml + with: + job-name: n8n + target: n8n + platform: linux + runner: '["ubuntu-22.04"]' + repo: "${{ inputs.repo }}" + context: ${{ inputs.context }} + python-version: "${{ inputs.python-version }}" + latest: ${{ inputs.latest }} + agent-image: "${{ inputs.agent-image }}" + agent-image-py2: "${{ inputs.agent-image-py2 }}" + agent-image-windows: "${{ inputs.agent-image-windows }}" + agent-image-windows-py2: "${{ inputs.agent-image-windows-py2 }}" + test-py2: ${{ inputs.test-py2 }} + test-py3: ${{ inputs.test-py3 }} + minimum-base-package: ${{ inputs.minimum-base-package }} + pytest-args: ${{ inputs.pytest-args }} + secrets: inherit j5df646e: uses: ./.github/workflows/test-target.yml with: diff --git a/n8n/CHANGELOG.md b/n8n/CHANGELOG.md new file mode 100644 index 0000000000000..13505aa587aeb --- /dev/null +++ b/n8n/CHANGELOG.md @@ -0,0 +1,4 @@ +# CHANGELOG - n8n + + + diff --git a/n8n/README.md b/n8n/README.md new file mode 100644 index 0000000000000..6a1a92c121804 --- /dev/null +++ b/n8n/README.md @@ -0,0 +1,69 @@ +# Agent Check: n8n + +## Overview + +This check monitors [n8n][1] through the Datadog Agent. + +Collect n8n metrics including: +- Cache metrics: Hit and miss statistics. +- Message event bus metrics: Event-related metrics. +- Workflow metrics: Can include workflow ID labels. +- Node metrics: Can include node type labels. +- Credential metrics: Can include credential type labels. +- Queue Metrics + + +## Setup + +Follow the instructions below to install and configure this check for an Agent running on a host. For containerized environments, see the [Autodiscovery Integration Templates][3] for guidance on applying these instructions. + +### Installation + +The n8n check is included in the [Datadog Agent][2] package. +No additional installation is needed on your server. + +### Configuration + +The `/metrics` endpoint is disabled by default and must be enabled by setting `N8N_METRICS`=`true`. You can also customize the metric prefix using `N8N_METRICS_PREFIX` (default is `n8n_`). + +Note that the `/metrics` endpoint is only available for self-hosted instances and is not available on n8n Cloud. + +For the datadog agent to collect metrics, you will need to follow the instructions provided [here][10]. + +1. Edit the `n8n.d/conf.yaml` file, in the `conf.d/` folder at the root of your Agent's configuration directory to start collecting your n8n performance data. See the [sample n8n.d/conf.yaml][4] for all available configuration options. + +2. [Restart the Agent][5]. + +### Validation + +[Run the Agent's status subcommand][6] and look for `n8n` under the Checks section. + +## Data Collected + +### Metrics + +See [metadata.csv][7] for a list of metrics provided by this integration. + +### Events + +The n8n integration does not include any events. + +### Service Checks + +See [service_checks.json][8] for a list of service checks provided by this integration. + +## Troubleshooting + +Need help? Contact [Datadog support][9]. + + +[1]: https://n8n.io/ +[2]: https://app.datadoghq.com/account/settings/agent/latest +[3]: https://docs.datadoghq.com/containers/kubernetes/integrations/ +[4]: https://github.com/DataDog/integrations-core/blob/master/n8n/datadog_checks/n8n/data/conf.yaml.example +[5]: https://docs.datadoghq.com/agent/configuration/agent-commands/#start-stop-and-restart-the-agent +[6]: https://docs.datadoghq.com/agent/configuration/agent-commands/#agent-status-and-information +[7]: https://github.com/DataDog/integrations-core/blob/master/n8n/metadata.csv +[8]: https://github.com/DataDog/integrations-core/blob/master/n8n/assets/service_checks.json +[9]: https://docs.datadoghq.com/help/ +[10]: https://docs.n8n.io/hosting/configuration/configuration-examples/prometheus/ diff --git a/n8n/assets/configuration/spec.yaml b/n8n/assets/configuration/spec.yaml new file mode 100644 index 0000000000000..258a027ef4912 --- /dev/null +++ b/n8n/assets/configuration/spec.yaml @@ -0,0 +1,44 @@ +name: n8n +files: +- name: n8n.yaml + options: + - template: init_config + options: + - template: init_config/default + - template: instances + options: + - template: instances/openmetrics + overrides: + openmetrics_endpoint.required: true + openmetrics_endpoint.hidden: false + openmetrics_endpoint.display_priority: 1 + openmetrics_endpoint.value.example: http://localhost:5678 + openmetrics_endpoint.description: | + Endpoint exposing the n8n's metrics in the OpenMetrics format. For more information, refer to: + https://docs.n8n.io/hosting/logging-monitoring/monitoring/ + https://docs.n8n.io/hosting/configuration/environment-variables/endpoints/ + raw_metric_prefix.description: | + The prefix prepended to all metrics from n8n. + If not set, the default prefix is used. + The default prefix is 'n8n'. + If you are using a custom prefix in n8n through N8N_METRICS_PREFIX, you can set it here. + raw_metric_prefix.value: + display_default: n8n + type: string + example: n8n + raw_metric_prefix.hidden: false + - name: server_port + description: | + The port exposing the HTTP endpoint of the n8n API. + value: + display_default: 5678 + type: integer + - template: logs + example: + - type: file + path: /var/log/n8n/*.log + source: n8n + service: + - type: docker + source: n8n + service: diff --git a/n8n/assets/dashboards/n8n_overview.json b/n8n/assets/dashboards/n8n_overview.json new file mode 100644 index 0000000000000..6f5d5cd95db6b --- /dev/null +++ b/n8n/assets/dashboards/n8n_overview.json @@ -0,0 +1,9 @@ +{ + "title": "N8N Overview Dashboard", + "description": "N8N Overview Dashboard", + "widgets": [], + "template_variables": [], + "layout_type": "ordered", + "notify_list": [], + "reflow_type": "fixed" +} \ No newline at end of file diff --git a/n8n/assets/service_checks.json b/n8n/assets/service_checks.json new file mode 100644 index 0000000000000..1b5d085f8020c --- /dev/null +++ b/n8n/assets/service_checks.json @@ -0,0 +1,29 @@ +[ + { + "agent_version": "7.75.0", + "integration": "n8n", + "check": "n8n.openmetrics.health", + "statuses": ["ok", "critical"], + "groups": [], + "name": "n8n OpenMetrics Health", + "description": "Returns `CRITICAL` if the check cannot access the metrics endpoint. Returns `OK` otherwise." + }, + { + "agent_version": "7.75.0", + "integration": "n8n", + "check": "n8n.health.status", + "statuses": ["ok", "critical"], + "groups": [], + "name": "n8n Health Status", + "description": "Returns `CRITICAL` if the health check endpoint returns an unhealthy status. Returns `OK` otherwise." + }, + { + "agent_version": "7.75.0", + "integration": "n8n", + "check": "n8n.readiness.status", + "statuses": ["ok", "critical"], + "groups": [], + "name": "n8n Readiness Status", + "description": "Returns `CRITICAL` if the readiness check endpoint returns an unready status. Returns `OK` otherwise." + } +] diff --git a/n8n/changelog.d/21835.added b/n8n/changelog.d/21835.added new file mode 100644 index 0000000000000..aa949b47b7b41 --- /dev/null +++ b/n8n/changelog.d/21835.added @@ -0,0 +1 @@ +Initial Release \ No newline at end of file diff --git a/n8n/datadog_checks/n8n/__about__.py b/n8n/datadog_checks/n8n/__about__.py new file mode 100644 index 0000000000000..1bde5986a04b2 --- /dev/null +++ b/n8n/datadog_checks/n8n/__about__.py @@ -0,0 +1,4 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +__version__ = '0.0.1' diff --git a/n8n/datadog_checks/n8n/__init__.py b/n8n/datadog_checks/n8n/__init__.py new file mode 100644 index 0000000000000..5aecd3a74de9d --- /dev/null +++ b/n8n/datadog_checks/n8n/__init__.py @@ -0,0 +1,7 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +from .__about__ import __version__ +from .check import N8nCheck + +__all__ = ['__version__', 'N8nCheck'] diff --git a/n8n/datadog_checks/n8n/check.py b/n8n/datadog_checks/n8n/check.py new file mode 100644 index 0000000000000..5f947cca05846 --- /dev/null +++ b/n8n/datadog_checks/n8n/check.py @@ -0,0 +1,99 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from urllib.parse import urljoin, urlparse # noqa: F401 + +from datadog_checks.base import AgentCheck, OpenMetricsBaseCheckV2 +from datadog_checks.n8n.metrics import METRIC_MAP + +DEFAULT_READY_ENDPOINT = '/healthz/readiness' +DEFAULT_HEALTH_ENDPOINT = '/healthz' +DEFAULT_VERSION_ENDPOINT = '/rest/settings' + + +class N8nCheck(OpenMetricsBaseCheckV2): + __NAMESPACE__ = 'n8n' + DEFAULT_METRIC_LIMIT = 0 + + def __init__(self, name, init_config, instances=None): + super(N8nCheck, self).__init__( + name, + init_config, + instances, + ) + self.openmetrics_endpoint = self.instance["openmetrics_endpoint"] + self.tags = self.instance.get('tags', []) + self._ready_endpoint = DEFAULT_READY_ENDPOINT + self._health_endpoint = DEFAULT_HEALTH_ENDPOINT + self._version_endpoint = DEFAULT_VERSION_ENDPOINT + # Get the N8N API port if specified, otherwise use the default 5678. + self.server_port = str(self.instance.get('server_port', 5678)) + self.raw_metric_prefix = self.instance.get('raw_metric_prefix', 'n8n') + + def get_default_config(self): + # If raw_metric_prefix is 'n8n', metrics start with 'n8n' + if self.raw_metric_prefix == 'n8n': + namespace = 'n8n' + else: + namespace = f'n8n.{self.raw_metric_prefix}' + + return {'namespace': namespace, 'metrics': [METRIC_MAP]} + + @AgentCheck.metadata_entrypoint + def _submit_version_metadata(self): + endpoint = urljoin(self.openmetrics_endpoint, self._version_endpoint) + response = self.http.get(endpoint) + + if response.ok: + data = response.json() + version = data.get("versionCli", "") + version_split = version.split(".") + if len(version_split) >= 3: + major = version_split[0] + minor = version_split[1] + patch = version_split[2] + + version_raw = f'{major}.{minor}.{patch}' + + version_parts = { + 'major': major, + 'minor': minor, + 'patch': patch, + } + self.set_metadata('version', version_raw, scheme='semver', part_map=version_parts) + else: + self.log.debug("Malformed N8N Server version format: %s", version) + else: + self.log.debug("Could not retrieve version metadata.") + + def _check_n8n_health(self): + endpoint = urljoin(self.openmetrics_endpoint, self._health_endpoint) + response = self.http.get(endpoint) + + # Any 4xx or 5xx response from the API endpoint (/healthz) means the n8n process is not responding + if 400 <= response.status_code and response.status_code < 600: + self.service_check('health.status', AgentCheck.CRITICAL, self.tags) + if response.status_code == 200: + self.service_check('health.status', AgentCheck.OK, self.tags) + else: + self.service_check('health.status', AgentCheck.UNKNOWN, self.tags) + + def _check_n8n_readiness(self): + endpoint = urljoin(self.openmetrics_endpoint, self._ready_endpoint) + response = self.http.get(endpoint) + + # Any 4xx or 5xx response from the API endpoint (/healthz/readiness) + # means the n8n is not ready to accept requests + if 400 <= response.status_code and response.status_code < 600: + self.service_check('health.status', AgentCheck.CRITICAL, self.tags) + if response.status_code == 200: + self.service_check('health.status', AgentCheck.OK, self.tags) + else: + self.service_check('health.status', AgentCheck.UNKNOWN, self.tags) + + def check(self, instance): + super().check(instance) + self._submit_version_metadata() + self._check_n8n_health() + self._check_n8n_readiness() diff --git a/n8n/datadog_checks/n8n/config_models/__init__.py b/n8n/datadog_checks/n8n/config_models/__init__.py new file mode 100644 index 0000000000000..4d80b1e478f62 --- /dev/null +++ b/n8n/datadog_checks/n8n/config_models/__init__.py @@ -0,0 +1,24 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from .instance import InstanceConfig +from .shared import SharedConfig + + +class ConfigMixin: + _config_model_instance: InstanceConfig + _config_model_shared: SharedConfig + + @property + def config(self) -> InstanceConfig: + return self._config_model_instance + + @property + def shared_config(self) -> SharedConfig: + return self._config_model_shared diff --git a/n8n/datadog_checks/n8n/config_models/defaults.py b/n8n/datadog_checks/n8n/config_models/defaults.py new file mode 100644 index 0000000000000..4135203c1eefd --- /dev/null +++ b/n8n/datadog_checks/n8n/config_models/defaults.py @@ -0,0 +1,132 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + + +def instance_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_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_raw_metric_prefix(): + return 'n8n' + + +def instance_request_size(): + return 16 + + +def instance_server_port(): + return 5678 + + +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/n8n/datadog_checks/n8n/config_models/instance.py b/n8n/datadog_checks/n8n/config_models/instance.py new file mode 100644 index 0000000000000..41324a6b93245 --- /dev/null +++ b/n8n/datadog_checks/n8n/config_models/instance.py @@ -0,0 +1,174 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from 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 + + +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 + exclude_labels: 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_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 + server_port: Optional[int] = 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) + 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/n8n/datadog_checks/n8n/config_models/shared.py b/n8n/datadog_checks/n8n/config_models/shared.py new file mode 100644 index 0000000000000..8721d9e284e5f --- /dev/null +++ b/n8n/datadog_checks/n8n/config_models/shared.py @@ -0,0 +1,45 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, ConfigDict, field_validator, model_validator + +from datadog_checks.base.utils.functions import identity +from datadog_checks.base.utils.models import validation + +from . import validators + + +class SharedConfig(BaseModel): + model_config = ConfigDict( + validate_default=True, + arbitrary_types_allowed=True, + frozen=True, + ) + service: Optional[str] = None + + @model_validator(mode='before') + def _initial_validation(cls, values): + return validation.core.initialize_config(getattr(validators, 'initialize_shared', identity)(values)) + + @field_validator('*', mode='before') + def _validate(cls, value, info): + field = cls.model_fields[info.field_name] + field_name = field.alias or info.field_name + if field_name in info.context['configured_fields']: + value = getattr(validators, f'shared_{info.field_name}', identity)(value, field=field) + + return validation.utils.make_immutable(value) + + @model_validator(mode='after') + def _final_validation(cls, model): + return validation.core.check_model(getattr(validators, 'check_shared', identity)(model)) diff --git a/n8n/datadog_checks/n8n/config_models/validators.py b/n8n/datadog_checks/n8n/config_models/validators.py new file mode 100644 index 0000000000000..8495a481e5308 --- /dev/null +++ b/n8n/datadog_checks/n8n/config_models/validators.py @@ -0,0 +1,13 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# Here you can include additional config validators or transformers +# +# def initialize_instance(values, **kwargs): +# if 'my_option' not in values and 'my_legacy_option' in values: +# values['my_option'] = values['my_legacy_option'] +# if values.get('my_number') > 10: +# raise ValueError('my_number max value is 10, got %s' % str(values.get('my_number'))) +# +# return values diff --git a/n8n/datadog_checks/n8n/data/conf.yaml.example b/n8n/datadog_checks/n8n/data/conf.yaml.example new file mode 100644 index 0000000000000..af98392be000c --- /dev/null +++ b/n8n/datadog_checks/n8n/data/conf.yaml.example @@ -0,0 +1,642 @@ +## All options defined here are available to all instances. +# +init_config: + + ## @param service - string - optional + ## Attach the tag `service:` to every metric, event, and service check emitted by this integration. + ## + ## Additionally, this sets the default `service` for every log source. + # + # service: + +## Every instance is scheduled independently of the others. +# +instances: + + ## @param openmetrics_endpoint - string - required + ## Endpoint exposing the n8n's metrics in the OpenMetrics format. For more information, refer to: + ## https://docs.n8n.io/hosting/logging-monitoring/monitoring/ + ## https://docs.n8n.io/hosting/configuration/environment-variables/endpoints/ + # + - openmetrics_endpoint: http://localhost:5678 + + ## @param raw_metric_prefix - string - optional - default: n8n + ## The prefix prepended to all metrics from n8n. + ## If not set, the default prefix is used. + ## The default prefix is 'n8n'. + ## If you are using a custom prefix in n8n through N8N_METRICS_PREFIX, you can set it here. + # + # raw_metric_prefix: n8n + + ## @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 server_port - integer - optional - default: 5678 + ## The port exposing the HTTP endpoint of the n8n API. + # + # server_port: + +## Log Section +## +## type - required - Type of log input source (tcp / udp / file / windows_event). +## port / path / channel_path - required - Set port if type is tcp or udp. +## Set path if type is file. +## Set channel_path if type is windows_event. +## source - required - Attribute that defines which integration sent the logs. +## encoding - optional - For file specifies the file encoding. Default is utf-8. Other +## possible values are utf-16-le and utf-16-be. +## service - optional - The name of the service that generates the log. +## Overrides any `service` defined in the `init_config` section. +## tags - optional - Add tags to the collected logs. +## +## Discover Datadog log collection: https://docs.datadoghq.com/logs/log_collection/ +# +# logs: +# - type: file +# path: /var/log/n8n/*.log +# source: n8n +# service: +# - type: docker +# source: n8n +# service: diff --git a/n8n/datadog_checks/n8n/metrics.py b/n8n/datadog_checks/n8n/metrics.py new file mode 100644 index 0000000000000..f28b63d2946c4 --- /dev/null +++ b/n8n/datadog_checks/n8n/metrics.py @@ -0,0 +1,79 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# Metrics mapping without prefix - use raw_metric_prefix config to strip prefixes like 'n8n_', 'n8n_my_team_', etc. +# Namespace will be applied by the check +METRIC_MAP = { + 'active_workflow_count': 'active.workflow.count', + 'api_request_duration_seconds': 'api.request.duration.seconds', + 'api_requests': 'api.requests.count', + 'cache_errors': 'cache.errors.count', + 'cache_hits': 'cache.hits.count', + 'cache_latency_seconds': 'cache.latency.seconds', + 'cache_misses': 'cache.misses.count', + 'cache_operations': 'cache.operations.count', + 'eventbus_connections_total': 'eventbus.connections.total', + 'eventbus_events_failed': 'eventbus.events.failed.count', + 'eventbus_events_processed': 'eventbus.events.processed.count', + 'eventbus_events': 'eventbus.events.count', + 'eventbus_queue_size': 'eventbus.queue.size', + 'http_request_duration_seconds': 'http.request.duration.seconds', + 'instance_role_leader': 'instance.role.leader', + 'last_activity': 'last.activity', + 'nodejs_active_handles': 'nodejs.active.handles', + 'nodejs_active_handles_total': 'nodejs.active.handles.total', + 'nodejs_active_requests': 'nodejs.active.requests', + 'nodejs_active_requests_total': 'nodejs.active.requests.total', + 'nodejs_active_resources': 'nodejs.active.resources', + 'nodejs_active_resources_total': 'nodejs.active.resources.total', + 'nodejs_event_loop_lag_seconds': 'nodejs.event.loop.lag.seconds', + 'nodejs_eventloop_lag_max_seconds': 'nodejs.eventloop.lag.max.seconds', + 'nodejs_eventloop_lag_mean_seconds': 'nodejs.eventloop.lag.mean.seconds', + 'nodejs_eventloop_lag_min_seconds': 'nodejs.eventloop.lag.min.seconds', + 'nodejs_eventloop_lag_p50_seconds': 'nodejs.eventloop.lag.p50.seconds', + 'nodejs_eventloop_lag_p90_seconds': 'nodejs.eventloop.lag.p90.seconds', + 'nodejs_eventloop_lag_p99_seconds': 'nodejs.eventloop.lag.p99.seconds', + 'nodejs_eventloop_lag_seconds': 'nodejs.eventloop.lag.seconds', + 'nodejs_eventloop_lag_stddev_seconds': 'nodejs.eventloop.lag.stddev.seconds', + 'nodejs_external_memory_bytes': 'nodejs.external.memory.bytes', + 'nodejs_gc_duration_seconds': 'nodejs.gc.duration.seconds', + 'nodejs_heap_size_total_bytes': 'nodejs.heap.size.total.bytes', + 'nodejs_heap_size_used_bytes': 'nodejs.heap.size.used.bytes', + 'nodejs_heap_space_size_available_bytes': 'nodejs.heap.space.size.available.bytes', + 'nodejs_heap_space_size_total_bytes': 'nodejs.heap.space.size.total.bytes', + 'nodejs_heap_space_size_used_bytes': 'nodejs.heap.space.size.used.bytes', + 'nodejs_heap_total_bytes': 'nodejs.heap.total.bytes', + 'nodejs_heap_used_bytes': 'nodejs.heap.used.bytes', + 'nodejs_version_info': 'nodejs.version.info', + 'process_cpu_system_seconds': 'process.cpu.system.seconds.count', + 'process_cpu_user_seconds': 'process.cpu.user.seconds.count', + 'process_heap_bytes': 'process.heap.bytes', + 'process_max_fds': 'process.max.fds', + 'process_open_fds': 'process.open.fds', + 'process_resident_memory_bytes': 'process.resident.memory.bytes', + 'process_start_time_seconds': { + 'name': 'process.uptime.seconds', + 'type': 'time_elapsed', + }, + 'process_virtual_memory_bytes': 'process.virtual.memory.bytes', + 'queue_job_active_total': 'queue.job.active.total', + 'queue_job_attempts': 'queue.job.attempts.count', + 'queue_job_completed': 'queue.job.completed.count', + 'queue_job_delayed_total': 'queue.job.delayed.total', + 'queue_job_dequeued': 'queue.job.dequeued.count', + 'queue_job_enqueued': 'queue.job.enqueued.count', + 'queue_job_failed': 'queue.job.failed.count', + 'queue_job_waiting_duration_seconds': 'queue.job.waiting.duration.seconds', + 'queue_job_waiting_total': 'queue.job.waiting.total', + 'queue_jobs_duration_seconds': 'queue.jobs.duration.seconds', + 'queue_jobs': 'queue.jobs.count', + 'workflow_executions_active': 'workflow.executions.active', + 'workflow_executions_duration_seconds': 'workflow.executions.duration.seconds', + 'workflow_executions': 'workflow.executions.count', + 'workflow_failed': 'workflow.failed.count', + 'workflow_started': 'workflow.started.count', + 'workflow_success': 'workflow.success.count', + 'process_cpu_seconds': 'process.cpu.seconds.count', + 'version_info': 'version.info', +} diff --git a/n8n/hatch.toml b/n8n/hatch.toml new file mode 100644 index 0000000000000..fee2455000258 --- /dev/null +++ b/n8n/hatch.toml @@ -0,0 +1,4 @@ +[env.collectors.datadog-checks] + +[[envs.default.matrix]] +python = ["3.13"] diff --git a/n8n/manifest.json b/n8n/manifest.json new file mode 100644 index 0000000000000..607f65ee5b0e6 --- /dev/null +++ b/n8n/manifest.json @@ -0,0 +1,56 @@ +{ + "manifest_version": "2.0.0", + "app_uuid": "4775a4f7-3e41-49db-8a3f-846d18f1a4c8", + "app_id": "n8n", + "display_on_public_website": false, + "tile": { + "overview": "README.md#Overview", + "configuration": "README.md#Setup", + "support": "README.md#Support", + "changelog": "CHANGELOG.md", + "description": "Monitor the health and performance of n8n.", + "title": "n8n", + "media": [], + "classifier_tags": [ + "Supported OS::Linux", + "Supported OS::Windows", + "Supported OS::macOS", + "Category::Metrics", + "Category::Log Collection", + "Offering::Integration", + "Queried Data Type::Metrics", + "Queried Data Type::Logs", + "Submitted Data Type::Logs", + "Submitted Data Type::Metrics" + ] + }, + "assets": { + "integration": { + "auto_install": true, + "source_type_id": 61226509, + "source_type_name": "n8n", + "configuration": { + "spec": "assets/configuration/spec.yaml" + }, + "events": { + "creates_events": false + }, + "metrics": { + "prefix": "n8n.", + "check": "n8n.active.workflow.count", + "metadata_path": "metadata.csv" + } + }, + "dashboards": { + "N8N overview": "assets/dashboards/n8n_overview.json" + }, + "monitors": {}, + "saved_views": {} + }, + "author": { + "support_email": "help@datadoghq.com", + "name": "Datadog", + "homepage": "https://www.datadoghq.com", + "sales_email": "info@datadoghq.com" + } +} diff --git a/n8n/metadata.csv b/n8n/metadata.csv new file mode 100644 index 0000000000000..0cc823442f6b1 --- /dev/null +++ b/n8n/metadata.csv @@ -0,0 +1,76 @@ +metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name,curated_metric,sample_tags +n8n.active.workflow.count,gauge,,,,Total number of active workflows.,0,n8n,,, +n8n.api.request.duration.seconds.count,count,,,,The count of API request duration in seconds,0,n8n,,, +n8n.api.request.duration.seconds.sum,count,,,,The sum of API request duration in seconds,0,n8n,,, +n8n.api.requests.count,count,,,,Total API requests,0,n8n,,, +n8n.cache.errors.count,count,,,,Cache errors,0,n8n,,, +n8n.cache.hits.count,count,,,,Cache hits,0,n8n,,, +n8n.cache.latency.seconds.count,count,,,,The count of cache operation latency in seconds,0,n8n,,, +n8n.cache.latency.seconds.sum,count,,,,The sum of cache operation latency in seconds,0,n8n,,, +n8n.cache.misses.count,count,,,,Cache misses,0,n8n,,, +n8n.cache.operations.count,count,,,,Total cache operations,0,n8n,,, +n8n.eventbus.connections.total,gauge,,,,Active event bus backend connections,0,n8n,,, +n8n.eventbus.events.count,count,,,,Total events published on the event bus,0,n8n,,, +n8n.eventbus.events.failed.count,count,,,,Total failed event processing,0,n8n,,, +n8n.eventbus.events.processed.count,count,,,,Total processed events,0,n8n,,, +n8n.eventbus.queue.size,gauge,,,,Current event queue size,0,n8n,,, +n8n.http.request.duration.seconds.count,count,,,,The count of the http responses duration labeled with: status_code,0,n8n,,, +n8n.http.request.duration.seconds.sum,count,,,,The sum of the http responses duration labeled with: status_code,0,n8n,,, +n8n.instance.role.leader,gauge,,,,Whether this main instance is the leader (1) or not (0).,0,n8n,,, +n8n.last.activity,gauge,,,,last instance activity (backend request) in Unix time (seconds).,0,n8n,,, +n8n.nodejs.active.handles,gauge,,,,Number of active libuv handles grouped by handle type. Every handle type is C++ class name.,0,n8n,,, +n8n.nodejs.active.handles.total,gauge,,,,Total number of active handles.,0,n8n,,, +n8n.nodejs.active.requests,gauge,,,,Number of active libuv requests grouped by request type. Every request type is C++ class name.,0,n8n,,, +n8n.nodejs.active.requests.total,gauge,,,,Total number of active requests.,0,n8n,,, +n8n.nodejs.active.resources,gauge,,,,"Number of active resources that are currently keeping the event loop alive, grouped by async resource type.",0,n8n,,, +n8n.nodejs.active.resources.total,gauge,,,,Total number of active resources.,0,n8n,,, +n8n.nodejs.event.loop.lag.seconds,gauge,,,,Event loop lag in seconds,0,n8n,,, +n8n.nodejs.eventloop.lag.max.seconds,gauge,,,,The maximum recorded event loop delay.,0,n8n,,, +n8n.nodejs.eventloop.lag.mean.seconds,gauge,,,,The mean of the recorded event loop delays.,0,n8n,,, +n8n.nodejs.eventloop.lag.min.seconds,gauge,,,,The minimum recorded event loop delay.,0,n8n,,, +n8n.nodejs.eventloop.lag.p50.seconds,gauge,,,,The 50th percentile of the recorded event loop delays.,0,n8n,,, +n8n.nodejs.eventloop.lag.p90.seconds,gauge,,,,The 90th percentile of the recorded event loop delays.,0,n8n,,, +n8n.nodejs.eventloop.lag.p99.seconds,gauge,,,,The 99th percentile of the recorded event loop delays.,0,n8n,,, +n8n.nodejs.eventloop.lag.seconds,gauge,,,,Lag of event loop in seconds.,0,n8n,,, +n8n.nodejs.eventloop.lag.stddev.seconds,gauge,,,,The standard deviation of the recorded event loop delays.,0,n8n,,, +n8n.nodejs.external.memory.bytes,gauge,,,,Node.js external memory size in bytes.,0,n8n,,, +n8n.nodejs.gc.duration.seconds.count,count,,,,The count of garbage collection duration by kind,0,n8n,,, +n8n.nodejs.gc.duration.seconds.sum,count,,,,The sum of garbage collection duration by kind,0,n8n,,, +n8n.nodejs.heap.size.total.bytes,gauge,,,,Process heap size from Node.js in bytes.,0,n8n,,, +n8n.nodejs.heap.size.used.bytes,gauge,,,,Process heap size used from Node.js in bytes.,0,n8n,,, +n8n.nodejs.heap.space.size.available.bytes,gauge,,,,Process heap space size available from Node.js in bytes.,0,n8n,,, +n8n.nodejs.heap.space.size.total.bytes,gauge,,,,Process heap space size total from Node.js in bytes.,0,n8n,,, +n8n.nodejs.heap.space.size.used.bytes,gauge,,,,Process heap space size used from Node.js in bytes.,0,n8n,,, +n8n.nodejs.heap.total.bytes,gauge,,,,Total heap size allocated in bytes,0,n8n,,, +n8n.nodejs.heap.used.bytes,gauge,,,,Heap memory used in bytes,0,n8n,,, +n8n.nodejs.version.info,gauge,,,,Node.js version info.,0,n8n,,, +n8n.process.cpu.seconds.count,count,,,,Total user and system CPU time spent in seconds.,0,n8n,,, +n8n.process.cpu.system.seconds.count,count,,,,Total system CPU time spent in seconds.,0,n8n,,, +n8n.process.cpu.user.seconds.count,count,,,,Total user CPU time spent in seconds.,0,n8n,,, +n8n.process.heap.bytes,gauge,,,,Process heap size in bytes.,0,n8n,,, +n8n.process.max.fds,gauge,,,,Maximum number of open file descriptors.,0,n8n,,, +n8n.process.open.fds,gauge,,,,Number of open file descriptors.,0,n8n,,, +n8n.process.resident.memory.bytes,gauge,,,,Resident memory size in bytes.,0,n8n,,, +n8n.process.start.time.seconds,gauge,,,,Start time of the process since unix epoch in seconds.,0,n8n,,, +n8n.process.virtual.memory.bytes,gauge,,,,Virtual memory size in bytes.,0,n8n,,, +n8n.queue.job.active.total,gauge,,,,Number of jobs currently being processed,0,n8n,,, +n8n.queue.job.attempts.count,count,,,,Total number of job attempts,0,n8n,,, +n8n.queue.job.completed.count,count,,,,Number of jobs completed successfully,0,n8n,,, +n8n.queue.job.delayed.total,gauge,,,,Number of jobs scheduled to run later,0,n8n,,, +n8n.queue.job.dequeued.count,count,,,,Number of jobs dequeued (picked up from queue),0,n8n,,, +n8n.queue.job.enqueued.count,count,,,,Number of jobs added to the queue,0,n8n,,, +n8n.queue.job.failed.count,count,,,,Number of jobs that have failed,0,n8n,,, +n8n.queue.job.waiting.duration.seconds.count,count,,,,The count of duration jobs spend waiting before being processed,0,n8n,,, +n8n.queue.job.waiting.duration.seconds.sum,count,,,,The sum of duration jobs spend waiting before being processed,0,n8n,,, +n8n.queue.job.waiting.total,gauge,,,,Number of jobs currently waiting in the queue,0,n8n,,, +n8n.queue.jobs.count,count,,,,Total number of queue jobs,0,n8n,,, +n8n.queue.jobs.duration.seconds.count,count,,,,The count of job duration in seconds,0,n8n,,, +n8n.queue.jobs.duration.seconds.sum,count,,,,The sum of job duration in seconds,0,n8n,,, +n8n.version.info,gauge,,,,n8n version info.,0,n8n,,, +n8n.workflow.executions.active,gauge,,,,Number of active workflow executions,0,n8n,,, +n8n.workflow.executions.count,count,,,,Total number of workflow executions,0,n8n,,, +n8n.workflow.executions.duration.seconds.count,count,,,,The count of workflow execution duration in seconds,0,n8n,,, +n8n.workflow.executions.duration.seconds.sum,count,,,,The sum of workflow execution duration in seconds,0,n8n,,, +n8n.workflow.failed.count,count,,,,Total number of workflows that failed,0,n8n,,, +n8n.workflow.started.count,count,,,,Total number of workflows started,0,n8n,,, +n8n.workflow.success.count,count,,,,Total number of workflows completed successfully,0,n8n,,, diff --git a/n8n/pyproject.toml b/n8n/pyproject.toml new file mode 100644 index 0000000000000..431be71a610a4 --- /dev/null +++ b/n8n/pyproject.toml @@ -0,0 +1,60 @@ +[build-system] +requires = [ + "hatchling>=0.13.0", +] +build-backend = "hatchling.build" + +[project] +name = "datadog-n8n" +description = "The n8n check" +readme = "README.md" +license = "BSD-3-Clause" +requires-python = ">=3.13" +keywords = [ + "datadog", + "datadog agent", + "datadog check", + "n8n", +] +authors = [ + { name = "Datadog", email = "packages@datadoghq.com" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Private :: Do Not Upload", + "Programming Language :: Python :: 3.13", + "Topic :: System :: Monitoring", +] +dependencies = [ + "datadog-checks-base>=37.21.0", +] +dynamic = [ + "version", +] + +[project.optional-dependencies] +deps = [] + +[project.urls] +Source = "https://github.com/DataDog/integrations-core" + +[tool.hatch.version] +path = "datadog_checks/n8n/__about__.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/datadog_checks", + "/tests", + "/manifest.json", +] + +[tool.hatch.build.targets.wheel] +include = [ + "/datadog_checks/n8n", +] +dev-mode-dirs = [ + ".", +] diff --git a/n8n/tests/__init__.py b/n8n/tests/__init__.py new file mode 100644 index 0000000000000..c9f1f2a9882c7 --- /dev/null +++ b/n8n/tests/__init__.py @@ -0,0 +1,3 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) diff --git a/n8n/tests/common.py b/n8n/tests/common.py new file mode 100644 index 0000000000000..414dff6627745 --- /dev/null +++ b/n8n/tests/common.py @@ -0,0 +1,103 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +import os + +from datadog_checks.dev import get_docker_hostname + +HERE = os.path.dirname(os.path.abspath(__file__)) +COMPOSE_FILE = os.path.join(HERE, 'docker', 'docker-compose.yaml') +HOST = get_docker_hostname() +PORT = 5678 + + +def get_fixture_path(filename): + return os.path.join(HERE, 'fixtures', filename) + + +OPENMETRICS_URL = f'http://{HOST}:{PORT}' +INSTANCE = { + 'openmetrics_endpoint': f'{OPENMETRICS_URL}/metrics', +} + +E2E_METADATA = { + 'docker_volumes': ['/var/run/docker.sock:/var/run/docker.sock:ro'], +} + +TEST_METRICS = [ + 'n8n.active.workflow.count', + 'n8n.api.request.duration.seconds.count', + 'n8n.api.request.duration.seconds.sum', + 'n8n.api.requests.count', + 'n8n.cache.errors.count', + 'n8n.cache.hits.count', + 'n8n.cache.latency.seconds.count', + 'n8n.cache.latency.seconds.sum', + 'n8n.cache.misses.count', + 'n8n.cache.operations.count', + 'n8n.eventbus.connections.total', + 'n8n.eventbus.events.failed.count', + 'n8n.eventbus.events.processed.count', + 'n8n.eventbus.events.count', + 'n8n.eventbus.queue.size', + 'n8n.http.request.duration.seconds.count', + 'n8n.http.request.duration.seconds.sum', + 'n8n.instance.role.leader', + 'n8n.last.activity', + 'n8n.nodejs.active.handles', + 'n8n.nodejs.active.handles.total', + 'n8n.nodejs.active.requests', + 'n8n.nodejs.active.requests.total', + 'n8n.nodejs.active.resources', + 'n8n.nodejs.active.resources.total', + 'n8n.nodejs.event.loop.lag.seconds', + 'n8n.nodejs.eventloop.lag.max.seconds', + 'n8n.nodejs.eventloop.lag.mean.seconds', + 'n8n.nodejs.eventloop.lag.min.seconds', + 'n8n.nodejs.eventloop.lag.p50.seconds', + 'n8n.nodejs.eventloop.lag.p90.seconds', + 'n8n.nodejs.eventloop.lag.p99.seconds', + 'n8n.nodejs.eventloop.lag.seconds', + 'n8n.nodejs.eventloop.lag.stddev.seconds', + 'n8n.nodejs.external.memory.bytes', + 'n8n.nodejs.gc.duration.seconds.count', + 'n8n.nodejs.gc.duration.seconds.sum', + 'n8n.nodejs.heap.size.total.bytes', + 'n8n.nodejs.heap.size.used.bytes', + 'n8n.nodejs.heap.space.size.available.bytes', + 'n8n.nodejs.heap.space.size.total.bytes', + 'n8n.nodejs.heap.space.size.used.bytes', + 'n8n.nodejs.heap.total.bytes', + 'n8n.nodejs.heap.used.bytes', + 'n8n.nodejs.version.info', + 'n8n.process.cpu.system.seconds.count', + 'n8n.process.cpu.user.seconds.count', + 'n8n.process.heap.bytes', + 'n8n.process.max.fds', + 'n8n.process.open.fds', + 'n8n.process.resident.memory.bytes', + 'n8n.process.start.time.seconds', + 'n8n.process.virtual.memory.bytes', + 'n8n.queue.job.active.total', + 'n8n.queue.job.attempts.count', + 'n8n.queue.job.completed.count', + 'n8n.queue.job.delayed.total', + 'n8n.queue.job.dequeued.count', + 'n8n.queue.job.enqueued.count', + 'n8n.queue.job.failed.count', + 'n8n.queue.job.waiting.duration.seconds.count', + 'n8n.queue.job.waiting.duration.seconds.sum', + 'n8n.queue.job.waiting.total', + 'n8n.queue.jobs.duration.seconds.count', + 'n8n.queue.jobs.duration.seconds.sum', + 'n8n.queue.jobs.count', + 'n8n.workflow.executions.active', + 'n8n.workflow.executions.duration.seconds.count', + 'n8n.workflow.executions.duration.seconds.sum', + 'n8n.workflow.executions.count', + 'n8n.workflow.failed.count', + 'n8n.workflow.started.count', + 'n8n.workflow.success.count', + 'n8n.process.cpu.seconds.count', + 'n8n.version.info', +] diff --git a/n8n/tests/conftest.py b/n8n/tests/conftest.py new file mode 100644 index 0000000000000..1ad12d6808609 --- /dev/null +++ b/n8n/tests/conftest.py @@ -0,0 +1,32 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +import copy + +import pytest + +from datadog_checks.dev import docker_run +from datadog_checks.dev.conditions import CheckEndpoints + +from . import common + + +@pytest.fixture(scope='session') +def dd_environment(): + compose_file = common.COMPOSE_FILE + conditions = [ + # CheckDockerLogs(identifier='n8n', patterns=['server running']), + CheckEndpoints(common.INSTANCE["openmetrics_endpoint"]), + CheckEndpoints(f'{common.OPENMETRICS_URL}/healthz', attempts=60, wait=3), + CheckEndpoints(f'{common.OPENMETRICS_URL}/metrics', attempts=60, wait=3), + ] + with docker_run(compose_file, conditions=conditions): + yield { + 'instances': [common.INSTANCE], + } + + +@pytest.fixture +def instance(): + return copy.deepcopy(common.INSTANCE) diff --git a/n8n/tests/docker/Dockerfile b/n8n/tests/docker/Dockerfile new file mode 100644 index 0000000000000..232eb7b8808d1 --- /dev/null +++ b/n8n/tests/docker/Dockerfile @@ -0,0 +1,13 @@ +FROM docker.n8n.io/n8nio/n8n:latest + +# Set environment variables to enable metrics and logging +ENV N8N_METRICS=true \ + N8N_LOG_LEVEL=debug \ + N8N_METRICS_INCLUDE_DEFAULT_METRICS=true \ + N8N_METRICS_INCLUDE_CACHE_METRICS=true \ + N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS=true \ + N8N_HOST=0.0.0.0 \ + N8N_PORT=5678 + +# Expose the n8n port +EXPOSE 5678 diff --git a/n8n/tests/docker/README.md b/n8n/tests/docker/README.md new file mode 100644 index 0000000000000..bb1d23cc34ce1 --- /dev/null +++ b/n8n/tests/docker/README.md @@ -0,0 +1,88 @@ +# n8n Test Environment + +This directory contains Docker configuration for running an n8n instance with metrics and logging enabled for integration testing. + +## Prerequisites + +- Docker +- Docker Compose + +## Usage + +### Starting the environment + +```bash +cd tests/docker +docker-compose up -d +``` + +### Accessing n8n + +- **Web UI**: http://localhost:5678 +- **Metrics endpoint**: http://localhost:5678/metrics +- **Health check**: http://localhost:5678/healthz + +Default credentials: +- Username: `admin` +- Password: `admin` + +### Viewing logs + +```bash +docker-compose logs -f n8n +``` + +### Stopping the environment + +```bash +docker-compose down +``` + +### Cleaning up (including volumes) + +```bash +docker-compose down -v +``` + +## Configuration + +### Metrics + +The following metrics are enabled: +- Default system metrics +- Cache metrics +- Message event bus metrics +- API endpoint metrics +- Workflow ID labels on workflow metrics + +The metrics are exposed in Prometheus/OpenMetrics format at `http://localhost:5678/metrics`. + +### Logging + +Logs are configured with: +- Log level: `debug` +- Log output: `console` +- Log directory: `./logs` (can be overridden with `N8N_LOG_FOLDER` environment variable) + +### Environment Variables + +You can override environment variables by setting them before running docker-compose: + +```bash +N8N_LOG_FOLDER=/path/to/logs docker-compose up -d +``` + +## Testing + +This setup is designed for integration testing. The n8n instance will: +1. Start with metrics endpoint enabled +2. Expose detailed logs for debugging +3. Include workflow_id labels in workflow metrics +4. Provide a health check endpoint for monitoring readiness + +## Notes + +- The container uses the latest official n8n Docker image +- Data is persisted in a Docker volume named `n8n_data` +- The health check waits up to 30 seconds for n8n to start before marking it as healthy + diff --git a/n8n/tests/docker/docker-compose.yaml b/n8n/tests/docker/docker-compose.yaml new file mode 100644 index 0000000000000..fb8da72559b78 --- /dev/null +++ b/n8n/tests/docker/docker-compose.yaml @@ -0,0 +1,41 @@ +services: + n8n: + build: + context: . + dockerfile: Dockerfile + container_name: n8n-test + ports: + - "5678:5678" + environment: + # Enable metrics endpoint + - N8N_METRICS=true + - N8N_METRICS_INCLUDE_DEFAULT_METRICS=true + - N8N_METRICS_INCLUDE_CACHE_METRICS=true + - N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS=true + - N8N_METRICS_INCLUDE_API_ENDPOINTS=true + - N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL=true + # Logging configuration + - N8N_LOG_LEVEL=debug + - N8N_LOG_OUTPUT=console + # Basic configuration + - N8N_HOST=0.0.0.0 + - N8N_PORT=5678 + - N8N_PROTOCOL=http + # Authentication (optional for testing) + - N8N_BASIC_AUTH_ACTIVE=true + - N8N_BASIC_AUTH_USER=admin + - N8N_BASIC_AUTH_PASSWORD=admin + volumes: + - n8n_data:/home/node/.n8n + - ${N8N_LOG_FOLDER:-./logs}:/var/log/n8n + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + +volumes: + n8n_data: + driver: local + diff --git a/n8n/tests/fixtures/n8n.txt b/n8n/tests/fixtures/n8n.txt new file mode 100644 index 0000000000000..c670f02d7fe46 --- /dev/null +++ b/n8n/tests/fixtures/n8n.txt @@ -0,0 +1,409 @@ +# HELP n8n_process_cpu_user_seconds_total Total user CPU time spent in seconds. +# TYPE n8n_process_cpu_user_seconds_total counter +n8n_process_cpu_user_seconds_total 8.298932999999998 + +# HELP n8n_process_cpu_system_seconds_total Total system CPU time spent in seconds. +# TYPE n8n_process_cpu_system_seconds_total counter +n8n_process_cpu_system_seconds_total 3.1041119999999998 + +# HELP n8n_process_cpu_seconds_total Total user and system CPU time spent in seconds. +# TYPE n8n_process_cpu_seconds_total counter +n8n_process_cpu_seconds_total 11.403044999999999 + +# HELP n8n_process_start_time_seconds Start time of the process since unix epoch in seconds. +# TYPE n8n_process_start_time_seconds gauge +n8n_process_start_time_seconds 1761656578 + +# HELP n8n_process_resident_memory_bytes Resident memory size in bytes. +# TYPE n8n_process_resident_memory_bytes gauge +n8n_process_resident_memory_bytes 245043200 + +# HELP n8n_process_virtual_memory_bytes Virtual memory size in bytes. +# TYPE n8n_process_virtual_memory_bytes gauge +n8n_process_virtual_memory_bytes 33656197120 + +# HELP n8n_process_heap_bytes Process heap size in bytes. +# TYPE n8n_process_heap_bytes gauge +n8n_process_heap_bytes 277200896 + +# HELP n8n_process_open_fds Number of open file descriptors. +# TYPE n8n_process_open_fds gauge +n8n_process_open_fds 44 + +# HELP n8n_process_max_fds Maximum number of open file descriptors. +# TYPE n8n_process_max_fds gauge +n8n_process_max_fds 1048576 + +# HELP n8n_nodejs_eventloop_lag_seconds Lag of event loop in seconds. +# TYPE n8n_nodejs_eventloop_lag_seconds gauge +n8n_nodejs_eventloop_lag_seconds 0.002765567 + +# HELP n8n_nodejs_eventloop_lag_min_seconds The minimum recorded event loop delay. +# TYPE n8n_nodejs_eventloop_lag_min_seconds gauge +n8n_nodejs_eventloop_lag_min_seconds 0.010018816 + +# HELP n8n_nodejs_eventloop_lag_max_seconds The maximum recorded event loop delay. +# TYPE n8n_nodejs_eventloop_lag_max_seconds gauge +n8n_nodejs_eventloop_lag_max_seconds 0.011239423 + +# HELP n8n_nodejs_eventloop_lag_mean_seconds The mean of the recorded event loop delays. +# TYPE n8n_nodejs_eventloop_lag_mean_seconds gauge +n8n_nodejs_eventloop_lag_mean_seconds 0.010092521938958708 + +# HELP n8n_nodejs_eventloop_lag_stddev_seconds The standard deviation of the recorded event loop delays. +# TYPE n8n_nodejs_eventloop_lag_stddev_seconds gauge +n8n_nodejs_eventloop_lag_stddev_seconds 0.00016945350643679045 + +# HELP n8n_nodejs_eventloop_lag_p50_seconds The 50th percentile of the recorded event loop delays. +# TYPE n8n_nodejs_eventloop_lag_p50_seconds gauge +n8n_nodejs_eventloop_lag_p50_seconds 0.010067967 + +# HELP n8n_nodejs_eventloop_lag_p90_seconds The 90th percentile of the recorded event loop delays. +# TYPE n8n_nodejs_eventloop_lag_p90_seconds gauge +n8n_nodejs_eventloop_lag_p90_seconds 0.010067967 + +# HELP n8n_nodejs_eventloop_lag_p99_seconds The 99th percentile of the recorded event loop delays. +# TYPE n8n_nodejs_eventloop_lag_p99_seconds gauge +n8n_nodejs_eventloop_lag_p99_seconds 0.011124735 + +# HELP n8n_nodejs_active_resources Number of active resources that are currently keeping the event loop alive, grouped by async resource type. +# TYPE n8n_nodejs_active_resources gauge +n8n_nodejs_active_resources{type="PipeWrap"} 2 +n8n_nodejs_active_resources{type="TCPServerWrap"} 1 +n8n_nodejs_active_resources{type="TCPSocketWrap"} 1 +n8n_nodejs_active_resources{type="Timeout"} 13 +n8n_nodejs_active_resources{type="Immediate"} 1 + +# HELP n8n_nodejs_active_resources_total Total number of active resources. +# TYPE n8n_nodejs_active_resources_total gauge +n8n_nodejs_active_resources_total 18 + +# HELP n8n_nodejs_active_handles Number of active libuv handles grouped by handle type. Every handle type is C++ class name. +# TYPE n8n_nodejs_active_handles gauge +n8n_nodejs_active_handles{type="Socket"} 3 +n8n_nodejs_active_handles{type="Server"} 1 + +# HELP n8n_nodejs_active_handles_total Total number of active handles. +# TYPE n8n_nodejs_active_handles_total gauge +n8n_nodejs_active_handles_total 4 + +# HELP n8n_nodejs_active_requests Number of active libuv requests grouped by request type. Every request type is C++ class name. +# TYPE n8n_nodejs_active_requests gauge + +# HELP n8n_nodejs_active_requests_total Total number of active requests. +# TYPE n8n_nodejs_active_requests_total gauge +n8n_nodejs_active_requests_total 0 + +# HELP n8n_nodejs_heap_size_total_bytes Process heap size from Node.js in bytes. +# TYPE n8n_nodejs_heap_size_total_bytes gauge +n8n_nodejs_heap_size_total_bytes 142774272 + +# HELP n8n_nodejs_heap_size_used_bytes Process heap size used from Node.js in bytes. +# TYPE n8n_nodejs_heap_size_used_bytes gauge +n8n_nodejs_heap_size_used_bytes 136342632 + +# HELP n8n_nodejs_external_memory_bytes Node.js external memory size in bytes. +# TYPE n8n_nodejs_external_memory_bytes gauge +n8n_nodejs_external_memory_bytes 20824585 + +# HELP n8n_nodejs_heap_space_size_total_bytes Process heap space size total from Node.js in bytes. +# TYPE n8n_nodejs_heap_space_size_total_bytes gauge +n8n_nodejs_heap_space_size_total_bytes{space="read_only"} 0 +n8n_nodejs_heap_space_size_total_bytes{space="new"} 1048576 +n8n_nodejs_heap_space_size_total_bytes{space="old"} 122208256 +n8n_nodejs_heap_space_size_total_bytes{space="code"} 4718592 +n8n_nodejs_heap_space_size_total_bytes{space="shared"} 0 +n8n_nodejs_heap_space_size_total_bytes{space="trusted"} 7643136 +n8n_nodejs_heap_space_size_total_bytes{space="new_large_object"} 0 +n8n_nodejs_heap_space_size_total_bytes{space="large_object"} 7000064 +n8n_nodejs_heap_space_size_total_bytes{space="code_large_object"} 155648 +n8n_nodejs_heap_space_size_total_bytes{space="shared_large_object"} 0 +n8n_nodejs_heap_space_size_total_bytes{space="trusted_large_object"} 0 + +# HELP n8n_nodejs_heap_space_size_used_bytes Process heap space size used from Node.js in bytes. +# TYPE n8n_nodejs_heap_space_size_used_bytes gauge +n8n_nodejs_heap_space_size_used_bytes{space="read_only"} 0 +n8n_nodejs_heap_space_size_used_bytes{space="new"} 652896 +n8n_nodejs_heap_space_size_used_bytes{space="old"} 119347344 +n8n_nodejs_heap_space_size_used_bytes{space="code"} 4183424 +n8n_nodejs_heap_space_size_used_bytes{space="shared"} 0 +n8n_nodejs_heap_space_size_used_bytes{space="trusted"} 5187192 +n8n_nodejs_heap_space_size_used_bytes{space="new_large_object"} 0 +n8n_nodejs_heap_space_size_used_bytes{space="large_object"} 6837144 +n8n_nodejs_heap_space_size_used_bytes{space="code_large_object"} 138432 +n8n_nodejs_heap_space_size_used_bytes{space="shared_large_object"} 0 +n8n_nodejs_heap_space_size_used_bytes{space="trusted_large_object"} 0 + +# HELP n8n_nodejs_heap_space_size_available_bytes Process heap space size available from Node.js in bytes. +# TYPE n8n_nodejs_heap_space_size_available_bytes gauge +n8n_nodejs_heap_space_size_available_bytes{space="read_only"} 0 +n8n_nodejs_heap_space_size_available_bytes{space="new"} 378016 +n8n_nodejs_heap_space_size_available_bytes{space="old"} 430568 +n8n_nodejs_heap_space_size_available_bytes{space="code"} 239680 +n8n_nodejs_heap_space_size_available_bytes{space="shared"} 0 +n8n_nodejs_heap_space_size_available_bytes{space="trusted"} 2323072 +n8n_nodejs_heap_space_size_available_bytes{space="new_large_object"} 1048576 +n8n_nodejs_heap_space_size_available_bytes{space="large_object"} 0 +n8n_nodejs_heap_space_size_available_bytes{space="code_large_object"} 0 +n8n_nodejs_heap_space_size_available_bytes{space="shared_large_object"} 0 +n8n_nodejs_heap_space_size_available_bytes{space="trusted_large_object"} 0 + +# HELP n8n_nodejs_version_info Node.js version info. +# TYPE n8n_nodejs_version_info gauge +n8n_nodejs_version_info{version="v22.18.0",major="22",minor="18",patch="0"} 1 + +# HELP n8n_nodejs_gc_duration_seconds Garbage collection duration by kind, one of major, minor, incremental or weakcb. +# TYPE n8n_nodejs_gc_duration_seconds histogram +n8n_nodejs_gc_duration_seconds_bucket{le="0.001",kind="minor"} 128 +n8n_nodejs_gc_duration_seconds_bucket{le="0.01",kind="minor"} 132 +n8n_nodejs_gc_duration_seconds_bucket{le="0.1",kind="minor"} 132 +n8n_nodejs_gc_duration_seconds_bucket{le="1",kind="minor"} 132 +n8n_nodejs_gc_duration_seconds_bucket{le="2",kind="minor"} 132 +n8n_nodejs_gc_duration_seconds_bucket{le="5",kind="minor"} 132 +n8n_nodejs_gc_duration_seconds_bucket{le="+Inf",kind="minor"} 132 +n8n_nodejs_gc_duration_seconds_sum{kind="minor"} 0.09924478498101237 +n8n_nodejs_gc_duration_seconds_count{kind="minor"} 132 +n8n_nodejs_gc_duration_seconds_bucket{le="0.001",kind="incremental"} 1 +n8n_nodejs_gc_duration_seconds_bucket{le="0.01",kind="incremental"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="0.1",kind="incremental"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="1",kind="incremental"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="2",kind="incremental"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="5",kind="incremental"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="+Inf",kind="incremental"} 2 +n8n_nodejs_gc_duration_seconds_sum{kind="incremental"} 0.0022786640077829363 +n8n_nodejs_gc_duration_seconds_count{kind="incremental"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="0.001",kind="major"} 0 +n8n_nodejs_gc_duration_seconds_bucket{le="0.01",kind="major"} 0 +n8n_nodejs_gc_duration_seconds_bucket{le="0.1",kind="major"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="1",kind="major"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="2",kind="major"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="5",kind="major"} 2 +n8n_nodejs_gc_duration_seconds_bucket{le="+Inf",kind="major"} 2 +n8n_nodejs_gc_duration_seconds_sum{kind="major"} 0.1028408939987421 +n8n_nodejs_gc_duration_seconds_count{kind="major"} 2 + +# HELP n8n_version_info n8n version info. +# TYPE n8n_version_info gauge +n8n_version_info{version="v1.117.2",major="1",minor="117",patch="2"} 1 + +# HELP n8n_instance_role_leader Whether this main instance is the leader (1) or not (0). +# TYPE n8n_instance_role_leader gauge +n8n_instance_role_leader 1 + +# HELP n8n_http_request_duration_seconds duration histogram of http responses labeled with: status_code +# TYPE n8n_http_request_duration_seconds histogram + +# HELP n8n_last_activity last instance activity (backend request) in Unix time (seconds). +# TYPE n8n_last_activity gauge +n8n_last_activity 1761656582 + +# HELP n8n_active_workflow_count Total number of active workflows. +# TYPE n8n_active_workflow_count gauge +n8n_active_workflow_count{workflow_id="wf_8a3b2c1d"} 0 +n8n_active_workflow_count{workflow_id="wf_7f4e9a2b"} 0 +n8n_active_workflow_count{workflow_id="wf_5d6c8e1f"} 0 + +# HELP n8n_nodejs_event_loop_lag_seconds Event loop lag in seconds +# TYPE n8n_nodejs_event_loop_lag_seconds gauge +n8n_nodejs_event_loop_lag_seconds 0.0035 + +# HELP n8n_nodejs_heap_total_bytes Total heap size allocated in bytes +# TYPE n8n_nodejs_heap_total_bytes gauge +n8n_nodejs_heap_total_bytes 73400320 + +# HELP n8n_nodejs_heap_used_bytes Heap memory used in bytes +# TYPE n8n_nodejs_heap_used_bytes gauge +n8n_nodejs_heap_used_bytes 51200000 + +# HELP n8n_workflow_executions_total Total number of workflow executions +# TYPE n8n_workflow_executions_total counter +n8n_workflow_executions_total{status="success",workflow_id="wf_8a3b2c1d"} 45 +n8n_workflow_executions_total{status="success",workflow_id="wf_7f4e9a2b"} 38 +n8n_workflow_executions_total{status="success",workflow_id="wf_5d6c8e1f"} 45 +n8n_workflow_executions_total{status="error",workflow_id="wf_8a3b2c1d"} 3 +n8n_workflow_executions_total{status="error",workflow_id="wf_5d6c8e1f"} 4 + +# HELP n8n_workflow_executions_duration_seconds Workflow execution duration in seconds +# TYPE n8n_workflow_executions_duration_seconds histogram +n8n_workflow_executions_duration_seconds_bucket{le="0.1",workflow_id="wf_8a3b2c1d"} 5 +n8n_workflow_executions_duration_seconds_bucket{le="1",workflow_id="wf_8a3b2c1d"} 18 +n8n_workflow_executions_duration_seconds_bucket{le="+Inf",workflow_id="wf_8a3b2c1d"} 48 +n8n_workflow_executions_duration_seconds_sum{workflow_id="wf_8a3b2c1d"} 14.3 +n8n_workflow_executions_duration_seconds_count{workflow_id="wf_8a3b2c1d"} 48 +n8n_workflow_executions_duration_seconds_bucket{le="0.1",workflow_id="wf_7f4e9a2b"} 4 +n8n_workflow_executions_duration_seconds_bucket{le="1",workflow_id="wf_7f4e9a2b"} 15 +n8n_workflow_executions_duration_seconds_bucket{le="+Inf",workflow_id="wf_7f4e9a2b"} 38 +n8n_workflow_executions_duration_seconds_sum{workflow_id="wf_7f4e9a2b"} 11.2 +n8n_workflow_executions_duration_seconds_count{workflow_id="wf_7f4e9a2b"} 38 +n8n_workflow_executions_duration_seconds_bucket{le="0.1",workflow_id="wf_5d6c8e1f"} 3 +n8n_workflow_executions_duration_seconds_bucket{le="1",workflow_id="wf_5d6c8e1f"} 12 +n8n_workflow_executions_duration_seconds_bucket{le="+Inf",workflow_id="wf_5d6c8e1f"} 49 +n8n_workflow_executions_duration_seconds_sum{workflow_id="wf_5d6c8e1f"} 12.7 +n8n_workflow_executions_duration_seconds_count{workflow_id="wf_5d6c8e1f"} 49 + +# HELP n8n_workflow_started_total Total number of workflows started +# TYPE n8n_workflow_started_total counter +n8n_workflow_started_total 25634 +n8n_workflow_started_total{workflow_id="12",workflow_name="CRM Sync"} 8142 +n8n_workflow_started_total{workflow_id="25",workflow_name="Webhook Intake"} 14290 +n8n_workflow_started_total{workflow_id="33",workflow_name="Slack Alerts"} 2202 + +# HELP n8n_workflow_success_total Total number of workflows completed successfully +# TYPE n8n_workflow_success_total counter +n8n_workflow_success_total 25209 +n8n_workflow_success_total{workflow_id="12",workflow_name="CRM Sync"} 8059 +n8n_workflow_success_total{workflow_id="25",workflow_name="Webhook Intake"} 14135 +n8n_workflow_success_total{workflow_id="33",workflow_name="Slack Alerts"} 2015 + +# HELP n8n_workflow_failed_total Total number of workflows that failed +# TYPE n8n_workflow_failed_total counter +n8n_workflow_failed_total 425 +n8n_workflow_failed_total{workflow_id="12",workflow_name="CRM Sync"} 83 +n8n_workflow_failed_total{workflow_id="25",workflow_name="Webhook Intake"} 155 +n8n_workflow_failed_total{workflow_id="33",workflow_name="Slack Alerts"} 187 + + +# HELP n8n_queue_jobs_total Total number of queue jobs +# TYPE n8n_queue_jobs_total counter +n8n_queue_jobs_total{state="waiting"} 3 +n8n_queue_jobs_total{state="active"} 2 +n8n_queue_jobs_total{state="completed"} 148 +n8n_queue_jobs_total{state="failed"} 5 + +# HELP n8n_queue_jobs_duration_seconds Job duration in seconds +# TYPE n8n_queue_jobs_duration_seconds histogram +n8n_queue_jobs_duration_seconds_bucket{le="0.1"} 22 +n8n_queue_jobs_duration_seconds_bucket{le="1"} 84 +n8n_queue_jobs_duration_seconds_bucket{le="+Inf"} 150 +n8n_queue_jobs_duration_seconds_sum 44.8 +n8n_queue_jobs_duration_seconds_count 150 + +# HELP n8n_queue_job_waiting_total Number of jobs currently waiting in the queue +# TYPE n8n_queue_job_waiting_total gauge +n8n_queue_job_waiting_total{queue="default"} 3 + +# HELP n8n_queue_job_active_total Number of jobs currently being processed +# TYPE n8n_queue_job_active_total gauge +n8n_queue_job_active_total{queue="default"} 2 + +# HELP n8n_queue_job_completed_total Number of jobs completed successfully +# TYPE n8n_queue_job_completed_total counter +n8n_queue_job_completed_total{queue="default"} 15892 + +# HELP n8n_queue_job_failed_total Number of jobs that have failed +# TYPE n8n_queue_job_failed_total counter +n8n_queue_job_failed_total{queue="default"} 47 + +# HELP n8n_queue_job_dequeued_total Number of jobs dequeued (picked up from queue) +# TYPE n8n_queue_job_dequeued_total counter +n8n_queue_job_dequeued_total{queue="default"} 15939 + +# HELP n8n_queue_job_enqueued_total Number of jobs added to the queue +# TYPE n8n_queue_job_enqueued_total counter +n8n_queue_job_enqueued_total{queue="default"} 15670 + +# HELP n8n_queue_job_delayed_total Number of jobs scheduled to run later +# TYPE n8n_queue_job_delayed_total gauge +n8n_queue_job_delayed_total{queue="default"} 5 + +# HELP n8n_queue_job_waiting_duration_seconds Duration jobs spend waiting before being processed +# TYPE n8n_queue_job_waiting_duration_seconds histogram +n8n_queue_job_waiting_duration_seconds_bucket{queue="default",le="0.1"} 50 +n8n_queue_job_waiting_duration_seconds_bucket{queue="default",le="1"} 241 +n8n_queue_job_waiting_duration_seconds_bucket{queue="default",le="5"} 820 +n8n_queue_job_waiting_duration_seconds_bucket{queue="default",le="10"} 1105 +n8n_queue_job_waiting_duration_seconds_bucket{queue="default",le="30"} 1240 +n8n_queue_job_waiting_duration_seconds_bucket{queue="default",le="+Inf"} 1253 +n8n_queue_job_waiting_duration_seconds_sum{queue="default"} 450.32 +n8n_queue_job_waiting_duration_seconds_count{queue="default"} 1253 + +# HELP n8n_api_requests_total Total API requests +# TYPE n8n_api_requests_total counter +n8n_api_requests_total{method="GET",endpoint="/workflows"} 240 +n8n_api_requests_total{method="POST",endpoint="/executions"} 75 + +# HELP n8n_api_request_duration_seconds API request duration in seconds +# TYPE n8n_api_request_duration_seconds histogram +n8n_api_request_duration_seconds_bucket{le="0.1"} 90 +n8n_api_request_duration_seconds_bucket{le="1"} 120 +n8n_api_request_duration_seconds_bucket{le="+Inf"} 125 +n8n_api_request_duration_seconds_sum 15.3 +n8n_api_request_duration_seconds_count 125 + +# HELP n8n_cache_operations_total Total cache operations +# TYPE n8n_cache_operations_total counter +n8n_cache_operations_total{operation="get"} 1250 +n8n_cache_operations_total{operation="set"} 320 +n8n_cache_operations_total{operation="delete"} 10 + +# HELP n8n_cache_hits_total Cache hits +# TYPE n8n_cache_hits_total counter +n8n_cache_hits_total 1080 + +# HELP n8n_cache_misses_total Cache misses +# TYPE n8n_cache_misses_total counter +n8n_cache_misses_total 170 + +# HELP n8n_cache_errors_total Cache errors +# TYPE n8n_cache_errors_total counter +n8n_cache_errors_total 0 + +# HELP n8n_cache_latency_seconds Cache operation latency in seconds +# TYPE n8n_cache_latency_seconds histogram +n8n_cache_latency_seconds_bucket{le="0.001"} 90 +n8n_cache_latency_seconds_bucket{le="0.01"} 240 +n8n_cache_latency_seconds_bucket{le="+Inf"} 260 +n8n_cache_latency_seconds_sum 1.42 +n8n_cache_latency_seconds_count 260 + +# HELP n8n_eventbus_events_total Total events published on the event bus +# TYPE n8n_eventbus_events_total counter +n8n_eventbus_events_total{event_type="workflowStarted"} 140 +n8n_eventbus_events_total{event_type="workflowCompleted"} 135 +n8n_eventbus_events_total{event_type="workflowFailed"} 5 + +# HELP n8n_eventbus_events_processed_total Total processed events +# TYPE n8n_eventbus_events_processed_total counter +n8n_eventbus_events_processed_total 138 + +# HELP n8n_eventbus_events_failed_total Total failed event processing +# TYPE n8n_eventbus_events_failed_total counter +n8n_eventbus_events_failed_total 2 + +# HELP n8n_eventbus_queue_size Current event queue size +# TYPE n8n_eventbus_queue_size gauge +n8n_eventbus_queue_size 1 + +# HELP n8n_eventbus_connections_total Active event bus backend connections +# TYPE n8n_eventbus_connections_total gauge +n8n_eventbus_connections_total 1 + +# HELP n8n_workflow_executions_active Number of active workflow executions +# TYPE n8n_workflow_executions_active gauge +n8n_workflow_executions_active 3 + +# HELP n8n_queue_job_attempts_total Total number of job attempts +# TYPE n8n_queue_job_attempts_total counter +n8n_queue_job_attempts_total{result="success"} 435 +n8n_queue_job_attempts_total{result="failed"} 12 + +# HELP n8n_workflow_started_total Total number of workflows started +# TYPE n8n_workflow_started_total counter +n8n_workflow_started_total 25634 +n8n_workflow_started_total{workflow_id="12",workflow_name="CRM Sync"} 8142 +n8n_workflow_started_total{workflow_id="25",workflow_name="Webhook Intake"} 14290 +n8n_workflow_started_total{workflow_id="33",workflow_name="Slack Alerts"} 2202 + +# HELP n8n_workflow_success_total Total number of workflows completed successfully +# TYPE n8n_workflow_success_total counter +n8n_workflow_success_total 25209 +n8n_workflow_success_total{workflow_id="12",workflow_name="CRM Sync"} 8059 +n8n_workflow_success_total{workflow_id="25",workflow_name="Webhook Intake"} 14135 +n8n_workflow_success_total{workflow_id="33",workflow_name="Slack Alerts"} 2015 + +# HELP n8n_workflow_failed_total Total number of workflows that failed +# TYPE n8n_workflow_failed_total counter +n8n_workflow_failed_total 425 +n8n_workflow_failed_total{workflow_id="12",workflow_name="CRM Sync"} 83 +n8n_workflow_failed_total{workflow_id="25",workflow_name="Webhook Intake"} 155 +n8n_workflow_failed_total{workflow_id="33",workflow_name="Slack Alerts"} 187 \ No newline at end of file diff --git a/n8n/tests/test_e2e.py b/n8n/tests/test_e2e.py new file mode 100644 index 0000000000000..e23178d80b702 --- /dev/null +++ b/n8n/tests/test_e2e.py @@ -0,0 +1,14 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +import pytest + +from datadog_checks.base.constants import ServiceCheck +from datadog_checks.dev.utils import assert_service_checks + + +@pytest.mark.e2e +def test_check_n8n_e2e(dd_agent_check, instance): + aggregator = dd_agent_check(instance, rate=True) + aggregator.assert_service_check('n8n.openmetrics.health', ServiceCheck.OK, count=2) + assert_service_checks(aggregator) \ No newline at end of file diff --git a/n8n/tests/test_unit.py b/n8n/tests/test_unit.py new file mode 100644 index 0000000000000..c3de0ee3e05d2 --- /dev/null +++ b/n8n/tests/test_unit.py @@ -0,0 +1,49 @@ +# (C) Datadog, Inc. 2025-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.n8n import N8nCheck + +from . import common + + +def test_check_namespace_default(): + """ + Test that the check applies the correct namespace when raw_metric_prefix is 'n8n' (default). + """ + instance = { + 'openmetrics_endpoint': 'http://localhost:5678/metrics', + } + check = N8nCheck('n8n', {}, [instance]) + config = check.get_default_config() + + # When raw_metric_prefix is 'n8n' (default), namespace should be 'n8n' + assert config['namespace'] == 'n8n', f"Expected namespace 'n8n', got '{config['namespace']}'" + + +def test_check_namespace_custom(): + """ + Test that the check applies the correct namespace when raw_metric_prefix is custom. + """ + instance = { + 'openmetrics_endpoint': 'http://localhost:5678/metrics', + 'raw_metric_prefix': 'my_n8n_team', + } + check = N8nCheck('n8n', {}, [instance]) + config = check.get_default_config() + + # When raw_metric_prefix is custom, namespace should be 'n8n.' + assert config['namespace'] == 'n8n.my_n8n_team', ( + f"Expected namespace 'n8n.my_n8n_team', got '{config['namespace']}'" + ) + + +def test_unit_metrics(dd_agent_check, instance, aggregator, mock_http_response): + mock_http_response(file_path=common.get_fixture_path('n8n.txt')) + check = N8nCheck('n8n', {}, [instance], rate=True) + dd_agent_check(check) + aggregator = dd_agent_check(instance, rate=True) + + for metric in common.TEST_METRICS: + aggregator.assert_metric(metric) + aggregator.assert_all_metrics_covered()