3333from datetime import datetime
3434from dateutil import parser
3535from markdownify import markdownify as md
36+ from datadog_api_client import ApiClient , Configuration
37+ from datadog_api_client .v2 .api .metrics_api import MetricsApi
38+ from datadog_api_client .v2 .model .metric_intake_type import MetricIntakeType
39+ from datadog_api_client .v2 .model .metric_payload import MetricPayload
40+ from datadog_api_client .v2 .model .metric_point import MetricPoint
41+ from datadog_api_client .v2 .model .metric_resource import MetricResource
42+ from datadog_api_client .v2 .model .metric_series import MetricSeries
3643import click
3744import json
3845import logging
4148import re
4249import requests
4350import sys
51+ import time
4452
4553try :
4654 import xdg_base_dirs as xdg
5159GUIDE_NAME = "SCaLE 23x"
5260
5361
62+ class StatsTracker :
63+ """Track statistics for additions, updates, and deletions of items."""
64+
65+ def __init__ (self ):
66+ self .stats = {
67+ "tracks" : {"added" : 0 , "updated" : 0 , "deleted" : 0 },
68+ "rooms" : {"added" : 0 , "updated" : 0 , "deleted" : 0 },
69+ "sessions" : {"added" : 0 , "updated" : 0 , "deleted" : 0 },
70+ "map_regions" : {"added" : 0 , "updated" : 0 , "deleted" : 0 },
71+ }
72+
73+ def increment (self , item_type , operation ):
74+ """Increment counter for a given item type and operation."""
75+ if item_type in self .stats and operation in self .stats [item_type ]:
76+ self .stats [item_type ][operation ] += 1
77+
78+ def get_stats (self ):
79+ """Return the current statistics."""
80+ return self .stats
81+
82+ def log_stats (self , logger ):
83+ """Log statistics summary."""
84+ logger .info ("=" * 60 )
85+ logger .info ("SYNC STATISTICS SUMMARY" )
86+ logger .info ("=" * 60 )
87+ for item_type , operations in self .stats .items ():
88+ total = sum (operations .values ())
89+ if total > 0 :
90+ logger .info (
91+ f"{ item_type .upper ()} : "
92+ f"Added={ operations ['added' ]} , "
93+ f"Updated={ operations ['updated' ]} , "
94+ f"Deleted={ operations ['deleted' ]} "
95+ )
96+ logger .info ("=" * 60 )
97+
98+ def send_to_datadog (self , logger , dryrun = False ):
99+ """Send metrics to Datadog."""
100+ if dryrun :
101+ logger .info ("[DRYRUN] Would have sent metrics to Datadog" )
102+ return
103+
104+ dd_api_key = os .getenv ("DD_API_KEY" )
105+ dd_site = os .getenv ("DD_SITE" , "datadoghq.com" )
106+
107+ if not dd_api_key :
108+ logger .warning (
109+ "DD_API_KEY not set. Skipping Datadog metrics submission."
110+ )
111+ return
112+
113+ try :
114+ configuration = Configuration ()
115+ configuration .api_key ["apiKeyAuth" ] = dd_api_key
116+ configuration .server_variables ["site" ] = dd_site
117+
118+ timestamp = int (time .time ())
119+ series = []
120+
121+ for item_type , operations in self .stats .items ():
122+ for operation , count in operations .items ():
123+ metric_name = f"guidebook.sync.{ item_type } .{ operation } "
124+ series .append (
125+ MetricSeries (
126+ metric = metric_name ,
127+ type = MetricIntakeType .COUNT ,
128+ points = [
129+ MetricPoint (
130+ timestamp = timestamp ,
131+ value = float (count ),
132+ )
133+ ],
134+ tags = [f"guide:{ GUIDE_NAME } " ],
135+ )
136+ )
137+
138+ if series :
139+ with ApiClient (configuration ) as api_client :
140+ api_instance = MetricsApi (api_client )
141+ body = MetricPayload (
142+ series = series ,
143+ )
144+ response = api_instance .submit_metrics (body = body )
145+ logger .info ("Successfully sent metrics to Datadog" )
146+ logger .debug (f"Datadog response: { response } " )
147+ else :
148+ logger .debug ("No metrics to send to Datadog" )
149+
150+ except Exception as e :
151+ logger .error (f"Failed to send metrics to Datadog: { e } " )
152+
153+
54154class OurJSON :
55155 rooms = set ()
56156 tracks = set ()
@@ -165,11 +265,21 @@ class GuideBook:
165265
166266 REGIONED_MAP = "Pasadena-Convention-Center-Map-1000-72-fs8"
167267
168- def __init__ (self , logger , update , dryrun , max_deletes , key , x_key = None ):
268+ def __init__ (
269+ self ,
270+ logger ,
271+ update ,
272+ dryrun ,
273+ max_deletes ,
274+ key ,
275+ stats_tracker ,
276+ x_key = None ,
277+ ):
169278 self .logger = logger
170279 self .update = update
171280 self .dryrun = dryrun
172281 self .max_deletes = max_deletes
282+ self .stats = stats_tracker
173283 self .headers = {"Authorization" : "JWT " + key }
174284 self .guide = self .get_guide ()
175285 self .tracks = self .get_things ("tracks" )
@@ -308,6 +418,8 @@ def add_track(self, track, update, tid):
308418 "color" : self .COLOR_MAP [track ].upper (),
309419 }
310420 self .tracks [track ] = self .add_thing ("tracks" , track , data , update , tid )
421+ operation = "updated" if update else "added"
422+ self .stats .increment ("tracks" , operation )
311423
312424 def setup_tracks (self , tracks ):
313425 """
@@ -344,6 +456,8 @@ def add_room(self, room, update, rid):
344456 "location_type" : 2 , # not google maps
345457 }
346458 self .rooms [room ] = self .add_thing ("rooms" , room , data , update , rid )
459+ operation = "updated" if update else "added"
460+ self .stats .increment ("rooms" , operation )
347461
348462 def setup_rooms (self , rooms ):
349463 """
@@ -382,6 +496,8 @@ def add_x_map_region(self, map_region, update, rid, location_id):
382496 "relative_height" : map_region ["h" ],
383497 }
384498 self .add_thing ("x-map-regions" , name , data , update , rid )
499+ operation = "updated" if update else "added"
500+ self .stats .increment ("map_regions" , operation )
385501
386502 def get_x_map_region_for_room (self , location_id ):
387503 return next (
@@ -545,6 +661,8 @@ def add_session(self, session, original_session=None):
545661 s = self .add_thing ("sessions" , name , data , update , sid )
546662 self .sessions_by_nid [session ["nid" ]] = s
547663 self .sessions_by_name [name ] = s
664+ operation = "updated" if update else "added"
665+ self .stats .increment ("sessions" , operation )
548666
549667 def normalize_html (self , html ):
550668 """
@@ -706,6 +824,7 @@ def delete_session(self, session):
706824 self .logger .error ("Failed to delete" )
707825 self .logger .error ("RESPONSE: %s" % response .json ())
708826 sys .exit (1 )
827+ self .stats .increment ("sessions" , "deleted" )
709828
710829 def delete_sessions (self ):
711830 self .logger .warning ("Deleting all sessions" )
@@ -735,6 +854,7 @@ def delete_track(self, track):
735854 self .logger .error ("Failed to delete" )
736855 self .logger .error ("RESPONSE: %s" % response .json ())
737856 sys .exit (1 )
857+ self .stats .increment ("tracks" , "deleted" )
738858
739859 def delete_tracks (self ):
740860 self .logger .warning ("Deleting all tracks" )
@@ -762,6 +882,7 @@ def delete_room(self, room):
762882 self .logger .error ("Failed to delete" )
763883 self .logger .error ("RESPONSE: %s" % response .json ())
764884 sys .exit (1 )
885+ self .stats .increment ("rooms" , "deleted" )
765886
766887 def delete_rooms (self ):
767888 self .logger .warning ("Deleting all rooms" )
@@ -889,6 +1010,7 @@ def main(debug, update, delete_all, feed, dryrun, max_deletes):
8891010 logger .addHandler (ch )
8901011
8911012 key , x_key = get_tokens (logger )
1013+ stats_tracker = StatsTracker ()
8921014
8931015 if delete_all :
8941016 print ("WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" ) # noqa: E999
@@ -898,7 +1020,9 @@ def main(debug, update, delete_all, feed, dryrun, max_deletes):
8981020 else :
8991021 ourdata = OurJSON (feed , logger )
9001022
901- ourguide = GuideBook (logger , update , dryrun , max_deletes , key , x_key = x_key )
1023+ ourguide = GuideBook (
1024+ logger , update , dryrun , max_deletes , key , stats_tracker , x_key = x_key
1025+ )
9021026 if delete_all :
9031027 ourguide .delete_all ()
9041028 else :
@@ -912,6 +1036,10 @@ def main(debug, update, delete_all, feed, dryrun, max_deletes):
9121036 # unclear exactly when this is needed.
9131037 ourguide .publish_updates ()
9141038
1039+ # Log and send statistics
1040+ stats_tracker .log_stats (logger )
1041+ stats_tracker .send_to_datadog (logger , dryrun = dryrun )
1042+
9151043
9161044if __name__ == "__main__" :
9171045 main ()
0 commit comments