Skip to content

Commit 4a5c26c

Browse files
authored
Submit general resource via checks base (DataDog#23905)
* submit general resource via checks base * wording * changelog * lint * small nits * rework genresource submission * Rename 23818.added to 23905.added * rename * include INCLUDE_ALL * address some comments
1 parent 57d05ec commit 4a5c26c

10 files changed

Lines changed: 793 additions & 4 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add ``AgentCheck.submit_generic_resource`` to submit resource snapshots on the ``genresources`` event-platform track with allow-list field selection.

datadog_checks_base/datadog_checks/base/checks/base.py

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -798,18 +798,170 @@ def database_monitoring_metadata(self, raw_event):
798798
aggregator.submit_event_platform_event(self, self.check_id, to_native_string(raw_event), "dbm-metadata")
799799

800800
def event_platform_event(self, raw_event, event_track_type):
801-
# type: (str, str) -> None
801+
# type: (str | bytes, str) -> None
802802
"""Send an event platform event.
803803
804804
Parameters:
805-
raw_event (str):
806-
JSON formatted string representing the event to send
805+
raw_event (str | bytes):
806+
JSON formatted string representing the event to send, or
807+
pre-encoded bytes for proto tracks such as ``genresources``
807808
event_track_type (str):
808809
type of event ingested and processed by the event platform
809810
"""
810811
if raw_event is None:
811812
return
812-
aggregator.submit_event_platform_event(self, self.check_id, to_native_string(raw_event), event_track_type)
813+
if isinstance(raw_event, (bytearray, memoryview)):
814+
raw_event = bytes(raw_event)
815+
elif not isinstance(raw_event, bytes):
816+
raw_event = to_native_string(raw_event)
817+
aggregator.submit_event_platform_event(self, self.check_id, raw_event, event_track_type)
818+
819+
def submit_generic_resource(self, *, type, key, fields, include, seen_at=None, expire_at=None):
820+
# type: (str, str, dict | None, dict, int | None, int | None) -> None
821+
"""Ship a resource on the ``genresources`` event-platform track.
822+
823+
``fields`` is the resource body. ``include`` chooses what to keep from it:
824+
``{"paths": [...], "map_paths": [...], "annotation_keys": [...]}``. Evaluated against ``fields``,
825+
``paths`` select individual values, ``map_paths`` select whole flat maps (e.g.
826+
``metadata.labels``), and ``annotation_keys`` glob ``metadata.annotations`` keys. A path that
827+
resolves to a structured object is dropped. Pass ``include=INCLUDE_ALL`` to ship ``fields``
828+
as-is — only safe when your code constructed every value, never for a raw upstream object.
829+
``seen_at`` / ``expire_at`` are optional ``int`` unix-seconds.
830+
"""
831+
if fields is None:
832+
return
833+
834+
# stdlib json on purpose: module-level json is the orjson wrapper, which coerces datetime instead of failing.
835+
import json as _json
836+
837+
# Lazy import: avoids loading the protobuf runtime for every check that imports base.py.
838+
from datadog_checks.base.utils.genresources import (
839+
GENRESOURCES_TRACK,
840+
INCLUDE_ALL,
841+
INTEGRATIONS_CORE_SOURCE,
842+
MAX_FIELDS_JSON_BYTES,
843+
GenericResource,
844+
GenericResourceEvent,
845+
apply_allow_list,
846+
find_invalid_include,
847+
)
848+
849+
integration = self.name
850+
851+
def _emit_dropped(count=1):
852+
datadog_agent.emit_agent_telemetry(integration, "datadog.agent.check.genresources.dropped", count, "count")
853+
854+
if not key:
855+
self.log.warning("genresources: dropping resource with empty key for type=%s", type)
856+
_emit_dropped()
857+
return
858+
859+
if not type:
860+
self.log.warning("genresources: dropping resource with empty type for key=%s", key)
861+
_emit_dropped()
862+
return
863+
864+
if not isinstance(fields, dict):
865+
self.log.warning(
866+
"genresources: dropping resource with non-dict fields type=%s key=%s actual_type=%s",
867+
type,
868+
key,
869+
fields.__class__.__name__,
870+
)
871+
_emit_dropped()
872+
return
873+
874+
if include is INCLUDE_ALL:
875+
# Caller built `fields` in code and owns its contents; ship as-is, no allow-list.
876+
included = fields
877+
else:
878+
if not isinstance(include, dict):
879+
self.log.warning(
880+
"genresources: dropping resource with non-dict include type=%s key=%s actual_type=%s",
881+
type,
882+
key,
883+
include.__class__.__name__,
884+
)
885+
_emit_dropped()
886+
return
887+
888+
paths = include.get("paths", [])
889+
map_paths = include.get("map_paths", [])
890+
annotation_keys = include.get("annotation_keys", [])
891+
892+
def _is_str_list(value):
893+
return isinstance(value, list) and all(isinstance(item, str) for item in value)
894+
895+
if not (_is_str_list(paths) and _is_str_list(map_paths) and _is_str_list(annotation_keys)):
896+
self.log.warning("genresources: dropping resource with malformed include type=%s key=%s", type, key)
897+
_emit_dropped()
898+
return
899+
900+
if any(not pattern.strip("*?") for pattern in annotation_keys):
901+
self.log.warning(
902+
"genresources: dropping resource with catch-all annotation pattern type=%s key=%s", type, key
903+
)
904+
_emit_dropped()
905+
return
906+
907+
invalid = find_invalid_include(fields, paths, map_paths)
908+
if invalid is not None:
909+
offending_path, reason = invalid
910+
self.log.warning(
911+
"genresources: dropping resource (%s) path=%s type=%s key=%s", reason, offending_path, type, key
912+
)
913+
_emit_dropped()
914+
return
915+
916+
included = apply_allow_list(fields, paths=paths, map_paths=map_paths, annotation_keys=annotation_keys)
917+
918+
if not included:
919+
self.log.warning("genresources: dropping resource with empty inclusion type=%s key=%s", type, key)
920+
_emit_dropped()
921+
return
922+
923+
try:
924+
fields_json = _json.dumps(included, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
925+
except (TypeError, ValueError):
926+
self.log.exception("genresources: failed to encode fields for type=%s key=%s", type, key)
927+
_emit_dropped()
928+
return
929+
930+
if len(fields_json) > MAX_FIELDS_JSON_BYTES:
931+
self.log.warning(
932+
"genresources: dropping oversize resource type=%s key=%s size=%d",
933+
type,
934+
key,
935+
len(fields_json),
936+
)
937+
_emit_dropped()
938+
return
939+
940+
resource = GenericResource(type=type, key=key, fields_json=fields_json)
941+
942+
def _set_seconds(ts, value, label):
943+
if value is None:
944+
return
945+
if isinstance(value, int) and not isinstance(value, bool):
946+
ts.seconds = value
947+
else:
948+
self.log.warning(
949+
"genresources: ignoring non-int %s for type=%s key=%s value=%r", label, type, key, value
950+
)
951+
952+
_set_seconds(resource.seen_at, seen_at, "seen_at")
953+
_set_seconds(resource.expire_at, expire_at, "expire_at")
954+
955+
event = GenericResourceEvent(source=INTEGRATIONS_CORE_SOURCE, resource=resource)
956+
try:
957+
payload = event.SerializeToString()
958+
except Exception:
959+
self.log.exception("genresources: failed to serialize type=%s key=%s", type, key)
960+
_emit_dropped()
961+
return
962+
963+
self.event_platform_event(payload, GENRESOURCES_TRACK)
964+
datadog_agent.emit_agent_telemetry(integration, "datadog.agent.check.genresources.emitted", 1, "count")
813965

814966
def should_send_metric(self, metric_name):
815967
return not self._metric_excluded(metric_name) and self._metric_included(metric_name)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# (C) Datadog, Inc. 2026-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
5+
from .inclusion import INCLUDE_ALL, apply_allow_list, find_invalid_include
6+
from .proto.genericresource_pb2 import GenericResource, GenericResourceEvent
7+
8+
GENRESOURCES_TRACK = "genresources"
9+
INTEGRATIONS_CORE_SOURCE = "integrations-core"
10+
MAX_FIELDS_JSON_BYTES = 1_000_000
11+
12+
__all__ = [
13+
"GENRESOURCES_TRACK",
14+
"INCLUDE_ALL",
15+
"INTEGRATIONS_CORE_SOURCE",
16+
"MAX_FIELDS_JSON_BYTES",
17+
"GenericResource",
18+
"GenericResourceEvent",
19+
"apply_allow_list",
20+
"find_invalid_include",
21+
]

0 commit comments

Comments
 (0)