@@ -311,14 +311,91 @@ def acp(
311311 debug : bool = typer .Option (False , "--debug" , "-d" , help = _ ("Enable debug logging" )),
312312) -> None :
313313 """Run iac-code as an ACP server."""
314- if transport == "http" :
315- from iac_code .acp import acp_main_http
314+ import atexit
315+ import signal as _signal_mod
316+ import time
317+
318+ from iac_code .services .telemetry import add_metric , bootstrap_telemetry , graceful_shutdown , log_event
319+ from iac_code .services .telemetry .names import Events , Metrics
320+
321+ telemetry_session_id = f"acp-server-{ uuid .uuid4 ()} "
322+ bootstrap_telemetry (session_id = telemetry_session_id )
323+ log_event (
324+ Events .SESSION_STARTED ,
325+ {
326+ "mode" : "acp-server" ,
327+ "transport" : transport ,
328+ },
329+ )
330+ add_metric (Metrics .SESSION_COUNT , 1 , {})
316331
317- acp_main_http (host = host , port = port , debug = debug )
318- else :
319- from iac_code .acp import acp_main
332+ started = time .monotonic ()
333+ exit_reason = "normal"
334+ _finalized = [False ]
335+
336+ def _finalize_telemetry (reason_override : str | None = None ) -> None :
337+ if _finalized [0 ]:
338+ return
339+ _finalized [0 ] = True
340+ final_reason = reason_override or exit_reason
341+ try :
342+ log_event (
343+ Events .SESSION_EXITED ,
344+ {
345+ "mode" : "acp-server" ,
346+ "reason" : final_reason ,
347+ "duration_s" : int (time .monotonic () - started ),
348+ },
349+ )
350+ finally :
351+ graceful_shutdown ()
352+
353+ atexit .register (_finalize_telemetry , "atexit" )
354+
355+ def _telemetry_excepthook (exc_type , exc_value , traceback_obj ):
356+ try :
357+ log_event (
358+ Events .EXCEPTION_UNCAUGHT ,
359+ {
360+ "error_name" : exc_type .__name__ ,
361+ "location" : "acp" ,
362+ },
363+ )
364+ _finalize_telemetry (f"exception:{ exc_type .__name__ } " )
365+ finally :
366+ sys .__excepthook__ (exc_type , exc_value , traceback_obj )
367+
368+ sys .excepthook = _telemetry_excepthook
369+
370+ _prev_sigterm = _signal_mod .getsignal (_signal_mod .SIGTERM )
371+ _prev_sigint = _signal_mod .getsignal (_signal_mod .SIGINT )
372+
373+ def _telemetry_signal_handler (signum , frame ):
374+ _finalize_telemetry (f"signal:{ signum } " )
375+ prev = _prev_sigterm if signum == _signal_mod .SIGTERM else _prev_sigint
376+ if callable (prev ):
377+ prev (signum , frame ) # ty: ignore[call-top-callable]
378+ return
379+ _signal_mod .signal (signum , _signal_mod .SIG_DFL )
380+ os .kill (os .getpid (), signum )
381+
382+ _signal_mod .signal (_signal_mod .SIGTERM , _telemetry_signal_handler )
383+ _signal_mod .signal (_signal_mod .SIGINT , _telemetry_signal_handler )
384+
385+ try :
386+ if transport == "http" :
387+ from iac_code .acp import acp_main_http
320388
321- acp_main (debug = debug )
389+ acp_main_http (host = host , port = port , debug = debug )
390+ else :
391+ from iac_code .acp import acp_main
392+
393+ acp_main (debug = debug )
394+ except Exception :
395+ exit_reason = "error"
396+ raise
397+ finally :
398+ _finalize_telemetry ()
322399
323400
324401def _load_a2a_config (path : str ) -> dict [str , Any ]:
@@ -506,6 +583,86 @@ def a2a(
506583 err = True ,
507584 )
508585 raise typer .Exit (1 ) from exc
586+
587+ import atexit
588+ import signal as _signal_mod
589+ import time
590+
591+ from iac_code .services .telemetry import add_metric , bootstrap_telemetry , graceful_shutdown , log_event
592+ from iac_code .services .telemetry .names import Events , Metrics
593+
594+ telemetry_session_id = f"a2a-server-{ uuid .uuid4 ()} "
595+ bootstrap_telemetry (session_id = telemetry_session_id )
596+ log_event (
597+ Events .SESSION_STARTED ,
598+ {
599+ "mode" : "a2a-server" ,
600+ "transport" : transport ,
601+ },
602+ )
603+ add_metric (Metrics .SESSION_COUNT , 1 , {})
604+
605+ started = time .monotonic ()
606+ exit_reason = "normal"
607+ _finalized = [False ]
608+
609+ def _finalize_telemetry (reason_override : str | None = None ) -> None :
610+ if _finalized [0 ]:
611+ return
612+ _finalized [0 ] = True
613+ final_reason = reason_override or exit_reason
614+ try :
615+ log_event (
616+ Events .SESSION_EXITED ,
617+ {
618+ "mode" : "a2a-server" ,
619+ "reason" : final_reason ,
620+ "duration_s" : int (time .monotonic () - started ),
621+ },
622+ )
623+ finally :
624+ graceful_shutdown ()
625+
626+ # atexit fires on normal interpreter exit, including after uvicorn returns
627+ # from a graceful shutdown — covers the path where the finally below also
628+ # ran (idempotent via the flag).
629+ atexit .register (_finalize_telemetry , "atexit" )
630+
631+ def _telemetry_excepthook (exc_type , exc_value , traceback_obj ):
632+ try :
633+ log_event (
634+ Events .EXCEPTION_UNCAUGHT ,
635+ {
636+ "error_name" : exc_type .__name__ ,
637+ "location" : "a2a" ,
638+ },
639+ )
640+ _finalize_telemetry (f"exception:{ exc_type .__name__ } " )
641+ finally :
642+ sys .__excepthook__ (exc_type , exc_value , traceback_obj )
643+
644+ sys .excepthook = _telemetry_excepthook
645+
646+ # Install our own SIGTERM/SIGINT handlers BEFORE uvicorn does its own. In
647+ # the common path uvicorn replaces these and triggers graceful shutdown
648+ # itself, after which the finally below runs. These handlers only fire if
649+ # a signal arrives before uvicorn installs its own (or if uvicorn never
650+ # got a chance to) — the ARMS sandbox SIGTERM-then-SIGKILL pattern.
651+ _prev_sigterm = _signal_mod .getsignal (_signal_mod .SIGTERM )
652+ _prev_sigint = _signal_mod .getsignal (_signal_mod .SIGINT )
653+
654+ def _telemetry_signal_handler (signum , frame ):
655+ _finalize_telemetry (f"signal:{ signum } " )
656+ prev = _prev_sigterm if signum == _signal_mod .SIGTERM else _prev_sigint
657+ if callable (prev ):
658+ prev (signum , frame ) # ty: ignore[call-top-callable]
659+ return
660+ _signal_mod .signal (signum , _signal_mod .SIG_DFL )
661+ os .kill (os .getpid (), signum )
662+
663+ _signal_mod .signal (_signal_mod .SIGTERM , _telemetry_signal_handler )
664+ _signal_mod .signal (_signal_mod .SIGINT , _telemetry_signal_handler )
665+
509666 try :
510667 if transport == "unix" and not socket_path :
511668 raise RuntimeError ("socket-path is required in --config for --transport unix." )
@@ -547,8 +704,11 @@ def a2a(
547704 auto_approve_permissions = auto_approve_permissions ,
548705 )
549706 except RuntimeError as exc :
707+ exit_reason = "error"
550708 typer .echo (str (exc ), err = True )
551709 raise typer .Exit (1 ) from exc
710+ finally :
711+ _finalize_telemetry ()
552712
553713
554714@a2a_client_app .command (name = "call" , help = _ ("Send a prompt to an A2A JSON-RPC endpoint." ))
0 commit comments