Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ coverage:
target: 75
flags:
- bind9
Chrony:
target: 75
flags:
- chrony
CloudNatix:
target: 75
flags:
Expand Down Expand Up @@ -374,6 +378,11 @@ flags:
paths:
- cfssl/datadog_checks/cfssl
- cfssl/tests
chrony:
carryforward: true
paths:
- chrony/datadog_checks/chrony
- chrony/tests
cloudnatix:
carryforward: true
paths:
Expand Down
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ code-coverage.datadog.yml @DataDog/agent-integr
/causely/ @esara support@causely.io @DataDog/ecosystems-review
/census/ @sankalp04 support@getcensus.com @DataDog/ecosystems-review
/cfssl/ @JeanFred
/chrony/ @naoyukiseki
/cloudaeye/ @nazrul-cloudaeye nazrul@cloudaeye.com @bhavyalatha26 bhavya@cloudaeye.com @DataDog/ecosystems-review
/cloudnatix/ @junm-cloudnatix @kenji-cloudnatix @somik-cloudnatix @rohit-cloudnatix
/cloudquery/ @cloudquery/cloudquery-framework
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,25 @@ jobs:
test-py3: ${{ inputs.test-py3 }}
setup-env-vars: "${{ inputs.setup-env-vars }}"
secrets: inherit
j9532002:
uses: DataDog/integrations-core/.github/workflows/test-target.yml@574d63ba88365ffbab915280ceddbaa333c63d6a
with:
job-name: Chrony
target: chrony
platform: linux
runner: '["ubuntu-22.04"]'
repo: "${{ inputs.repo }}"
context: ${{ inputs.context }}
python-version: "${{ inputs.python-version }}"
latest: ${{ inputs.latest }}
agent-image: "${{ inputs.agent-image }}"
agent-image-py2: "${{ inputs.agent-image-py2 }}"
agent-image-windows: "${{ inputs.agent-image-windows }}"
agent-image-windows-py2: "${{ inputs.agent-image-windows-py2 }}"
test-py2: ${{ inputs.test-py2 }}
test-py3: ${{ inputs.test-py3 }}
setup-env-vars: "${{ inputs.setup-env-vars }}"
secrets: inherit
j6a8ad70:
uses: DataDog/integrations-core/.github/workflows/test-target.yml@574d63ba88365ffbab915280ceddbaa333c63d6a
with:
Expand Down
7 changes: 7 additions & 0 deletions chrony/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CHANGELOG - chrony

## 1.0.0 / 2026-03-26

***Added***

* Initial Release
66 changes: 66 additions & 0 deletions chrony/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Agent Check: chrony

## Overview

This check monitors [chrony][1].

The check executes `chronyc tracking` command and parses the output to extract key time synchronization metrics. It's designed to monitor the health and performance of chrony NTP service.

## Setup

### Installation

To install the chrony check for development testing:

1. Install the [developer toolkit][5] on any machine.
2. Run `ddev release build chrony` to build the package.
3. [Download the Datadog Agent][2].
4. Upload the build artifact to a host with an Agent and run:
`datadog-agent integration install -w path/to/chrony/dist/<ARTIFACT_NAME>.whl`.

### Configuration

1. Ensure `chronyc` is installed on the host where the Agent runs (the check executes `chronyc tracking`).
2. Create the Agent configuration file:
- Linux: `/etc/datadog-agent/conf.d/chrony.d/conf.yaml`
- macOS (developer mode): `/opt/datadog-agent/etc/conf.d/chrony.d/conf.yaml`
3. Minimal config example:
```
init_config:

instances:
- {}
```
4. For additional options, see the example config: [conf.yaml.example][4].

### Validation

Run the Agent check to verify it collects metrics:

```
datadog-agent check chrony
```

You should see the service check `chrony.can_connect` report `OK` when the `chronyc` command executes successfully.

## Data Collected

### Metrics

### Service Checks

- **`chrony.can_connect`**: Returns `OK` if chronyc command executes successfully, `CRITICAL` otherwise

### Events

chrony does not include any events.

## Troubleshooting

Need help? Contact [Datadog support][3].

[1]: https://chrony-project.org
[2]: https://app.datadoghq.com/account/settings/agent/latest
[3]: https://docs.datadoghq.com/help/
[4]: https://github.com/DataDog/integrations-extras/blob/master/chrony/datadog_checks/chrony/data/conf.yaml.example
[5]: https://docs.datadoghq.com/developers/integrations/python/
10 changes: 10 additions & 0 deletions chrony/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: chrony
files:
- name: chrony.yaml
options:
- template: init_config
options:
- template: init_config/default
- template: instances
options:
- template: instances/default
11 changes: 11 additions & 0 deletions chrony/assets/service_checks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"agent_version": "6.0.0",
"integration": "Chrony",
"check": "chrony.can_connect",
"statuses": ["ok", "critical"],
"groups": [],
"name": "Chrony can connect",
"description": "Returns CRITICAL if the chronyc command fails to execute, otherwise OK."
}
]
1 change: 1 addition & 0 deletions chrony/datadog_checks/chrony/__about__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '1.0.0'
5 changes: 5 additions & 0 deletions chrony/datadog_checks/chrony/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

from .__about__ import __version__
from .check import ChronyCheck

__all__ = ['__version__', 'ChronyCheck']
164 changes: 164 additions & 0 deletions chrony/datadog_checks/chrony/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/env python3

import re
import subprocess

from datadog_checks.base import AgentCheck


class ChronyCheck(AgentCheck):
def __init__(self, name, init_config, instances):
super(ChronyCheck, self).__init__(name, init_config, instances)

# Field definitions - using only Datadog supported units
# Format: fieldname, regex_pattern, description, unit (None for unitless)
self.fields = [
('stratum', r'^Stratum', 'Stratum', None), # Unitless (level not supported)
('systime', r'^System.time', 'System Time', 'second'),
('frequency', r'^Frequency', 'Frequency', None), # Unitless (ppm not supported)
('residualfreq', r'^Residual.freq', 'Residual Frequency', None), # Unitless (ppm not supported)
('skew', r'^Skew', 'Skew', None), # Unitless (ppm not supported)
('rootdelay', r'^Root.delay', 'Root delay', 'second'),
('rootdispersion', r'^Root.dispersion', 'Root dispersion', 'second'),
]

self.chronyc_path = self._find_chronyc()

def _find_chronyc(self):
"""Find the chronyc executable path"""
try:
result = subprocess.run(['which', 'chronyc'], capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError:
return None

def _get_chrony_tracking(self):
"""Execute chronyc tracking command and return output"""
if not self.chronyc_path:
raise Exception("chronyc executable not found")

try:
result = subprocess.run(
[self.chronyc_path, 'tracking'], capture_output=True, text=True, check=True, timeout=30
)
return result.stdout
except subprocess.CalledProcessError as e:
raise Exception(f"Failed to execute chronyc tracking: {e}")
except subprocess.TimeoutExpired:
raise Exception("chronyc tracking command timed out")

def _parse_chrony_output(self, output):
"""Parse chronyc tracking output and extract metrics"""
metrics = {}

for fieldname, regex_pattern, _description, _unit in self.fields:
# Find the line matching the regex pattern
pattern = re.compile(regex_pattern, re.IGNORECASE)

for line in output.split('\n'):
if pattern.match(line):
# Extract the value part after the colon
parts = line.split(':', 1)
if len(parts) == 2:
value_part = parts[1].strip()

# Parse the numeric value (no scaling factor)
value = self._extract_numeric_value(value_part)
if value is not None:
metrics[fieldname] = value
break

return metrics

def _extract_numeric_value(self, value_part):
"""Extract numeric value from chrony output line"""
# Handle cases like "0.143763348 seconds slow of NTP time"
# or "9.733 ppm slow" or "+0.086 ppm"

# Extract the first number from the string
number_match = re.search(r'[+-]?\d+\.?\d*', value_part)
if not number_match:
return None

try:
value = float(number_match.group())

# Check if the line contains "slow" which indicates negative values
if 'slow' in value_part.lower():
value = -value

# Return the value in its original units
return value

except ValueError:
return None

def _get_reference_info(self, output):
"""Extract reference server information for tagging"""
reference_info = {}

for line in output.split('\n'):
if line.strip().startswith('Reference ID'):
# Extract reference ID and server name
parts = line.split(':', 1)
if len(parts) == 2:
ref_part = parts[1].strip()
# Parse format like "89BE0204 (time1.weber.edu)"
if '(' in ref_part and ')' in ref_part:
ip = ref_part.split('(')[0].strip()
server = ref_part.split('(')[1].split(')')[0].strip()
reference_info['reference_ip'] = ip
reference_info['reference_server'] = server
else:
reference_info['reference_ip'] = ref_part
break

return reference_info

def check(self, instance):
"""Main check method called by Datadog agent"""
try:
# Get chrony tracking output
output = self._get_chrony_tracking()

# Parse metrics
metrics = self._parse_chrony_output(output)

# Get reference server info for tagging
reference_info = self._get_reference_info(output)

# Prepare tags
tags = list(instance.get('tags', [])) if instance else []
service = instance.get('service') if instance else None
if not service:
service = self.init_config.get('service')
if service:
tags.append(f"service:{service}")
if reference_info.get('reference_server'):
tags.append(f"reference_server:{reference_info['reference_server']}")
if reference_info.get('reference_ip'):
tags.append(f"reference_ip:{reference_info['reference_ip']}")

# Submit metrics to Datadog with proper units
for fieldname, value in metrics.items():
metric_name = f"chrony.{fieldname}"

# Find the unit for this metric
unit = None
for field_name, _regex_pattern, _description, field_unit in self.fields:
if field_name == fieldname:
unit = field_unit
break

# Submit metric with unit information
self.gauge(metric_name, value, tags=tags)
self.log.debug("Submitted metric %s: %s %s", metric_name, value, unit or '')

# Submit a service check for chrony availability
self.service_check('chrony.can_connect', AgentCheck.OK, tags=tags)

self.log.info("Successfully collected %d chrony metrics", len(metrics))

except Exception as e:
self.log.error("Error collecting chrony metrics: %s", e)
self.service_check('chrony.can_connect', AgentCheck.CRITICAL, message=str(e))
20 changes: 20 additions & 0 deletions chrony/datadog_checks/chrony/config_models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# This file is autogenerated.
# To change this file you should edit assets/configuration/spec.yaml and then run the following commands:
# ddev -x validate config -s <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>

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
20 changes: 20 additions & 0 deletions chrony/datadog_checks/chrony/config_models/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# This file is autogenerated.
# To change this file you should edit assets/configuration/spec.yaml and then run the following commands:
# ddev -x validate config -s <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>


def instance_disable_generic_tags():
return False


def instance_empty_default_hostname():
return False


def instance_enable_legacy_tags_normalization():
return True


def instance_min_collection_interval():
return 15
Loading
Loading