@@ -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 )
0 commit comments