
    jU                        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	m
Z
 ej                  dk(  rddlmZ nddl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d0dZd0d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#d0d$Z$ G d% d&e      Z%dd'd(ejL                  d)ed*e!d+e!d e!d,ejN                  d-e	ej<                     ddfd.Z(d/ Z)y)1u   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)Path)OptionalSequencewin32)ConcurrentRotatingFileHandler)RotatingFileHandler)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      xs d}|j                         j	                  dd      dv r| S 	 t        | dd      }|#t        j                  |ddd	
      }d |_        |S 	 | S # t        $ r Y | S w xY w)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                       y N r       /home/cube/projects/richard/traning coach/.omo/evidence/nutricoach-v150-combined/st_01a0560c-r63-installed-wheel-first-claim-qa/venv/lib/python3.12/site-packages/hermes_logging.py<lambda>z_safe_stderr.<locals>.<lambda>o   s    Dr   )	sysstderrgetattrlowerr   ioTextIOWrapperclose	Exception)streamr   bufwrappeds       r   _safe_stderrr'   U   s     ZZFvz40;GH~~R(,KKfh-?&&  #	G )GMN  M  Ms   1A< <	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         _        y)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_contextr3   )r3   s    r   set_session_contextr8      s     #-r   c                      dt         _        y)z,Clear the session ID for the current thread.Nr6   r   r   r   clear_session_contextr:      s    "&r   c                      t        j                         t        dd      ryfd} d| _        t        j                  |        y)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                  f     | i |}t        t        dd       }|rd| d|_        |S d|_        |S )Nr3   z []r   )r   r7   session_tag)argskwargsrecordsidcurrent_factorys       r   _session_record_factoryz@_install_session_record_factory.<locals>._session_record_factory   sG     $1&1&d;,/r#a[ 68r   T)logginggetLogRecordFactoryr   r<   setLogRecordFactory)rE   rD   s    @r   _install_session_record_factoryrI      sB     113O :EB 8<4 78r   c                   V     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.
    prefixesr4   Nc                 B    t         |           t        |      | _        y r   )super__init__tuple	_prefixes)selfrL   	__class__s     r   rO   z_ComponentFilter.__init__   s    xr   rB   c                 L    |j                   j                  | j                        S r   )name
startswithrQ   )rR   rB   s     r   filterz_ComponentFilter.filter   s    {{%%dnn55r   )__name__
__module____qualname____doc__r   strrO   rF   	LogRecordboolrW   __classcell__rS   s   @r   rK   rK      s8    )# )4 )6W.. 64 6r   rK   )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)ra   rc   rg   ri   rj   gui)hermes_home	log_levelmax_size_mbbackup_countmodeforcern   ro   rp   rq   rr   rs   c                    | xs
 t               }|dz  }|j                  dd       t               \  }}	}
|xs |xs dj                         }t	        t
        |t
        j                        }|xs |	xs ddz  dz  }|xs |
xs d}dd	lm} t        j                         }t        ||d
z  ||| |t                     t        ||dz  t
        j                  dd |t                     |dk(  r=t        ||dz  t
        j                  dd |t              t        t        d                |dk(  r=t        ||dz  t
        j                  dd |t              t        t        d                t        r|s|S |j                   t
        j"                  k(  s|j                   |kD  r|j%                  |       t&        D ]4  }t        j                  |      j%                  t
        j                         6 d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_bytesrq   	formatterz
errors.logi       ra   zgateway.logi  P )r~   r   rq   r   
log_filterrm   zgui.logi   )r
   mkdir_read_logging_configupperr   rF   ry   agent.redactr}   	getLogger_add_rotating_handler_LOG_FORMATWARNINGrK   COMPONENT_PREFIXES_logging_initializedr~   NOTSETsetLevel_NOISY_LOGGERS)rn   ro   rp   rq   rr   rs   homelog_dir	cfg_levelcfg_max_size
cfg_backup
level_namer~   r   backupsr}   rootrU   s                     r   setup_loggingr      s   X +/+DVmGMM$M. +?*@'I|Z2y2F99;JGZ6E11T9D@I-j-AG 0D +$[1 ,oo!$[1 ym#,,%(5'(:9(EF	
 u}i,,&(5'(:5(AB	
 E zzW^^#tzzE'9e $((9   Nr   c                     ddl m}  t        j                         }|j                  D ]=  }t        |t        j                        st        |t              r/t        |dd      s= y t        j                  t                     }|j                  t        j                         |j                   | t        d             d|_        |j                  |       |j                   t        j                  kD  r|j                  t        j                         t"        D ]4  }t        j                  |      j                  t        j$                         6 t        j                  d	      j                  t        j&                         y)
zEnable DEBUG-level console logging for ``--verbose`` / ``-v`` mode.

    Called by ``AIAgent.__init__()`` when ``verbose_logging=True``.
    r   r|   _hermes_verboseFNz%H:%M:%S)datefmtTz
rex-deploy)r   r}   rF   r   handlers
isinstanceStreamHandlerr   r   r'   r   DEBUGsetFormatter_LOG_FORMAT_VERBOSEr   
addHandlerr~   r   r   ry   )r}   r   hhandlerrU   s        r   setup_verbose_loggingr   _  s
   
 0D ]]a../
1FY8Zq+U3 
 ##LN3GW]]#+,?TU"GOOG zzGMM!gmm$ $((9  l#,,W\\:r   c                   t     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        |   |i | d | _        d | _        | j                          y )Nr   )
is_managed)hermes_cli.configr   _managedrN   rO   	_stat_dev	_stat_ino_record_stream_stat)rR   r@   rA   r   rS   s       r   rO   z$_ManagedRotatingFileHandler.__init__  s=    0"$)&) )-(,  "r   c                 ~    | j                   r"	 t        j                  | j                  d       y y # t        $ r Y y w xY w)Ni  )r   oschmodbaseFilenameOSError)rR   s    r   _chmod_if_managedz-_ManagedRotatingFileHandler._chmod_if_managed  s;    ==**E2   s    0 	<<r4   Nc                     	 t        j                  | j                        }|j                  |j                  c| _        | _        y# t        $ r d\  | _        | _        Y yw xY w)zHSnapshot dev/ino of ``baseFilename`` so we can detect external rotation.)NNN)r   statr   st_devst_inor   r   r   rR   sts     r   r   z/_ManagedRotatingFileHandler._record_stream_stat  sP    	8**+B-/YY		*DNDN 	8-7*DNDN	8s   AA A A c                 D   	 t        j                  | j                        }| j                  | j                  $|j                  |j                  c| _
        | _        y|j                  |j                  f| j                  | j                  fk7  rh	 | j                  | j                  j                          d| _        	 | j                         | _        |j                  |j                  c| _
        | _        yy# t        $ rw 	 | j                  | j                  j                          n# t        $ r Y nw xY wd| _        	 | j                         | _        | j                          Y y# t        $ r Y Y yw xY wt        $ r Y yw xY w# t        $ r Y w xY w# t        $ r Y yw xY w)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}   	**+B* >>!T^^%;-/YY		*DNDNIIryy!dnndnn%EE;;*KK%%' DK"jjl13BII. F1 ! 	;;*KK%%' DK"jjl((*
 	   	
  		    sw   C7 &F =8F 7	F&D('F(	D41F3D44
F?%E&&	E3/F2E33F F	FF	FFrB   c                     | j                   )t        j                  j                  | j                        r| j                          t        |   |       y r   )r$   r   pathexistsr   r   rN   emit)rR   rB   rS   s     r   r   z _ManagedRotatingFileHandler.emit  s<     ;;"bggnnT5F5F&G..0Vr   c                 D    t         |          }| j                          |S r   )rN   r   r   )rR   r$   rS   s     r   r   z!_ManagedRotatingFileHandler._open  s     r   c                 b    t         |           | j                          | j                          y r   )rN   
doRolloverr   r   )rR   rS   s    r   r   z&_ManagedRotatingFileHandler.doRollover  s(      	  "r   r4   N)rX   rY   rZ   r[   rO   r   r   r   rF   r]   r   r   r   r_   r`   s   @r   r   r     sE    0#8/b7,,  
# #r   r   )r   loggerr   r~   r   r   r   c                   |j                         }| j                  D ]<  }t        |t              st	        t        |dd            j                         |k(  s< y |j                  j                  dd       t        t        |      ||d      }	|	j                  |       |	j                  |       ||	j                  |       | j                  |	       y)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   NTrv   r   )maxBytesbackupCountr   )resolver   r   r   r   r   parentr   r   r\   r   r   	addFilterr   )
r   r   r~   r   rq   r   r   resolvedexistingr   s
             r   r   r     s    & ||~HOOx!45WX~r:;CCEQ $ 	KKdT2)D	I<G U#*%
gr   c                     	 ddl } t               }|j                         rt        |dd      5 }| j	                  |      xs i }ddd       	 ddlm} |j                        }j                  di       }t        |t              r2|j                  d      |j                  d	      |j                  d
      fS y# 1 sw Y   vxY w# t        $ r Y lw xY w# t        $ r Y yw xY w)u   Best-effort read of ``logging.*`` from config.yaml.

    Returns ``(level, max_size_mb, backup_count)`` — any may be ``None``.
    r   Nrr   )r   )managed_scoperF   r~   rp   rq   )NNN)yamlr	   r   open	safe_loadrh   r   apply_managed_overlayr#   getr   dict)r   config_pathfcfgr   log_cfgs         r   r   r   !  s    
%'k39QnnQ'-2 :4#99#> ggi,G'4(KK(KK.KK/  % :9    sM   ,C B9C C $AC 9C>C 	CC CC 	C C r   )*r[   r    rF   r   r   	threadingpathlibr   typingr   r   platformconcurrent_log_handlerr   r   logging.handlershermes_constantsr	   r
   r   localr7   r   r   r'   r   r\   r8   r:   rI   FilterrK   r   intr^   r   r   r   Logger	Formatterr   r   r   r   r   <module>r      s  : 
  	 
   %0 <<7 5 >
   #9??$ 
 O[ D,-C -D -'9:   !6w~~ 6$ -B  * #'#!%"&u$u }u #	u
 3-u 3-u u 
up;Ht#"5 t#~ ,0$NN$
$ 	$
 $ $   $ ($ 
$Nr   