Skip to content

Commit 27b2e60

Browse files
Merge branch 'master' into iboss-v1.0.0
2 parents eaefad4 + 09df43f commit 27b2e60

File tree

27 files changed

+688
-87
lines changed

27 files changed

+688
-87
lines changed

appgate_sdp/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44

55
This check monitors [Appgate SDP][1] through the Datadog Agent.
66

7-
Include a high level overview of what this integration does:
8-
- What does your product do (in 1-2 sentences)?
9-
- What value will customers get from this integration, and why is it valuable to them?
10-
- What specific data will your integration monitor, and what's the value of that data?
7+
- Monitors the health and performance of Appgate SDP appliances, controllers, and gateways by collecting key metrics.
8+
- Provides visibility into resource utilization, active connections, session counts, and license usage to help ensure secure and efficient access management.
9+
- Enables proactive alerting and troubleshooting by tracking critical indicators such as CPU, memory, disk usage, and system events across distributed environments.
1110

1211
## Setup
1312

@@ -20,7 +19,7 @@ No additional installation is needed on your server.
2019

2120
### Configuration
2221

23-
1. Edit the `appgate_sdp.d/conf.yaml` file, in the `conf.d/` folder at the root of your Agent's configuration directory to start collecting your appgate_sdp performance data. See the [sample appgate_sdp.d/conf.yaml][4] for all available configuration options.
22+
1. Edit the `appgate_sdp.d/conf.yaml` file, in the `conf.d/` folder at the root of your Agent's configuration directory to start collecting your Appgate SDP performance data. See the [sample appgate_sdp.d/conf.yaml][4] for all available configuration options.
2423

2524
2. [Restart the Agent][5].
2625

appgate_sdp/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": "2.0.0",
33
"app_uuid": "77acdb8a-4ea8-4294-baa7-d5ccfe698d9f",
44
"app_id": "appgate-sdp",
5-
"display_on_public_website": false,
5+
"display_on_public_website": true,
66
"tile": {
77
"overview": "README.md#Overview",
88
"configuration": "README.md#Setup",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add TagManager class to dbm base utils

datadog_checks_base/datadog_checks/base/utils/db/utils.py

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
import threading
1212
import time
1313
from concurrent.futures.thread import ThreadPoolExecutor
14+
from enum import Enum, auto
1415
from ipaddress import IPv4Address
15-
from typing import Any, Callable, Dict, List, Tuple # noqa: F401
16+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union # noqa: F401
1617

1718
from cachetools import TTLCache
1819

@@ -441,3 +442,128 @@ def tracked_query(check, operation, tags=None):
441442
yield
442443
elapsed_ms = (time.time() - start_time) * 1000
443444
check.histogram("dd.{}.operation.time".format(check.name), elapsed_ms, **stats_kwargs)
445+
446+
447+
class TagType(Enum):
448+
"""Enum for different types of tags"""
449+
450+
KEYLESS = auto()
451+
452+
453+
class TagManager:
454+
"""
455+
Manages tags for a check. Tags are stored as a dictionary of key-value pairs
456+
for key-value tags and as a list of values for keyless tags useful for easy update and deletion.
457+
There's an internal cache of the tag list to avoid generating the list of tag strings
458+
multiple times.
459+
"""
460+
461+
def __init__(self) -> None:
462+
self._tags: Dict[Union[str, TagType], List[str]] = {}
463+
self._cached_tag_list: Optional[tuple[str, ...]] = None
464+
self._keyless: TagType = TagType.KEYLESS
465+
466+
def set_tag(self, key: Optional[str], value: str, replace: bool = False) -> None:
467+
"""
468+
Set a tag with the given key and value.
469+
If key is None or empty, the value is stored as a keyless tag.
470+
Args:
471+
key (str): The tag key, or None/empty for keyless tags
472+
value (str): The tag value
473+
replace (bool): If True, replaces all existing values for this key
474+
If False, appends the value if it doesn't exist
475+
"""
476+
if not key:
477+
key = self._keyless
478+
479+
if replace or key not in self._tags:
480+
self._tags[key] = [value]
481+
# Invalidate the cache since tags have changed
482+
self._cached_tag_list = None
483+
elif value not in self._tags[key]:
484+
self._tags[key].append(value)
485+
# Invalidate the cache since tags have changed
486+
self._cached_tag_list = None
487+
488+
def set_tags_from_list(self, tag_list: List[str], replace: bool = False) -> None:
489+
"""
490+
Set multiple tags from a list of strings.
491+
Strings can be in "key:value" format or just "value" format.
492+
Args:
493+
tag_list (List[str]): List of tags in "key:value" format or just "value"
494+
replace (bool): If True, replaces all existing tags with the new tags list
495+
"""
496+
if replace:
497+
self._tags.clear()
498+
self._cached_tag_list = None
499+
500+
for tag in tag_list:
501+
if ':' in tag:
502+
key, value = tag.split(':', 1)
503+
self.set_tag(key, value)
504+
else:
505+
self.set_tag(None, tag)
506+
507+
def delete_tag(self, key: Optional[str], value: Optional[str] = None) -> bool:
508+
"""
509+
Delete a tag or specific value for a tag.
510+
For keyless tags, use None or empty string as the key.
511+
Args:
512+
key (str): The tag key to delete, or None/empty for keyless tags
513+
value (str, optional): If provided, only deletes this specific value for the key.
514+
If None, deletes all values for the key.
515+
Returns:
516+
bool: True if something was deleted, False otherwise
517+
"""
518+
if not key:
519+
key = self._keyless
520+
521+
if key not in self._tags:
522+
return False
523+
524+
if value is None:
525+
# Delete the entire key
526+
del self._tags[key]
527+
# Invalidate the cache
528+
self._cached_tag_list = None
529+
return True
530+
else:
531+
# Delete specific value if it exists
532+
if value in self._tags[key]:
533+
self._tags[key].remove(value)
534+
# Clean up empty lists
535+
if not self._tags[key]:
536+
del self._tags[key]
537+
# Invalidate the cache
538+
self._cached_tag_list = None
539+
return True
540+
return False
541+
542+
def _generate_tag_strings(self, tags_dict: Dict[Union[str, TagType], List[str]]) -> tuple[str, ...]:
543+
"""
544+
Generate a tuple of tag strings from a tags dictionary.
545+
Args:
546+
tags_dict (Dict[Union[str, TagType], List[str]]): Dictionary of tags to convert to strings
547+
Returns:
548+
tuple[str, ...]: Tuple of tag strings
549+
"""
550+
return tuple(
551+
value if key == self._keyless else f"{key}:{value}" for key, values in tags_dict.items() for value in values
552+
)
553+
554+
def get_tags(self) -> List[str]:
555+
"""
556+
Get a list of tag strings.
557+
For key-value tags, returns "key:value" format.
558+
For keyless tags, returns just the value.
559+
The returned list is always sorted alphabetically.
560+
Returns:
561+
list: Sorted list of tag strings
562+
"""
563+
# Return cached list if available
564+
if self._cached_tag_list is not None:
565+
return list(self._cached_tag_list)
566+
567+
# Generate and cache regular tags
568+
self._cached_tag_list = self._generate_tag_strings(self._tags)
569+
return list(self._cached_tag_list)

datadog_checks_base/tests/base/utils/db/test_util.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
ConstantRateLimiter,
1818
DBMAsyncJob,
1919
RateLimitingTTLCache,
20+
TagManager,
21+
TagType,
2022
default_json_event_encoding,
2123
get_agent_host_tags,
2224
obfuscate_sql_with_metadata,
@@ -388,3 +390,211 @@ def test_tracked_query(aggregator):
388390
aggregator.assert_metric(
389391
"dd.testcheck.operation.time", tags=["test:tag", "operation:test_query"], count=1, value=1000.0
390392
)
393+
394+
395+
class TestTagManager:
396+
def test_init(self):
397+
"""Test initialization of TagManager"""
398+
tag_manager = TagManager()
399+
assert tag_manager._tags == {}
400+
assert tag_manager._cached_tag_list is None
401+
assert tag_manager._keyless == TagType.KEYLESS
402+
403+
@pytest.mark.parametrize(
404+
'key,value,expected_tags',
405+
[
406+
('test_key', 'test_value', {'test_key': ['test_value']}),
407+
(None, 'test_value', {TagType.KEYLESS: ['test_value']}),
408+
],
409+
)
410+
def test_set_tag(self, key, value, expected_tags):
411+
"""Test setting tags with various combinations of key and value"""
412+
tag_manager = TagManager()
413+
tag_manager.set_tag(key, value)
414+
assert tag_manager._tags == expected_tags
415+
assert tag_manager._cached_tag_list is None
416+
417+
def test_set_tag_existing_key_append(self):
418+
"""Test appending a value to an existing key"""
419+
tag_manager = TagManager()
420+
tag_manager.set_tag('test_key', 'value1')
421+
tag_manager.set_tag('test_key', 'value2')
422+
assert tag_manager._tags == {'test_key': ['value1', 'value2']}
423+
assert tag_manager._cached_tag_list is None
424+
425+
def test_set_tag_existing_key_replace(self):
426+
"""Test replacing values for an existing key"""
427+
tag_manager = TagManager()
428+
tag_manager.set_tag('test_key', 'value1')
429+
tag_manager.set_tag('test_key', 'value2', replace=True)
430+
assert tag_manager._tags == {'test_key': ['value2']}
431+
assert tag_manager._cached_tag_list is None
432+
433+
def test_set_tag_duplicate_value(self):
434+
"""Test setting a duplicate value for a key"""
435+
tag_manager = TagManager()
436+
tag_manager.set_tag('test_key', 'test_value')
437+
tag_manager.set_tag('test_key', 'test_value')
438+
assert tag_manager._tags == {'test_key': ['test_value']}
439+
assert tag_manager._cached_tag_list is None
440+
441+
@pytest.mark.parametrize(
442+
'key,values,delete_key,delete_value,expected_tags,description',
443+
[
444+
(
445+
'test_key',
446+
['value1', 'value2'],
447+
'test_key',
448+
'value1',
449+
{'test_key': ['value2']},
450+
'deleting specific value',
451+
),
452+
('test_key', ['value1', 'value2'], 'test_key', None, {}, 'deleting all values for key'),
453+
(
454+
None,
455+
['value1', 'value2'],
456+
None,
457+
'value1',
458+
{TagType.KEYLESS: ['value2']},
459+
'deleting specific keyless value',
460+
),
461+
(None, ['value1', 'value2'], None, None, {}, 'deleting all keyless values'),
462+
],
463+
)
464+
def test_delete_tag(self, key, values, delete_key, delete_value, expected_tags, description):
465+
"""Test various tag deletion scenarios"""
466+
tag_manager = TagManager()
467+
# Set up initial tags
468+
for value in values:
469+
tag_manager.set_tag(key, value)
470+
471+
# Generate initial cache
472+
tag_manager.get_tags()
473+
assert tag_manager._cached_tag_list is not None
474+
475+
# Perform deletion
476+
assert tag_manager.delete_tag(delete_key, delete_value)
477+
478+
# Verify cache is invalidated
479+
assert tag_manager._cached_tag_list is None
480+
481+
# Verify internal state
482+
assert tag_manager._tags == expected_tags
483+
484+
@pytest.mark.parametrize(
485+
'initial_tags,tag_list,replace,expected_tags,description',
486+
[
487+
(
488+
[],
489+
['key1:value1', 'key2:value2', 'value3'],
490+
False,
491+
['key1:value1', 'key2:value2', 'value3'],
492+
'setting new tags',
493+
),
494+
(
495+
['key1:old_value', 'key2:old_value', 'keyless1'],
496+
['key1:new_value', 'key3:new_value', 'keyless2'],
497+
True,
498+
['key1:new_value', 'key3:new_value', 'keyless2'],
499+
'replacing all existing tags with new ones',
500+
),
501+
(
502+
['key1:old_value'],
503+
['key1:new_value'],
504+
False,
505+
['key1:new_value', 'key1:old_value'],
506+
'appending to existing tags',
507+
),
508+
([], ['key1:value1', 'key1:value1'], False, ['key1:value1'], 'setting duplicate values'),
509+
(
510+
[],
511+
['key1:value1', 'value2', 'key2:value3'],
512+
False,
513+
['key1:value1', 'key2:value3', 'value2'],
514+
'setting mixed format tags',
515+
),
516+
],
517+
)
518+
def test_set_tags_from_list(self, initial_tags, tag_list, replace, expected_tags, description):
519+
"""Test various tag list setting scenarios"""
520+
tag_manager = TagManager()
521+
# Set up initial tags if any
522+
for tag in initial_tags:
523+
if ':' in tag:
524+
key, value = tag.split(':', 1)
525+
tag_manager.set_tag(key, value)
526+
else:
527+
tag_manager.set_tag(None, tag)
528+
529+
# Generate cache to ensure it exists
530+
_ = tag_manager.get_tags()
531+
assert tag_manager._cached_tag_list is not None
532+
533+
tag_manager.set_tags_from_list(tag_list, replace=replace)
534+
535+
# Verify cache was invalidated when needed
536+
if replace:
537+
assert tag_manager._cached_tag_list is None
538+
539+
# Verify final state
540+
assert sorted(tag_manager.get_tags()) == sorted(expected_tags)
541+
542+
def test_get_tags_empty(self):
543+
tag_manager = TagManager()
544+
assert tag_manager.get_tags() == []
545+
546+
@pytest.mark.parametrize(
547+
'regular_tags,expected_tags',
548+
[
549+
({'key1': ['value1']}, ['key1:value1']),
550+
({'key1': ['value1', 'value2']}, ['key1:value1', 'key1:value2']),
551+
],
552+
)
553+
def test_get_tags(self, regular_tags, expected_tags):
554+
"""Test getting tags with various combinations of regular tags"""
555+
tag_manager = TagManager()
556+
557+
# Set up regular tags
558+
for key, values in regular_tags.items():
559+
for value in values:
560+
tag_manager.set_tag(key, value)
561+
562+
# Verify tags
563+
assert sorted(tag_manager.get_tags()) == sorted(expected_tags)
564+
565+
def test_cache_management(self):
566+
"""Test that tag caches are properly managed"""
567+
tag_manager = TagManager()
568+
569+
# Set initial tags
570+
tag_manager.set_tag('regular_key', 'regular_value')
571+
572+
# First call should generate cache
573+
_ = tag_manager.get_tags()
574+
assert tag_manager._cached_tag_list is not None
575+
576+
# Modify regular tags
577+
tag_manager.set_tag('regular_key2', 'regular_value2')
578+
assert tag_manager._cached_tag_list is None
579+
580+
# Verify all tags are included
581+
second_result = tag_manager.get_tags()
582+
assert sorted(second_result) == sorted(['regular_key:regular_value', 'regular_key2:regular_value2'])
583+
584+
def test_get_tags_mutation(self):
585+
"""Test that modifying the returned list from get_tags() does not affect the internal cache"""
586+
tag_manager = TagManager()
587+
tag_manager.set_tag('test_key', 'test_value')
588+
589+
# Get the tags list
590+
tags = tag_manager.get_tags()
591+
assert tags == ['test_key:test_value']
592+
593+
# Modify the returned list
594+
tags.append('modified_tag')
595+
tags[0] = 'modified_existing_tag'
596+
597+
# Get tags again - should be unchanged
598+
new_tags = tag_manager.get_tags()
599+
assert new_tags == ['test_key:test_value']
600+
assert new_tags != tags # The lists should be different objects

0 commit comments

Comments
 (0)