
    L0&jgM                        d Z ddlZddlZddlZddlZddlZddlmZ ddlm	Z	 ddl
mZmZ ddlmZmZ da ej                    ZdZd	Zd
 ZdZdeddfdZd.dZd.dZ e              G d dej                  ZdddddddZddddddddee	         dee         dee         dee         dee         d ede	fd!Z d.d"Z! G d# d$e          Z"dd%d&ej#        d'e	d(ed)eded*ej$        d+eej                 ddfd,Z%d- Z&dS )/u   Centralized logging setup for Hermes Agent.

Provides a single ``setup_logging()`` entry point that both the CLI and
gateway call early in their startup path.  All log files live under
``~/.hermes/logs/`` (profile-aware via ``get_hermes_home()``).

Log files produced:
    agent.log   — INFO+, all agent/tool/session activity (the main log)
    errors.log  — WARNING+, errors and warnings only (quick triage)
    gateway.log — INFO+, gateway-only events (created when mode="gateway")
    gui.log     — INFO+, dashboard/websocket/TUI-gateway events
                  (created when mode="gui")

All files use ``RotatingFileHandler`` with ``RedactingFormatter`` so
secrets are never written to disk.

Component separation:
    gateway.log only receives records from ``gateway.*`` loggers —
    platform adapters, session management, slash commands, delivery.
    gui.log receives dashboard-side records from ``hermes_cli.web_server``,
    ``hermes_cli.pty_bridge``, ``tui_gateway.*``, and ``uvicorn.*``.
    agent.log remains the catch-all (everything goes there).

Session context:
    Call ``set_session_context(session_id)`` at the start of a conversation
    and ``clear_session_context()`` when done.  All log lines emitted on
    that thread will include ``[session_id]`` for filtering/correlation.
    N)RotatingFileHandler)Path)OptionalSequence)get_config_pathget_hermes_homeFz>%(asctime)s %(levelname)s%(session_tag)s %(name)s: %(message)szC%(asctime)s - %(name)s - %(levelname)s%(session_tag)s - %(message)sc                  *   t           j        } t          | dd          pd}|                                                    dd          dv r| S 	 t          | dd          }|"t          j        |ddd	
          }d |_        |S n# t          $ r Y nw xY w| S )u  Return a stderr stream that tolerates Unicode on all platforms.

    On Windows the console encoding is often a legacy MBCS codec
    (cp949, cp1252, …) that raises ``UnicodeEncodeError`` for characters
    like the em-dash (U+2014).  We wrap ``sys.stderr`` in a
    ``TextIOWrapper`` with ``errors='replace'`` so log lines are never
    lost — un-encodable characters are replaced with ``?`` instead of
    crashing the process.
    encodingNutf-8- )utf8utf8surrogateescapebufferreplaceT)r
   errorsline_bufferingc                      d S N r       3/home/ubuntu/.hermes/hermes-agent/hermes_logging.py<lambda>z_safe_stderr.<locals>.<lambda>R   s    D r   )	sysstderrgetattrlowerr   ioTextIOWrapperclose	Exception)streamr
   bufwrappeds       r   _safe_stderrr%   8   s     ZFvz400;GH~~R((,KKKfh--?&  #	  G )LGMN      Ms   4B 
BB)openaizopenai._base_clienthttpxhttpcoreasynciohpackzhpack.hpackgrpcmodalurllib3zurllib3.connectionpool
websocketscharset_normalizermarkdown_it
session_idreturnc                     | t           _        dS )zSet the session ID for the current thread.

    All subsequent log records on this thread will include ``[session_id]``
    in the formatted output.  Call at the start of ``run_conversation()``.
    N_session_contextr1   )r1   s    r   set_session_contextr6   p   s     #-r   c                      dt           _        dS )z,Clear the session ID for the current thread.Nr4   r   r   r   clear_session_contextr8   y   s    "&r   c                      t          j                    t          dd          rdS fd} d| _        t          j        |            dS )ub  Replace the global LogRecord factory with one that adds ``session_tag``.

    Unlike a ``logging.Filter`` on a handler or logger, the record factory
    runs for EVERY record in the process — including records that propagate
    from child loggers and records handled by third-party handlers.  This
    guarantees ``%(session_tag)s`` is always available in format strings,
    eliminating the KeyError that would occur if a handler used our format
    without having a ``_SessionFilter`` attached.

    Idempotent — checks for a marker attribute to avoid double-wrapping if
    the module is reloaded.
    _hermes_session_injectorFNc                  b     | i |}t          t          dd           }|rd| dnd|_        |S )Nr1   z []r   )r   r5   session_tag)argskwargsrecordsidcurrent_factorys       r   _session_record_factoryz@_install_session_record_factory.<locals>._session_record_factory   sG     $1&11&d;;,/7[#[[[[Rr   T)logginggetLogRecordFactoryr   r:   setLogRecordFactory)rC   rB   s    @r   _install_session_record_factoryrG      si     133O :EBB      8<4 788888r   c                   R     e Zd ZdZdee         ddf fdZdej        de	fdZ
 xZS )_ComponentFilterzOnly pass records whose logger name starts with one of *prefixes*.

    Used to route gateway-specific records to ``gateway.log`` while
    keeping ``agent.log`` as the catch-all.
    prefixesr2   Nc                 p    t                                                       t          |          | _        d S r   )super__init__tuple	_prefixes)selfrJ   	__class__s     r   rM   z_ComponentFilter.__init__   s*    xr   r@   c                 @    |j                             | j                  S r   )name
startswithrO   )rP   r@   s     r   filterz_ComponentFilter.filter   s    {%%dn555r   )__name__
__module____qualname____doc__r   strrM   rD   	LogRecordboolrU   __classcell__rQ   s   @r   rI   rI      s~         )# )4 ) ) ) ) ) )6W. 64 6 6 6 6 6 6 6 6r   rI   )gatewayhermes_plugins)agent	run_agentmodel_toolsbatch_runner)tools)
hermes_clicli)cron)zhermes_cli.web_serverzhermes_cli.pty_bridgetui_gatewayuvicorn)r_   ra   re   rg   rh   gui)hermes_home	log_levelmax_size_mbbackup_countmodeforcerl   rm   rn   ro   rp   rq   c                 *   | pt                      }|dz  }|                    dd           t                      \  }}	}
|p|pd                                }t	          t
          |t
          j                  }|p|	pddz  dz  }|p|
pd}dd	lm} t          j	                    }t          ||d
z  ||| |t                               t          ||dz  t
          j        dd |t                               |dk    rIt          ||dz  t
          j        dd |t                    t          t          d                              |dk    rIt          ||dz  t
          j        dd |t                    t          t          d                              t          r|s|S |j        t
          j        k    s|j        |k    r|                    |           t&          D ]3}t          j	        |                              t
          j                   4da|S )u@  Configure the Hermes logging subsystem.

    Safe to call multiple times — the second call is a no-op unless
    *force* is ``True``.

    Parameters
    ----------
    hermes_home
        Override for the Hermes home directory.  Falls back to
        ``get_hermes_home()`` (profile-aware).
    log_level
        Minimum level for the ``agent.log`` file handler.  Accepts any
        standard Python level name (``"DEBUG"``, ``"INFO"``, ``"WARNING"``).
        Defaults to ``"INFO"`` or the value from config.yaml ``logging.level``.
    max_size_mb
        Maximum size of each log file in megabytes before rotation.
        Defaults to 5 or the value from config.yaml ``logging.max_size_mb``.
    backup_count
        Number of rotated backup files to keep.
        Defaults to 3 or the value from config.yaml ``logging.backup_count``.
    mode
        Caller context: ``"cli"``, ``"gateway"``, ``"gui"``, ``"cron"``.
        When ``"gateway"``, an additional ``gateway.log`` file is created
        that receives only gateway-component records.
        When ``"gui"``, an additional ``gui.log`` file is created that
        receives dashboard and TUI-gateway component records.
    force
        Re-run setup even if it has already been called.

    Returns
    -------
    Path
        The ``logs/`` directory where files are written.
    logsTparentsexist_okINFO   i      r   RedactingFormatterz	agent.log)level	max_bytesro   	formatterz
errors.logi       r_   zgateway.logi  P )r|   r}   ro   r~   
log_filterrk   zgui.logi   )r   mkdir_read_logging_configupperr   rD   rw   agent.redactr{   	getLogger_add_rotating_handler_LOG_FORMATWARNINGrI   COMPONENT_PREFIXES_logging_initializedr|   NOTSETsetLevel_NOISY_LOGGERS)rl   rm   rn   ro   rp   rq   homelog_dir	cfg_levelcfg_max_size
cfg_backup
level_namer|   r}   backupsr{   rootrS   s                     r   setup_loggingr      sm   X +/++DVmGMM$M... +?*@*@'I|Z2y2F99;;JGZ66E11T9D@I-j-AG 0/////D +$$[11    ,o!$$[11    ym#,%((55'(:9(EFF	
 	
 	
 	
 u}}i,&((55'(:5(ABB	
 	
 	
 	
  E  zW^##tzE'9'9e  : :$((9999Nr   c                     ddl m}  t          j                    }|j        D ]E}t          |t          j                  r)t          |t                    st          |dd          r dS Ft          j        t                                }|
                    t          j                   |                     | t          d                     d|_        |                    |           |j        t          j        k    r|
                    t          j                   t"          D ]3}t          j        |          
                    t          j                   4t          j        d	          
                    t          j                   dS )
zEnable DEBUG-level console logging for ``--verbose`` / ``-v`` mode.

    Called by ``AIAgent.__init__()`` when ``verbose_logging=True``.
    r   rz   _hermes_verboseFNz%H:%M:%S)datefmtTz
rex-deploy)r   r{   rD   r   handlers
isinstanceStreamHandlerr   r   r%   r   DEBUGsetFormatter_LOG_FORMAT_VERBOSEr   
addHandlerr|   r   r   rw   )r{   r   hhandlerrS   s        r   setup_verbose_loggingr   B  sf   
 0/////D ]  a.// 	
1FY8Z8Z 	q+U33 #LNN33GW]###++,?TTTUUU"GOOG zGM!!gm$$$  : :$((9999l##,,W\:::::r   c                   j     e Zd ZdZ fdZd ZddZddZdej	        ddf fd	Z
 fd
Z fdZ xZS )_ManagedRotatingFileHandleru  RotatingFileHandler that ensures group-writable perms in managed mode
    AND survives external rotation.

    Two responsibilities:

    1.  In managed mode (NixOS), the stateDir uses setgid (2770) so new files
        inherit the hermes group. However, both ``_open()`` (initial creation)
        and ``doRollover()`` create files via ``open()``, which uses the
        process umask — typically 0022, producing 0644. This subclass applies
        ``chmod 0660`` after both operations so the gateway and interactive
        users can share log files.

    2.  ``RotatingFileHandler`` keeps an open file descriptor.  If anything
        rotates the file *externally* (``logrotate``, manual ``mv``,
        another process rotating under us, a transient unlink), our fd
        keeps pointing at the renamed/unlinked inode and every subsequent
        write goes to ``gateway.log.1`` instead of ``gateway.log`` — silent
        log loss for the file every operator expects to read.  Before each
        emit we ``stat`` ``baseFilename`` and compare it against the open
        stream's inode; on mismatch we reopen.  This is the same pattern
        as stdlib ``WatchedFileHandler.reopenIfNeeded()``, adapted for
        rotating handlers.
    c                     ddl m}  |            | _         t                      j        |i | d | _        d | _        |                                  d S )Nr   )
is_managed)hermes_cli.configr   _managedrL   rM   	_stat_dev	_stat_ino_record_stream_stat)rP   r>   r?   r   rQ   s       r   rM   z$_ManagedRotatingFileHandler.__init__  sf    000000"
$)&))) )-(,  """""r   c                 p    | j         r.	 t          j        | j        d           d S # t          $ r Y d S w xY wd S )Ni  )r   oschmodbaseFilenameOSError)rP   s    r   _chmod_if_managedz-_ManagedRotatingFileHandler._chmod_if_managed  sX    = 	*E22222   	 	s   % 
33r2   Nc                     	 t          j        | j                  }|j        |j        c| _        | _        dS # t          $ r d\  | _        | _        Y dS w xY w)zHSnapshot dev/ino of ``baseFilename`` so we can detect external rotation.)NNN)r   statr   st_devst_inor   r   r   rP   sts     r   r   z/_ManagedRotatingFileHandler._record_stream_stat  s`    	8*++B-/Y	*DNDNNN 	8 	8 	8-7*DNDNNNN	8s   26 AAc                    	 t          j        | j                  }n# t          $ r| 	 | j        | j                                         n# t          $ r Y nw xY wd| _        	 |                                 | _        |                                  n# t          $ r Y nw xY wY dS t          $ r Y dS w xY w| j
        | j        |j        |j        c| _
        | _        dS |j        |j        f| j
        | j        fk    r	 | j        | j                                         n# t          $ r Y nw xY wd| _        	 |                                 | _        |j        |j        c| _
        | _        dS # t          $ r Y dS w xY wdS )aa  Reopen the stream when ``baseFilename`` no longer matches our fd.

        Triggered when ``baseFilename`` was renamed (logrotate), unlinked,
        or replaced by a different inode.  Silent + best-effort: any error
        falls back to the existing (possibly stale) stream so logging keeps
        working instead of dying on a stat failure.
        N)r   r   r   FileNotFoundErrorr"   r    r!   _openr   r   r   r   r   r   r   s     r   _reopen_if_externally_rotatedz9_ManagedRotatingFileHandler._reopen_if_externally_rotated  s   	*++BB  	 	 	;*K%%'''   DK"jjll((****     FF 	 	 	FF	 >!T^%;-/Y	*DNDNFIry!dndn%EEE;*K%%'''   DK"jjll13BI.    FEs    
B. AB.
AB.A
B. -BB.
BB.BB.!	B.-B.9 D 
D'&D'22E& &
E43E4r@   c                     | j         $t          j                            | j                  r|                                  t                                          |           d S r   )r"   r   pathexistsr   r   rL   emit)rP   r@   rQ   s     r   r   z _ManagedRotatingFileHandler.emit  sQ     ;"bgnnT5F&G&G"..000Vr   c                 p    t                                                      }|                                  |S r   )rL   r   r   )rP   r"   rQ   s     r   r   z!_ManagedRotatingFileHandler._open  s+       r   c                     t                                                       |                                  |                                  d S r   )rL   
doRolloverr   r   )rP   rQ   s    r   r   z&_ManagedRotatingFileHandler.doRollover  sE        	  """""r   r2   N)rV   rW   rX   rY   rM   r   r   r   rD   r[   r   r   r   r]   r^   s   @r   r   r   f  s         0# # # # #  8 8 8 8/ / / /b7,           
# # # # # # # # #r   r   )r   loggerr   r|   r}   r~   r   c                   |                                 }| j        D ]N}t          |t                    r7t	          t          |dd                                                     |k    r dS O|j                            dd           t          t          |          ||d          }	|	
                    |           |	                    |           ||	                    |           |                     |	           dS )a  Add a ``RotatingFileHandler`` to *logger*, skipping if one already
    exists for the same resolved file path (idempotent).

    Parameters
    ----------
    log_filter
        Optional filter to attach to the handler (e.g. ``_ComponentFilter``
        for gateway.log).
    r   r   NTrt   r   )maxBytesbackupCountr
   )resolver   r   r   r   r   parentr   r   rZ   r   r   	addFilterr   )
r   r   r|   r}   ro   r~   r   resolvedexistingr   s
             r   r   r     s   & ||~~HO  x!455	WX~r::;;CCEEQQFFKdT222)D		I<  G U###*%%%
gr   c                     	 ddl } t                      }|                                rt          |dd          5 }|                     |          pi }ddd           n# 1 swxY w Y   |                    di           }t          |t                    r>|                    d          |                    d          |                    d	          fS n# t          $ r Y nw xY wd
S )u   Best-effort read of ``logging.*`` from config.yaml.

    Returns ``(level, max_size_mb, backup_count)`` — any may be ``None``.
    r   Nrr   )r
   rD   r|   rn   ro   )NNN)	yamlr   r   open	safe_loadgetr   dictr!   )r   config_pathfcfglog_cfgs        r   r   r     s1   
%'' 		k3999 .QnnQ''-2. . . . . . . . . . . . . . .ggi,,G'4(( KK((KK..KK// 
    s5   8C AC A""C %A"&A+C 
C C r   )'rY   r   rD   r   r   	threadinglogging.handlersr   pathlibr   typingr   r   hermes_constantsr   r   r   localr5   r   r   r%   r   rZ   r6   r8   rG   FilterrI   r   intr\   r   r   r   Logger	Formatterr   r   r   r   r   <module>r      s   : 
			  				 



     0 0 0 0 0 0       % % % % % % % % = = = = = = = =
   #9?$$ 
 O[   D,-C -D - - - -' ' ' '9 9 9 9:    ! ! !6 6 6 6 6w~ 6 6 6$ -B   * #'#!%"&u u u$u }u #	u
 3-u 3-u u 
u u u up; ; ; ;Ht# t# t# t# t#"5 t# t# t#~ ,0$ $ $N$
$ 	$
 $ $  $ ($ 
$ $ $ $N    r   