Architecture & Internals¶
Component Overview¶
┌─────────────────────────────────────────────────┐
│ aaLogForwarder (GUI) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Settings │ │ Polling │ │ Network │ │
│ │ Manager │ │ Loop │ │ Sender │ │
│ └──────────┘ └────┬─────┘ └───────┬───────┘ │
│ │ │ │
└─────────────────────┼────────────────┼───────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────┐
│ aaLogReader │ │ Syslog │
│ (library) │ │ Server │
└──────┬───────┘ └──────────┘
│
▼
┌──────────────┐
│ .aaLog Files │
│ + Cache File │
└──────────────┘
The system has three layers:
- aaLogForwarder -- WinForms GUI that drives the polling loop, manages settings, and sends records over the network
- aaLogReader -- Library that reads binary
.aaLogfiles and tracks position via a cache file - Binary log files -- Written by Archestra/System Platform on disk
Polling Loop¶
The forwarder runs a continuous polling loop on a background thread. Each cycle:
- Creates a new
aaLogReaderinstance - Calls
GetUnreadRecords()to fetch new log records - Formats each record (RFC 5424, RFC 3164, or KVP)
- Sends formatted records over TCP or UDP
- Waits for the configured polling interval
- Repeats
┌──────────────────────────────────────────────┐
│ │
▼ │
Read Records ──► Format ──► Send ──► Wait ──────┘
│ │
│ (on failure) │ (on failure)
▼ ▼
Log Error Backoff + Retry
Polling Interval¶
The interval between cycles is configurable from 500 ms to 60,000 ms (default: 1,000 ms). The interval is read from the UI control at the start of each cycle, so it can be changed while running.
First-Iteration Behavior¶
When Send Existing Logs is enabled, the first polling cycle calls:
This reads all records from disk, ignoring the cache file. Subsequent cycles call GetUnreadRecords() normally, reading only new records.
Error Handling & Retry¶
Exception Categories¶
The polling loop catches specific exception types:
| Exception | Meaning | Action |
|---|---|---|
OperationCanceledException |
User clicked Stop | Propagate (exit loop) |
SocketException |
Network unreachable, connection refused | Retry with backoff |
AuthenticationException |
TLS handshake failed | Retry with backoff |
IOException |
Connection reset, broken pipe | Retry with backoff |
Exception |
Anything else | Retry with backoff |
Exponential Backoff¶
On failure, the wait time increases exponentially:
| Consecutive Failures | Backoff (1s interval) |
|---|---|
| 1 | 1,000 ms |
| 2 | 2,000 ms |
| 3 | 4,000 ms |
| 4 | 8,000 ms |
| 5 | 16,000 ms |
| 6+ | 30,000 ms (cap) |
The backoff resets to zero after any successful cycle.
Auto-Stop¶
After 10 consecutive failures, the forwarder stops automatically. The status bar shows:
Check the connection settings and click Start to retry.
Network Sending¶
TCP¶
For TCP connections, the forwarder:
- Opens a new
TcpClientfor each polling cycle - Optionally wraps the stream in
SslStreamfor TLS - Sends all records in the batch sequentially
- Flushes and closes the connection
TCP uses RFC 6587 octet-counting framing for RFC 5424 and RFC 3164 formats:
KVP format uses newline-delimited framing.
UDP¶
For UDP connections, each record is sent as an independent datagram. There is no connection state, no TLS, and no delivery guarantee.
Log Record Reading¶
File Discovery¶
The aaLogReader library finds .aaLog files by resolving the log directory in this order:
OptionsStruct.LogDirectory(if set programmatically)- Registry:
HKLM\SOFTWARE\Wow6432Node\ArchestrA\Framework\Logger→LogDir %ProgramData%\ArchestrA\LogFiles(if the directory exists)- Fallback:
C:\ProgramData\ArchestrA\LogFiles
File Format¶
Each .aaLog file contains:
- A file header with metadata (message count, time range, computer name, offsets)
- A sequence of log records with backward and forward linkage
Records are stored in a binary format with fixed-size numeric fields and variable-length Unicode strings. See Binary Log Format for byte-level details.
File Sharing¶
The reader opens files with FileShare.ReadWrite, which allows reading active log files that Archestra is still writing to. This is critical for real-time forwarding.
Cache File¶
The cache file stores the last-read LogRecord as JSON. On each call to GetUnreadRecords():
- Read the cache file to get the last message number
- Scan forward from that position in the current (and newer) log files
- Return the new records
- Update the cache file with the latest record
The cache file is stored in the same directory as the .aaLog files, named after the calling process (e.g., aaLogGUITester-aaLogReaderCache).
Threading Model¶
aaLogForwarder¶
| Thread | Role |
|---|---|
| UI thread | Handles user input, updates controls |
| Background task | Runs PollingLoopAsync via Task.Run |
The polling loop accesses UI controls using Control.Invoke() / InvokeRequired for thread safety. The CancellationTokenSource coordinates shutdown between the UI thread and the background task.
aaLogReader¶
The aaLogReader class is not thread-safe. It maintains shared mutable state:
_fileStream-- the currently open log file_currentLogHeader-- parsed header of the current file_lastRecordRead-- the most recently read record
Each polling cycle creates a new aaLogReader instance (wrapped in using), so there is no shared state between cycles.
Message Formatting Pipeline¶
Each log record passes through this pipeline before being sent:
LogRecord
│
▼
Sanitize (flatten + truncate)
│
▼
Format (RFC5424 / RFC3164 / KVP)
│
▼
Frame (octet-counting or newline)
│
▼
Encode (UTF-8)
│
▼
Send (TCP or UDP)
Sanitization¶
Sanitization is applied at the LogRecord level via static properties:
- Flatten: Controlled by
LogRecord.FlattenMessages. Replaces\r\n,\n,\rwith|. - Truncate: Controlled by
LogRecord.MessageTruncateLength. Cuts at the character limit and appends....
These are set once when Start is clicked and apply to all records in the session.
Severity Mapping¶
The LogFlag string from each record is mapped to a numeric syslog severity:
| LogFlag | Severity |
|---|---|
critical |
2 (Critical) |
error, failure |
3 (Error) |
warning |
4 (Warning) |
success, info |
6 (Informational) |
debug |
7 (Debug) |
| (other) | 6 (Informational) |
The mapping is case-insensitive.