The audit log Python API

Query the audit log from your services with self.audit - search events, find silent feeds and read stored payloads.

Every service has self.audit - the same audit log the Dashboard shows, queried from Python. A service that confirms a message arrived, lists last night's failures or reports feed silence uses the same API for every supported audit-log database.

The API is read-only and has three methods:

MethodWhat it returns
self.audit.search(...)Which events match - by free text, source, object, outcome, event type or time window
self.audit.last_seen(source)The newest event time of each object of one source
self.audit.get_payload(event_id)The full message body of one event

Searching events

self.audit.search returns a list of dicts, newest first. Every parameter is optional and they combine - each one you pass narrows the result down further:

# -*- coding: utf-8 -*-

# Zato
from zato.common.audit_log.api import AuditOutcome
from zato.server.service import Service

class RecentFailures(Service):
    """ Returns the failures recorded since a given moment.
    """
    input = 'time_from'

    def handle(self) -> 'None':

        # Search for failures recorded in the requested period ..
        events = self.audit.search(
            outcome=AuditOutcome.Error,
            time_from=self.request.input.time_from,
        )

        # .. and return the matching events.
        self.response.payload = {'events': events}

The parameters:

ParameterDefaultDescription
query(none)Free text to search for - see what the free text covers
sourceallWhich part of the log to read - an AuditSource constant, e.g. AuditSource.MLLP_Channel
object_nameallAn object's name, e.g. the name of a specific channel or connection
outcomeallAuditOutcome.OK, AuditOutcome.Error or AuditOutcome.Expired
event_typeallAn AuditEvent constant, e.g. AuditEvent.Message_Received
time_from(none)Only events at or after this ISO timestamp
time_to(none)Only events before this ISO timestamp
status(none)Status_Outstanding returns open exchanges - the source must be named too
page1Which page of results to return
page_size50How many events one page holds

Each of source, object_name, outcome and event_type accepts one value or a list of values - a list matches events carrying any of them:

# Search for failures and expirations across both MLLP directions.
events = self.audit.search(
    source=[AuditSource.MLLP_Channel, AuditSource.MLLP_Outgoing],
    outcome=[AuditOutcome.Error, AuditOutcome.Expired],
)

All the constants come from one module:

# Zato
from zato.common.audit_log.api import AuditEvent, AuditOutcome, AuditSource, Status_Outstanding

Event times are ISO timestamps compared as strings, so a bare date works as a prefix - time_from='2026-08-29' matches everything from that day on.

Each returned event is a dict with these keys:

KeyDescription
idThe event's ID - what get_payload takes
cidThe correlation ID shared by all events of one message
sourceThe source the event is filed under
event_typeWhat happened, e.g. message-received or ack-sent
object_nameThe channel or connection the event belongs to
msg_idThe message's own ID, e.g. the MSH-10 control ID of an HL7 message
correl_idThe ID of the message this one answers or resubmits
ext_client_idThe ID the external caller sent, if any
event_time_isoWhen the event happened, as an ISO timestamp
endpointThe address the message arrived from or was sent to
sizeThe size of the data, in bytes
outcomeok, error or expired
classificationFor failures - transient, permanent or operator-fixable
statusThe error text or other status details
duration_msHow long the exchange took, in milliseconds
dataA preview of the payload - get_payload returns the full payload

What the free text covers

The query parameter looks inside payloads, message IDs, correlation IDs, endpoints, external client IDs, statuses, classifications and event types. Wildcards match literally - a % in the query is a percent sign to find, not a pattern.

When the search names a source, it also covers that source's own searchable attributes - an MLLP source's message type, MRN, facility and ACK status, a FHIR source's resource type and method, a scheduler source's job ID. An attribute match returns the whole trace it appears in, not just the single event that carries it - a search by an MRN returns the message that mentioned the MRN together with the ACK that answered it:

# Search for arrivals and acknowledgments of one patient's messages.
events = self.audit.search(
    source=AuditSource.MLLP_Channel,
    query='12345678',
)

This is the same search the Dashboard's search box runs, described on audit log views.

Exchanges still waiting

status=Status_Outstanding returns the open exchanges of one source. For outgoing MLLP, these are messages awaiting acknowledgment. Each source defines what counts as open, so this filter requires the source parameter:

# Search for outgoing HL7 messages awaiting an acknowledgment.
events = self.audit.search(
    source=AuditSource.MLLP_Outgoing,
    status=Status_Outstanding,
)

When each channel was last seen

self.audit.last_seen takes one source and returns a dict mapping each of its object names to the ISO timestamp of its newest event. Comparing these times with a threshold identifies silent feeds:

# -*- coding: utf-8 -*-

# stdlib
from datetime import datetime, timedelta

# Zato
from zato.common.audit_log.api import AuditSource
from zato.server.service import Service

class SilentFeeds(Service):
    """ Returns the HL7 channels whose latest event is older than one hour.
    """
    output = 'silent'

    def handle(self) -> 'None':

        # Get the newest event time of each channel ..
        last_seen = self.audit.last_seen(AuditSource.MLLP_Channel)

        # .. calculate the one-hour silence threshold ..
        cutoff = datetime.utcnow() - timedelta(hours=1)
        cutoff_iso = cutoff.isoformat()

        # .. collect channels whose latest event precedes the threshold ..
        silent = []

        for channel_name, last_event_time in last_seen.items():
            if last_event_time < cutoff_iso:
                silent.append(channel_name)

        # .. and return their names.
        self.response.payload = {'silent': silent}

Reading a payload

The event's data key is a preview. self.audit.get_payload returns the whole body of one event, by the event's id:

# Zato
from zato.common.audit_log.api import AuditBody

# Read the latest stored body of an event.
payload = self.audit.get_payload(event_id)

# Select a specific stored body.
request_body = self.audit.get_payload(event_id, AuditBody.Request)
response_body = self.audit.get_payload(event_id, AuditBody.Response)
error_body = self.audit.get_payload(event_id, AuditBody.Error)

The method returns None for a missing event, a payload-free source such as the MCP audit log, or an event whose payload retention period has ended.

Payloads are the sensitive part of the record - in clinical traffic they are protected health information - so return them from a service only when the caller needs the message body itself, not just the metadata about it.

Sources

The source parameter of search and last_seen names which part of the log to read, always through AuditSource constants - search accepts one or a list of them, last_seen takes one. These are the most commonly queried ones - the full catalog of what each source records is on the audit log overview:

ConstantEvents of
AuditSource.REST_ChannelREST channels
AuditSource.REST_OutgoingOutgoing REST connections
AuditSource.SOAP_ChannelSOAP channels
AuditSource.SOAP_OutgoingOutgoing SOAP connections
AuditSource.PubSubPublish/subscribe topics
AuditSource.Email_IMAPIMAP connections
AuditSource.Email_SMTPSMTP connections
AuditSource.File_OutgoingFile transfer connections
AuditSource.SQL_OutgoingOutgoing SQL connections
AuditSource.MLLP_ChannelHL7 MLLP channels
AuditSource.MLLP_OutgoingOutgoing HL7 MLLP connections
AuditSource.FHIROutgoing FHIR connections
AuditSource.MCPMCP gateways
AuditSource.LLMLLM connections
AuditSource.SchedulerScheduler jobs
AuditSource.ConfigConfiguration changes and payload access

A complete example

One service that answers whether a patient's messages arrived, ready to be exposed to AI agents as an MCP tool:

# -*- coding: utf-8 -*-

# Zato
from zato.common.audit_log.api import AuditEvent, AuditSource
from zato.server.service import Service

# #####################################################################
# #####################################################################

class PatientMessageStatus(Service):
    """ Returns whether HL7 messages for a given patient MRN arrived,
    with the control ID, event type and arrival time of each.
    """
    name = 'patient.message-status'

    input  = 'patient_mrn'
    output = 'has_arrived', 'messages'

    def handle(self) -> 'None':

        # Read the MRN the caller asked about ..
        patient_mrn = self.request.input.patient_mrn

        # .. search for received MLLP messages that mention it ..
        events = self.audit.search(
            source=AuditSource.MLLP_Channel,
            event_type=AuditEvent.Message_Received,
            query=patient_mrn,
        )

        # .. copy only the fields needed by the caller ..
        messages = []

        for event in events:
            messages.append({
                'control_id': event['msg_id'],
                'event_type': event['event_type'],
                'received_at': event['event_time_iso'],
            })

        # .. determine whether at least one message arrived ..
        has_arrived = len(messages) > 0

        # .. and return the minimized result.
        self.response.payload = {
            'has_arrived': has_arrived,
            'messages': messages,
        }

Learn more