44import requests
55import pandas as pd
66from io import StringIO
7- from typing import Dict , Any , Optional , Tuple
7+ from typing import Dict , Any , Optional , Tuple , List
88from urllib .parse import quote
99from auth import AppDAuthenticator
10+ from utils import Logger
1011
1112
1213class AppDAPIClient :
1314 """Client for AppDynamics REST API calls."""
1415
15- def __init__ (self , authenticator : AppDAuthenticator ):
16+ def __init__ (self , authenticator : AppDAuthenticator , logger : Optional [ Logger ] = None ):
1617 self .authenticator = authenticator
18+ self .logger = logger
1719
1820 def _make_request (self , method : str , url : str , ** kwargs ) -> Tuple [Optional [requests .Response ], str ]:
1921 """
@@ -23,15 +25,21 @@ def _make_request(self, method: str, url: str, **kwargs) -> Tuple[Optional[reque
2325 Tuple of (response, status) where status is 'valid', 'error', or 'empty'
2426 """
2527 if not self .authenticator .ensure_authenticated ():
28+ if self .logger :
29+ self .logger .error ("Authentication not valid; cannot make request" )
2630 return None , "error"
2731
2832 session = self .authenticator .get_session ()
2933 if not session :
3034 return None , "error"
3135
3236 try :
37+ if self .logger :
38+ self .logger .debug (f"HTTP { method } { url } " )
3339 response = session .request (method , url , verify = self .authenticator .verify_ssl , ** kwargs )
3440 response .raise_for_status ()
41+ if self .logger :
42+ self .logger .debug (f"HTTP { response .status_code } OK for { url } " )
3543 return response , "valid"
3644 except requests .exceptions .HTTPError as err :
3745 error_map = {
@@ -42,16 +50,28 @@ def _make_request(self, method: str, url: str, **kwargs) -> Tuple[Optional[reque
4250 500 : "Internal Server Error - Something went wrong on the server." ,
4351 }
4452 error_explanation = error_map .get (err .response .status_code , "Unknown HTTP Error" )
45- print (f"HTTP Error: { err .response .status_code } - { error_explanation } " )
53+ if self .logger :
54+ self .logger .error (f"HTTP Error { err .response .status_code } : { error_explanation } " )
55+ else :
56+ print (f"HTTP Error: { err .response .status_code } - { error_explanation } " )
4657 return None , "error"
4758 except requests .exceptions .RequestException as err :
4859 if isinstance (err , requests .exceptions .ConnectionError ):
49- print ("Connection Error: Failed to establish a new connection." )
60+ if self .logger :
61+ self .logger .error ("Connection Error: Failed to establish a new connection." )
62+ else :
63+ print ("Connection Error: Failed to establish a new connection." )
5064 else :
51- print (f"Request Exception: { err } " )
65+ if self .logger :
66+ self .logger .error (f"Request Exception: { err } " )
67+ else :
68+ print (f"Request Exception: { err } " )
5269 return None , "error"
5370 except Exception as err :
54- print (f"Unexpected Error: { type (err ).__name__ } : { err } " )
71+ if self .logger :
72+ self .logger .error (f"Unexpected Error: { type (err ).__name__ } : { err } " )
73+ else :
74+ print (f"Unexpected Error: { type (err ).__name__ } : { err } " )
5575 return None , "error"
5676
5777 def _urlencode_string (self , text : str ) -> str :
@@ -161,6 +181,156 @@ def get_apm_availability(self, object_type: str, app_name: str, tier_name: str,
161181
162182 return self ._make_request ("GET" , url )
163183
184+ def get_health_rule_violations (
185+ self ,
186+ application_id : str ,
187+ time_range_type : str = "BEFORE_NOW" ,
188+ duration_mins : Optional [int ] = None ,
189+ start_time : Optional [int ] = None ,
190+ end_time : Optional [int ] = None ,
191+ output : str = "XML" ,
192+ ) -> Tuple [Optional [requests .Response ], str ]:
193+ """Get health rule violations for a specific application.
194+
195+ Docs: https://docs.appdynamics.com/appd/24.x/latest/en/extend-splunk-appdynamics/splunk-appdynamics-apis/alert-and-respond-api/events-api
196+ Endpoint: /controller/rest/applications/{application_id}/problems/healthrule-violations
197+ """
198+ base = f"{ self .authenticator .credentials .base_url } /controller/rest/applications/{ application_id } /problems/healthrule-violations"
199+
200+ params = {"time-range-type" : time_range_type }
201+ if duration_mins is not None :
202+ params ["duration-in-mins" ] = str (duration_mins )
203+ if start_time is not None :
204+ params ["start-time" ] = str (start_time )
205+ if end_time is not None :
206+ params ["end-time" ] = str (end_time )
207+ if output :
208+ params ["output" ] = output
209+
210+ # Build URL with query params
211+ query = "&" .join ([f"{ k } ={ v } " for k , v in params .items ()])
212+ url = f"{ base } ?{ query } "
213+ return self ._make_request ("GET" , url )
214+
215+ def get_events (
216+ self ,
217+ application_id : str ,
218+ event_types : List [str ],
219+ severities : List [str ],
220+ time_range_type : str = "BEFORE_NOW" ,
221+ duration_mins : Optional [int ] = None ,
222+ start_time : Optional [int ] = None ,
223+ end_time : Optional [int ] = None ,
224+ tier : Optional [str ] = None ,
225+ output : str = "XML" ,
226+ ) -> Tuple [Optional [requests .Response ], str ]:
227+ """Get events for a specific application.
228+
229+ Endpoint: /controller/rest/applications/{application_id}/events
230+ Notes: Returns at most 600 events per request.
231+ """
232+ base = f"{ self .authenticator .credentials .base_url } /controller/rest/applications/{ application_id } /events"
233+
234+ params : Dict [str , Any ] = {
235+ "time-range-type" : time_range_type ,
236+ "event-types" : "," .join (event_types ) if event_types else "" ,
237+ "severities" : "," .join (severities ) if severities else "" ,
238+ }
239+ if duration_mins is not None :
240+ params ["duration-in-mins" ] = str (duration_mins )
241+ if start_time is not None :
242+ params ["start-time" ] = str (start_time )
243+ if end_time is not None :
244+ params ["end-time" ] = str (end_time )
245+ if tier :
246+ params ["tier" ] = self ._urlencode_string (tier )
247+ if output :
248+ params ["output" ] = output
249+
250+ query = "&" .join ([f"{ k } ={ v } " for k , v in params .items () if v is not None and v != "" ])
251+ url = f"{ base } ?{ query } "
252+ return self ._make_request ("GET" , url )
253+
254+ def get_events_paginated (
255+ self ,
256+ application_id : str ,
257+ event_types : List [str ],
258+ severities : List [str ],
259+ duration_mins : int ,
260+ tier : Optional [str ] = None ,
261+ ) -> List [Dict [str , Any ]]:
262+ """Retrieve all matching events using sliding windows to respect the 600-per-request cap.
263+
264+ Strategy:
265+ - Use BEFORE_NOW with duration windows; if a window returns 600 rows, bisect the window to reduce size
266+ - Continue until windows produce < 600, then slide backward until total duration is covered
267+ - Parse XML to DataFrame via pandas.read_xml at the call-site; here we just fetch text
268+ """
269+ import time as _time
270+ import math as _math
271+ from io import StringIO as _StringIO
272+ import pandas as _pd
273+
274+ all_events_df = _pd .DataFrame ()
275+
276+ # Start with the full duration; adaptively split if needed
277+ remaining = duration_mins
278+ while remaining > 0 :
279+ window = min (remaining , 240 ) # start with 4h windows to balance calls/results
280+ response , status = self .get_events (
281+ application_id = application_id ,
282+ event_types = event_types ,
283+ severities = severities ,
284+ time_range_type = "BEFORE_NOW" ,
285+ duration_mins = window ,
286+ tier = tier ,
287+ output = "XML" ,
288+ )
289+ if status != "valid" or not response :
290+ break
291+
292+ # Parse quickly to count rows
293+ try :
294+ xml_content = _StringIO (response .content .decode ())
295+ df = _pd .read_xml (xml_content , xpath = ".//event" )
296+ except Exception :
297+ df = _pd .DataFrame ()
298+
299+ if df is None or df .empty :
300+ remaining -= window
301+ continue
302+
303+ if len (df ) >= 600 and window > 5 :
304+ # Too many results; reduce window size and retry without consuming remaining time
305+ new_window = max (5 , window // 2 )
306+ if new_window == window :
307+ new_window = 5
308+ # small delay to avoid rate-limits
309+ _time .sleep (0.2 )
310+ # retry with smaller window
311+ response , status = self .get_events (
312+ application_id = application_id ,
313+ event_types = event_types ,
314+ severities = severities ,
315+ time_range_type = "BEFORE_NOW" ,
316+ duration_mins = new_window ,
317+ tier = tier ,
318+ output = "XML" ,
319+ )
320+ try :
321+ xml_content = _StringIO (response .content .decode ())
322+ df = _pd .read_xml (xml_content , xpath = ".//event" )
323+ except Exception :
324+ df = _pd .DataFrame ()
325+ window = new_window
326+
327+ all_events_df = _pd .concat ([all_events_df , df ])
328+ remaining -= window
329+ _time .sleep (0.2 )
330+
331+ # Return as list of dicts for caller flexibility
332+ return all_events_df .to_dict (orient = "records" )
333+
164334
165335
166336
0 commit comments