Initial commit: Wonderware / System Platform tools and reference
Five tools under one repo, all docs organized per DOCS-GUIDE.md: - aalogcli: .NET 4.8 / x86 CliFx CLI for reading System Platform binary logs (*.aaLGX) for LLM debugging, built on aaOpenSource/aaLog. Commands: last, tail, range, unread, fields. Stable JSON envelope under --llm-json. Build template under lib/build/ for rebuilding aaLogReader.dll. - aot: ArchestrA Object Toolkit 2014 v4.0 reference material. Dev guide (Markdown converted from CHM), API reference for the ArchestrA.Toolkit namespace, and the Monitor / Watchdog VS sample solutions. - graccesscli: .NET 4.8 / x86 CliFx CLI that automates Galaxy configuration via the ArchestrA GRAccess COM interop. Includes session daemon, IPC protocol, and llm-json envelope contract. - grdb: SQL/DDL exploration of the Galaxy Repository database. DDL captures, reusable queries, hierarchy / contained-name <-> tag-name translation notes. - histdb: LLM-oriented reference for AVEVA Historian retrieval. INSQL linked-server, extension tables, every wwXxx time-domain extension, every retrieval mode, alarm/event SQL recipes, REST API. Distilled from the 243-page Historian Retrieval Guide. Root contains: - CLAUDE.md: thin index pointing into each tool's README. - DOCS-GUIDE.md: doctrine for organizing docs for LLM consumption. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# AGENTS.md
|
||||
|
||||
Guidance for coding agents working in the `aalogcli` folder.
|
||||
|
||||
## Project Snapshot
|
||||
|
||||
This folder contains `aalogcli` (assembly name `aalog`), a `.NET Framework 4.8 / x86` CliFx-based CLI for reading AVEVA / Wonderware System Platform binary log files (`*.aaLGX`). It wraps the third-party reader library [`aaOpenSource/aaLog`](https://github.com/aaOpenSource/aaLog) and exposes it as `last`, `tail`, `range`, `unread`, and `fields` commands tailored for LLM-driven debugging (stable JSON envelope, bounded payloads, post-fetch filters).
|
||||
|
||||
For end-to-end command reference and examples, read [`docs/usage.md`](docs/usage.md). For the JSON shape of an emitted record, read [`docs/fields.md`](docs/fields.md). For the upstream reader's API (used by [`LogReaderFactory.cs`](src/AaLog.Cli/LogReaderFactory.cs) and the commands), see the [aaLog README](https://github.com/aaOpenSource/aaLog).
|
||||
|
||||
## Key Documentation
|
||||
|
||||
All paths are relative to this `aalogcli/` folder.
|
||||
|
||||
- [`README.md`](README.md) — tool entry point, hard constraints, build instructions.
|
||||
- [`docs/usage.md`](docs/usage.md) — every command, every option, with worked examples for the LLM-JSON envelope.
|
||||
- [`docs/fields.md`](docs/fields.md) — `LogRecordDto` field reference; the canonical shape of records in `--llm-json` output.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```text
|
||||
aalogcli/
|
||||
AaLog.Cli.slnx
|
||||
lib/aaLogReader.dll (provisioned out-of-band — see README)
|
||||
src/AaLog.Cli/
|
||||
AaLog.Cli.csproj
|
||||
Program.cs
|
||||
LogReaderFactory.cs
|
||||
Commands/
|
||||
CommonOptions.cs (ReadCommandBase — shared options)
|
||||
LastCommand.cs (`last` — last N records)
|
||||
TailCommand.cs (`tail` — last N minutes)
|
||||
RangeCommand.cs (`range` — explicit start/end)
|
||||
UnreadCommand.cs (`unread`— incremental, cache-backed)
|
||||
FieldsCommand.cs (`fields`— field reference printout)
|
||||
Output/
|
||||
LogRecordDto.cs (LLM-friendly subset of aaLogReader.LogRecord)
|
||||
OutputWriter.cs (human single-line + llm-json envelope)
|
||||
Filtering/
|
||||
RecordFilter.cs (substring / regex over Component, Level, Message)
|
||||
IsExternalInit.cs (C# 9 `init` polyfill for net48)
|
||||
```
|
||||
|
||||
## Build And Test
|
||||
|
||||
Run commands from this `aalogcli` folder unless noted otherwise.
|
||||
|
||||
```powershell
|
||||
dotnet build src/AaLog.Cli/AaLog.Cli.csproj -p:Platform=x86 -c Release
|
||||
dotnet run --project src/AaLog.Cli/AaLog.Cli.csproj -- <args>
|
||||
```
|
||||
|
||||
There is no test project at present. If you add one, mirror `graccesscli`'s convention: `tests/AaLog.Cli.Tests/AaLog.Cli.Tests.csproj`, `net48` / `x86`, xunit + Shouldly.
|
||||
|
||||
## Implementation Rules
|
||||
|
||||
- Keep the CLI targeting `net48` and `x86`. The upstream `aaLogReader` is net40 — net48 loads it cleanly, .NET 10 / x64 will not.
|
||||
- Do **not** add a `[STAThread]` apartment requirement. Unlike `graccesscli`, `aaLogReader` is plain managed file I/O and runs fine on any thread.
|
||||
- Construct readers through [`LogReaderFactory.Open`](src/AaLog.Cli/LogReaderFactory.cs) so every command honors `--log-dir` the same way.
|
||||
- Always wrap the reader in `using` — it implements `IDisposable` and holds a `FileStream`.
|
||||
- Treat the upstream library as authoritative for log decoding. Do not reimplement aaLGX parsing in this CLI.
|
||||
- Filtering is **client-side**, after the library returns records. Do not push filter strings into `OptionsStruct.LogRecordPostFilters` from the CLI layer — surface stays flatter that way.
|
||||
- Every read command must inherit [`ReadCommandBase`](src/AaLog.Cli/Commands/CommonOptions.cs) so `--log-dir` / `--component` / `--level` / `--message` / `--regex` / `--llm-json` stay consistent.
|
||||
- `--llm-json` envelope shape is `{ query: {...}, count: N, records: [LogRecordDto, ...] }`. Preserve the envelope contract — agents may parse it positionally.
|
||||
- `LogRecordDto` is the JSON contract. Adding a field is a non-breaking change; renaming or removing one is breaking — bump the docs in the same commit and call it out.
|
||||
- C# language version is 9.0. `init` is supported through `IsExternalInit.cs`; do not use the `required` keyword.
|
||||
- Use CliFx command patterns: `[Command]`, `[CommandOption]`, classes implement `ICommand`.
|
||||
|
||||
## Output Contracts
|
||||
|
||||
| Mode | Trigger | Shape |
|
||||
| --- | --- | --- |
|
||||
| Human | default | One line per record: `[localTs] [level] component (process#pid/tid) \| message` |
|
||||
| LLM-JSON | `--llm-json` | `{ "query": {...}, "count": N, "records": [LogRecordDto, ...] }` |
|
||||
|
||||
The `query` object echoes the invocation parameters so an agent reading the JSON can confirm which window it actually got. `log_dir` in the query is the **resolved** directory (post-`--log-dir` override), so it doubles as a sanity check.
|
||||
|
||||
## Adding A New Command
|
||||
|
||||
1. Add `Commands/<Name>Command.cs` inheriting `ReadCommandBase` and implementing `ICommand`.
|
||||
2. If it introduces a new query verb (e.g. `since-message-number`), wrap the corresponding `aaLogReader` method in `LogReaderFactory` or inline; document the verb in [`docs/usage.md`](docs/usage.md).
|
||||
3. Add a row in [`README.md`](README.md) and [`docs/usage.md`](docs/usage.md). Do not modify `../CLAUDE.md` — the root index points at this README and that is sufficient.
|
||||
4. Keep `--llm-json` envelope semantics identical across commands (`query`, `count`, `records`).
|
||||
@@ -0,0 +1,5 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/AaLog.Cli/AaLog.Cli.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
@@ -0,0 +1,95 @@
|
||||
# aalogcli
|
||||
|
||||
A `.NET Framework 4.8 / x86` CLI for reading the AVEVA / Wonderware **System Platform** binary log files (`*.aaLGX`) — built on the [aaOpenSource/aaLog](https://github.com/aaOpenSource/aaLog) reader library and tuned for LLM-driven debugging (last N records, last N minutes, explicit time ranges, incremental "unread" polling, JSON envelope output).
|
||||
|
||||
## Hard constraints
|
||||
|
||||
- **Default log directory is `C:\ProgramData\ArchestrA\LogFiles`**, which only exists on a host running System Platform / Application Server. Override with `--log-dir <path>` when reading copied logs.
|
||||
- **`lib/aaLogReader.dll` must be provisioned before building** — `aaLog` is not on NuGet. See [Provisioning aaLogReader.dll](#provisioning-aalogreaderdll).
|
||||
- **Target framework is `net48`, platform `x86`** to match the upstream library and stay aligned with `graccesscli`. Do not retarget to .NET 10 / x64.
|
||||
- **Filtering is post-fetch**, so `--component` / `--level` / `--message` narrow the result set the library returns; they do not push down into the binary scan.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
aalogcli/
|
||||
AaLog.Cli.slnx
|
||||
README.md this file
|
||||
AGENTS.md agent guidance for working in this folder
|
||||
lib/
|
||||
aaLogReader.dll (not committed — see provisioning section)
|
||||
src/AaLog.Cli/
|
||||
AaLog.Cli.csproj
|
||||
Program.cs
|
||||
LogReaderFactory.cs
|
||||
Commands/ last, tail, range, unread, fields
|
||||
Output/ LogRecordDto, OutputWriter (human + llm-json)
|
||||
Filtering/ post-fetch substring/regex filter
|
||||
docs/
|
||||
usage.md command surface, options, examples
|
||||
fields.md LogRecord JSON field reference
|
||||
```
|
||||
|
||||
## Resource index — by task
|
||||
|
||||
| Task | Go to |
|
||||
| --- | --- |
|
||||
| Agent rules for editing this CLI | [`AGENTS.md`](AGENTS.md) |
|
||||
| Run the CLI / option reference / example invocations | [`docs/usage.md`](docs/usage.md) |
|
||||
| `LogRecord` JSON field shape and types | [`docs/fields.md`](docs/fields.md) |
|
||||
| Upstream reader library (license, source, build) | [aaOpenSource/aaLog on GitHub](https://github.com/aaOpenSource/aaLog) |
|
||||
| LLM JSON envelope contract | [`docs/usage.md`](docs/usage.md#llm-json-envelope) |
|
||||
|
||||
## Provisioning `aaLogReader.dll`
|
||||
|
||||
The upstream library is GitHub-only, targets .NET 4.0, and ships with a legacy MSBuild `.csproj` plus a `packages.config` — none of which the modern .NET SDK 10 build pipeline restores cleanly. The recipe below rebuilds it as an SDK-style net48 project; this is the path that's been verified end-to-end on this repo.
|
||||
|
||||
If you have Visual Studio with full MSBuild + NuGet on PATH and prefer the upstream csproj as-is, that works too — just produce `aaLogReader.dll` somehow and drop it in `lib/`.
|
||||
|
||||
### SDK-style rebuild recipe (verified)
|
||||
|
||||
```powershell
|
||||
# 1. Clone upstream sources somewhere outside this repo:
|
||||
git clone https://github.com/aaOpenSource/aaLog.git $env:TEMP\aaLog
|
||||
|
||||
# 2. Create a sibling build folder with the SDK csproj from this repo's
|
||||
# aalogcli/lib/build/aaLogReader.csproj template (see below).
|
||||
mkdir $env:TEMP\aaLogReader-build
|
||||
copy <wwtools>\aalogcli\lib\build\aaLogReader.csproj $env:TEMP\aaLogReader-build\
|
||||
|
||||
# 3. Two source files need a one-line patch — they reference [CallerMemberName]
|
||||
# behind an #if NET45_OR_GREATER guard but never `using System.Runtime.CompilerServices;`.
|
||||
# Copy the templates from this repo's lib/build/patched/ into a sibling folder:
|
||||
mkdir $env:TEMP\aaLogReader-build\patched
|
||||
copy <wwtools>\aalogcli\lib\build\patched\*.cs $env:TEMP\aaLogReader-build\patched\
|
||||
|
||||
# 4. Build and copy out:
|
||||
cd $env:TEMP\aaLogReader-build
|
||||
dotnet build -c Release
|
||||
copy bin\Release\net48\aaLogReader.dll <wwtools>\aalogcli\lib\aaLogReader.dll
|
||||
```
|
||||
|
||||
The csproj template sets `<Deterministic>false</Deterministic>` (the upstream `AssemblyInfo.cs` uses `[assembly: AssemblyVersion("1.0.*")]`) and pins `Newtonsoft.Json 13.0.3` / `log4net 2.0.15` to match what `aalog` itself uses, so there are no transitive-version conflicts at runtime.
|
||||
|
||||
`aaLogReader.dll` only needs to land in `lib/`. `Newtonsoft.Json.dll` and `log4net.dll` are pulled in via NuGet by `aalog` itself and end up next to `aalog.exe` automatically.
|
||||
|
||||
## Build & run
|
||||
|
||||
```powershell
|
||||
dotnet build src/AaLog.Cli/AaLog.Cli.csproj -p:Platform=x86 -c Release
|
||||
|
||||
# Last 50 records, human readable:
|
||||
dotnet run --project src/AaLog.Cli/AaLog.Cli.csproj -- last
|
||||
|
||||
# Last 5 minutes, error-level only, JSON envelope for an LLM:
|
||||
dotnet run --project src/AaLog.Cli/AaLog.Cli.csproj -- tail --minutes 5 --level Error --llm-json
|
||||
|
||||
# Explicit range with regex message filter:
|
||||
dotnet run --project src/AaLog.Cli/AaLog.Cli.csproj -- range --from 2026-05-03T08:00 --to 2026-05-03T09:00 --message "Galaxy.*timeout" --regex --llm-json
|
||||
```
|
||||
|
||||
The built executable is `bin\Release\net48\aalog.exe` — drop it on `PATH` and use `aalog last -n 100`.
|
||||
|
||||
## Maintenance
|
||||
|
||||
This README follows the project doctrine in [`../DOCS-GUIDE.md`](../DOCS-GUIDE.md). When you add a command, an option, or a new field to the DTO, update [`docs/usage.md`](docs/usage.md) and [`docs/fields.md`](docs/fields.md) in the same change. The root [`../CLAUDE.md`](../CLAUDE.md) holds one row pointing at this README — it should not need to change unless the tool's task surface changes.
|
||||
@@ -0,0 +1,33 @@
|
||||
# aalog — LogRecord field reference
|
||||
|
||||
The `--llm-json` envelope emits records of shape `LogRecordDto`, an LLM-friendly subset of the upstream [`aaLogReader.LogRecord`](https://github.com/aaOpenSource/aaLog/blob/master/aaLogReader/Types/LogRecord.cs) (declared in the bare `aaLogReader` namespace despite the file path). File-format internals (`RecordLength`, `OffsetToPrevRecord`, `OffsetToNextRecord`) and the redundant `EventDate` / `EventTime` / `EventMillisec` triple are dropped.
|
||||
|
||||
## Fields
|
||||
|
||||
| Field | Type | Source | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `MessageNumber` | `ulong` | `LogRecord.MessageNumber` | Monotonic record id assigned by the logger. Stable across reads of the same log directory; useful as a cursor. |
|
||||
| `TimestampUtc` | `string` | `LogRecord.EventDateTimeUtc` | Event time in UTC, ISO-8601 with `Z` suffix and millisecond precision (`2026-05-03T13:59:42.117Z`). |
|
||||
| `TimestampLocal` | `string` | `LogRecord.EventDateTimeLocal`| Event time in the host's local zone, ISO-8601 without offset (`2026-05-03T08:59:42.117`). |
|
||||
| `Level` | `string` | `LogRecord.LogFlag` | Severity / category. Common values: `Info`, `Warning`, `Error`, `Trace`, `Debug`. |
|
||||
| `Component` | `string` | `LogRecord.Component` | Originating subsystem (e.g. `aaEngine`, `Bootstrap`, `aaGR`, `aaIDE`). Best filter axis for narrowing scope. |
|
||||
| `ProcessName` | `string` | `LogRecord.ProcessName` | Name of the OS process that emitted the record. |
|
||||
| `ProcessId` | `uint` | `LogRecord.ProcessID` | OS process id. |
|
||||
| `ThreadId` | `uint` | `LogRecord.ThreadID` | OS thread id within the process. |
|
||||
| `SessionId` | `string` | `LogRecord.SessionID` | Session identifier; often empty. |
|
||||
| `Host` | `string` | `LogRecord.HostFQDN` | Fully-qualified host name at time of emission. |
|
||||
| `Message` | `string` | `LogRecord.Message` | Free-form message body. |
|
||||
|
||||
## Backward / forward compatibility
|
||||
|
||||
- **Adding a new field** to `LogRecordDto` is non-breaking. Agents that pin the existing field set continue to work.
|
||||
- **Renaming or removing a field** is breaking. If it ever happens, bump this doc and the `--llm-json` example in [`usage.md`](usage.md) in the same change.
|
||||
- The upstream library's underlying `LogRecord` may change shape across `aaLogReader.dll` versions. The CLI shields callers from that — only fields mapped explicitly in `LogRecordDto.From` ([`src/AaLog.Cli/Output/LogRecordDto.cs`](../src/AaLog.Cli/Output/LogRecordDto.cs)) reach the JSON envelope.
|
||||
|
||||
## Quick reference at runtime
|
||||
|
||||
```powershell
|
||||
aalog fields
|
||||
```
|
||||
|
||||
prints the same field list to stdout in plain text — useful when the caller can't load this file.
|
||||
@@ -0,0 +1,137 @@
|
||||
# aalog — usage
|
||||
|
||||
Command surface for the `aalog` CLI. The tool reads AVEVA / Wonderware System Platform binary logs (`*.aaLGX`) under `C:\ProgramData\ArchestrA\LogFiles` (override with `--log-dir`).
|
||||
|
||||
## Common options
|
||||
|
||||
Inherited by `last`, `tail`, `range`, and `unread`:
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `--log-dir <path>` | `C:\ProgramData\ArchestrA\LogFiles` | Read from a copied-out log directory. |
|
||||
| `--component <pattern>` | (none) | Substring match against `Component`. With `--regex`, treated as a regex. |
|
||||
| `--level <pattern>` | (none) | Substring/regex against `LogFlag` (`Info`, `Warning`, `Error`, `Trace`, …). |
|
||||
| `--message <pattern>` | (none) | Substring/regex against the message body. |
|
||||
| `--regex` | off | Switch all three pattern options to regex (case-insensitive). |
|
||||
| `--llm-json` | off | Emit the stable JSON envelope instead of human-readable lines. |
|
||||
|
||||
## Commands
|
||||
|
||||
### `aalog last`
|
||||
|
||||
Most recent N records ending at `now` (or `--until`).
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `-n`, `--count <int>` | `50` | How many records to return. |
|
||||
| `--until <iso>` | now | ISO-8601 local time anchor for the end of the window. |
|
||||
|
||||
```powershell
|
||||
aalog last # last 50, human readable
|
||||
aalog last -n 200 --llm-json # last 200, JSON envelope
|
||||
aalog last --component aaEngine --level Error -n 100
|
||||
```
|
||||
|
||||
### `aalog tail`
|
||||
|
||||
Records from the last N minutes.
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `-m`, `--minutes <int>` | `10` | Window length in minutes. Must be positive. |
|
||||
| `--max <int>` | `1000` | Hard cap on records returned (keeps LLM payloads bounded). |
|
||||
|
||||
```powershell
|
||||
aalog tail # last 10 minutes
|
||||
aalog tail -m 60 --level Error --llm-json # last hour, errors only, JSON
|
||||
aalog tail -m 5 --message "checkpoint failed" --regex
|
||||
```
|
||||
|
||||
### `aalog range`
|
||||
|
||||
Explicit start/end timestamps.
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `--from <iso>` | (required) | Start timestamp, ISO-8601 local time. |
|
||||
| `--to <iso>` | now | End timestamp, ISO-8601 local time. Must be later than `--from`. |
|
||||
| `--max <int>` | `1000` | Hard cap on records returned. |
|
||||
|
||||
```powershell
|
||||
aalog range --from 2026-05-03T08:00 --to 2026-05-03T09:00 --llm-json
|
||||
aalog range --from 2026-05-03T14:30:00 --component "Galaxy" --level Warning
|
||||
```
|
||||
|
||||
### `aalog unread`
|
||||
|
||||
Incremental polling. Uses the upstream library's cache file (under `%LOCALAPPDATA%\aaLogReader\` by default) to remember the last record ID seen, so successive invocations only return what's new.
|
||||
|
||||
| Option | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
| `--max <ulong>` | `1000` | Maximum unread records to return. |
|
||||
| `--ignore-cache` | off | Re-read regardless; the next call resumes from the new high-water mark. |
|
||||
| `--client-id <name>` | (none) | Distinct cache files for parallel consumers. |
|
||||
|
||||
```powershell
|
||||
aalog unread --llm-json # everything new since last call
|
||||
aalog unread --client-id watchdog --max 200 # independent cache for this consumer
|
||||
```
|
||||
|
||||
### `aalog fields`
|
||||
|
||||
Print the LogRecord JSON field reference and exit. Same content as [`fields.md`](fields.md).
|
||||
|
||||
```powershell
|
||||
aalog fields
|
||||
```
|
||||
|
||||
## LLM-JSON envelope
|
||||
|
||||
When `--llm-json` is set, every read command writes a single JSON document to stdout:
|
||||
|
||||
```json
|
||||
{
|
||||
"query": {
|
||||
"command": "tail",
|
||||
"minutes": 5,
|
||||
"start": "2026-05-03T08:55:00",
|
||||
"end": "2026-05-03T09:00:00",
|
||||
"max": 1000,
|
||||
"component": null,
|
||||
"level": "Error",
|
||||
"message": null,
|
||||
"regex": false,
|
||||
"log_dir": "C:\\ProgramData\\ArchestrA\\LogFiles"
|
||||
},
|
||||
"count": 3,
|
||||
"records": [
|
||||
{
|
||||
"MessageNumber": 18234021,
|
||||
"TimestampUtc": "2026-05-03T13:59:42.117Z",
|
||||
"TimestampLocal": "2026-05-03T08:59:42.117",
|
||||
"Level": "Error",
|
||||
"Component": "aaEngine",
|
||||
"ProcessName": "aaEngine",
|
||||
"ProcessId": 4128,
|
||||
"ThreadId": 6884,
|
||||
"SessionId": "",
|
||||
"Host": "PROD-AS-01.example.local",
|
||||
"Message": "Galaxy connection timeout after 30000ms"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Envelope guarantees
|
||||
|
||||
- `query` echoes every parameter that affected the result, including the **resolved** `log_dir` after `--log-dir` is applied. Agents can confirm scope without re-running the command.
|
||||
- `count == records.length`.
|
||||
- `records` is sorted **newest first** for `last`, `tail`, `range`, and `unread`.
|
||||
- Field names in `records[*]` match exactly what `aalog fields` prints. Adding new fields is non-breaking; renaming or removing them is.
|
||||
|
||||
## Tips for LLM use
|
||||
|
||||
- Cap aggressively. `--minutes 60 --max 200 --level Error` is more useful than dumping a thousand lines of `Trace`.
|
||||
- Filter on `Component` first when chasing a specific subsystem (e.g. `aaEngine`, `Bootstrap`, `aaGR`). Levels alone are noisy.
|
||||
- For follow-along debugging, prefer `unread --client-id <agent-name>` so each agent has its own cache.
|
||||
- Pair with `graccesscli` mutations: run a `graccess` command, then `aalog tail -m 1 --llm-json` to read the resulting log activity.
|
||||
@@ -0,0 +1,3 @@
|
||||
aaLogReader.dll must be dropped into this folder before building.
|
||||
|
||||
See ../README.md "Provisioning aaLogReader.dll" for build instructions.
|
||||
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Andy Robinson (Phase 2 Automation)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# aaLogReader build template
|
||||
|
||||
Use these three files to rebuild `aaLogReader.dll` from upstream sources without depending on legacy MSBuild + `packages.config`. See [`../../README.md`](../../README.md) "Provisioning aaLogReader.dll" for the full step-by-step.
|
||||
|
||||
## Files
|
||||
|
||||
- [`aaLogReader.csproj`](aaLogReader.csproj) — SDK-style csproj targeting `net48`, references `Newtonsoft.Json 13.0.3` and `log4net 2.0.15`. Disables determinism (the upstream `AssemblyInfo.cs` uses a wildcarded `[assembly: AssemblyVersion("1.0.*")]`) and pulls in upstream source via relative `<Compile Include="..\aaLog\aaLogReader\..." />` globs. Two source files are excluded from those globs and replaced by the patched copies below.
|
||||
- [`patched/LogRecord.cs`](patched/LogRecord.cs) — upstream `aaLogReader/Types/LogRecord.cs` with `using System.Runtime.CompilerServices;` added.
|
||||
- [`patched/LogHeader.cs`](patched/LogHeader.cs) — same patch applied to `aaLogReader/Types/LogHeader.cs`.
|
||||
|
||||
## Why the patches
|
||||
|
||||
The upstream files reference `[CallerMemberName]` inside an `#if NET45_OR_GREATER` branch but never `using System.Runtime.CompilerServices;`. The original csproj only defined `NET45_OR_GREATER` for `TargetFrameworkVersion >= 4.5`; targeting net40 left the branch dead and the compiler never tripped. When SDK-style projects target net48, the compiler reaches that branch and fails because the attribute can't be resolved. The patched copies add the missing using directive and otherwise match upstream byte-for-byte.
|
||||
|
||||
## Layout the recipe expects
|
||||
|
||||
```
|
||||
$env:TEMP\
|
||||
aaLog\ (git clone https://github.com/aaOpenSource/aaLog.git)
|
||||
aaLogReader\
|
||||
aaLgxReader.cs
|
||||
aaLogReader.cs
|
||||
Enum\, Helpers\, Properties\, Struct\, Types\
|
||||
aaLogReader-build\
|
||||
aaLogReader.csproj (copied from this folder)
|
||||
patched\
|
||||
LogRecord.cs (copied from this folder)
|
||||
LogHeader.cs (copied from this folder)
|
||||
```
|
||||
|
||||
`dotnet build -c Release` from `aaLogReader-build/` produces `bin/Release/net48/aaLogReader.dll`. Copy that into `aalogcli/lib/aaLogReader.dll`.
|
||||
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<RootNamespace>aaLogReader</RootNamespace>
|
||||
<AssemblyName>aaLogReader</AssemblyName>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<Deterministic>false</Deterministic>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="log4net" Version="2.0.15" />
|
||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ComponentModel.Composition" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\aaLog\aaLogReader\aaLgxReader.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\aaLogReader.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\Enum\*.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\Helpers\*.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\Struct\*.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\Types\ILogHeader.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\Types\ILogRecord.cs" />
|
||||
<Compile Include="..\aaLog\aaLogReader\Types\aaLogReaderException.cs" />
|
||||
<Compile Include="patched\LogRecord.cs" />
|
||||
<Compile Include="patched\LogHeader.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,259 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace aaLogReader
|
||||
{
|
||||
public class LogHeader : ILogHeader
|
||||
{
|
||||
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
public string LogFilePath { get; set; }
|
||||
|
||||
public ulong StartMsgNumber { get; set; }
|
||||
|
||||
public ulong MsgCount { get; set; }
|
||||
|
||||
public ulong EndMsgNumber
|
||||
{
|
||||
get
|
||||
{
|
||||
return (ulong)(checked(this.StartMsgNumber + this.MsgCount) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private ulong _startFileTime;
|
||||
private DateTimeOffset _startDateTime;
|
||||
|
||||
public ulong StartFileTime
|
||||
{
|
||||
get { return _startFileTime; }
|
||||
set
|
||||
{
|
||||
_startFileTime = value;
|
||||
_startDateTime = DateTimeOffset.FromFileTime((long)value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset StartDateTime
|
||||
{
|
||||
get { return _startDateTime; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime StartDateTimeLocal
|
||||
{
|
||||
get { return _startDateTime.LocalDateTime; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime StartDateTimeUtc
|
||||
{
|
||||
get { return _startDateTime.UtcDateTime; }
|
||||
}
|
||||
|
||||
private ulong _endFileTime;
|
||||
private DateTimeOffset _endDateTime;
|
||||
|
||||
public ulong EndFileTime
|
||||
{
|
||||
get { return _endFileTime; }
|
||||
set
|
||||
{
|
||||
_endFileTime = value;
|
||||
_endDateTime = DateTimeOffset.FromFileTime((long)value);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset EndDateTime
|
||||
{
|
||||
get { return _endDateTime; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime EndDateTimeLocal
|
||||
{
|
||||
get { return _endDateTime.LocalDateTime; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime EndDateTimeUtc
|
||||
{
|
||||
get { return _endDateTime.UtcDateTime; }
|
||||
}
|
||||
|
||||
public int OffsetFirstRecord { get; set; }
|
||||
|
||||
public int OffsetLastRecord { get; set; }
|
||||
|
||||
public string ComputerName { get; set; }
|
||||
|
||||
public string Session { get; set; }
|
||||
|
||||
public string PrevFileName { get; set; }
|
||||
|
||||
public string HostFQDN { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public ReturnCodeStruct ReturnCode { get; set; }
|
||||
|
||||
public string ToJSON()
|
||||
{
|
||||
return JsonConvert.SerializeObject(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the log header data in the form of a Key-Value Pair
|
||||
/// </summary>
|
||||
/// <param name="format">Full or Minimal</param>
|
||||
/// <returns></returns>
|
||||
public string ToKVP()
|
||||
{
|
||||
string returnValue;
|
||||
StringBuilder localSB = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
localSB.AppendFormat("MsgStartingNumber=\"{0}\"", this.StartMsgNumber.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||
localSB.AppendFormat(", MsgCount=\"{0}\"", this.MsgCount);
|
||||
localSB.AppendFormat(", MsgLastNumber=\"{0}\"", this.EndMsgNumber);
|
||||
localSB.AppendFormat(", StartDateTime=\"{0}\"", this.StartDateTime);
|
||||
localSB.AppendFormat(", EndDateTime=\"{0}\"", this.EndDateTime);
|
||||
localSB.AppendFormat(", OffsetFirstRecord=\"{0}\"", this.OffsetFirstRecord);
|
||||
localSB.AppendFormat(", OffsetLastRecord=\"{0}\"", this.OffsetLastRecord);
|
||||
localSB.AppendFormat(", ComputerName=\"{0}\"", this.ComputerName);
|
||||
localSB.AppendFormat(", Session=\"{0}\"", this.Session);
|
||||
localSB.AppendFormat(", PrevFileName=\"{0}\"", this.PrevFileName);
|
||||
localSB.AppendFormat(", HostFQDN=\"{0}\"", this.HostFQDN);
|
||||
|
||||
returnValue = localSB.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ex);
|
||||
returnValue = "";
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a header for a series of log records with a delimiter
|
||||
/// </summary>
|
||||
/// <param name="Delimiter"></param>
|
||||
/// <param name="format"></param>
|
||||
/// <returns></returns>
|
||||
private string localHeader(char Delimiter = ',')
|
||||
{
|
||||
string returnValue;
|
||||
StringBuilder localSB = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
localSB.Append("LogFilePath");
|
||||
localSB.Append(Delimiter + "MsgStartingNumber");
|
||||
localSB.Append(Delimiter + "MsgCount");
|
||||
localSB.Append(Delimiter + "MsgLastNumber");
|
||||
localSB.Append(Delimiter + "StartDateTime");
|
||||
localSB.Append(Delimiter + "StartFileTime");
|
||||
localSB.Append(Delimiter + "EndDateTime");
|
||||
localSB.Append(Delimiter + "EndFileTime");
|
||||
localSB.Append(Delimiter + "OffsetFirstRecord");
|
||||
localSB.Append(Delimiter + "OffsetLastRecord");
|
||||
localSB.Append(Delimiter + "ComputerName");
|
||||
localSB.Append(Delimiter + "Session");
|
||||
localSB.Append(Delimiter + "PrevFileName");
|
||||
localSB.Append(Delimiter + "HostFQDN");
|
||||
|
||||
returnValue = localSB.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ex);
|
||||
returnValue = "";
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public static string Header(char Delimiter = ',')
|
||||
{
|
||||
LogHeader lh = new LogHeader();
|
||||
return lh.localHeader(Delimiter);
|
||||
}
|
||||
|
||||
public static string HeaderCSV()
|
||||
{
|
||||
return LogHeader.Header(',');
|
||||
}
|
||||
|
||||
public static string HeaderTSV()
|
||||
{
|
||||
return LogHeader.Header('\t');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the lastRecordRead in the form of a delimited string
|
||||
/// </summary>
|
||||
/// <param name="Delimiter">Delimiter to Use</param>
|
||||
/// <param name="format">Full or Minimal</param>
|
||||
/// <returns></returns>
|
||||
public string ToDelimitedString(char Delimiter = ',')
|
||||
{
|
||||
|
||||
string returnValue;
|
||||
StringBuilder localSB = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
localSB.Append("\"" + this.LogFilePath + "\"");
|
||||
localSB.Append(Delimiter + this.StartMsgNumber.ToString());
|
||||
localSB.Append(Delimiter + this.MsgCount.ToString());
|
||||
localSB.Append(Delimiter + this.EndMsgNumber.ToString());
|
||||
localSB.Append(Delimiter + "\"" + this.StartDateTime.ToString("yyyy-MM-dd HH:mm:ss.fff") + "\"");
|
||||
localSB.Append(Delimiter + this.StartFileTime.ToString());
|
||||
localSB.Append(Delimiter + "\"" + this.EndDateTime.ToString("yyyy-MM-dd HH:mm:ss.fff") + "\"");
|
||||
localSB.Append(Delimiter + this.EndFileTime.ToString());
|
||||
localSB.Append(Delimiter + this.OffsetFirstRecord.ToString());
|
||||
localSB.Append(Delimiter + this.OffsetLastRecord.ToString());
|
||||
localSB.Append(Delimiter + "\"" + this.ComputerName + "\"");
|
||||
localSB.Append(Delimiter + this.Session);
|
||||
localSB.Append(Delimiter + "\"" + this.PrevFileName + "\"");
|
||||
localSB.Append(Delimiter + "\"" + this.HostFQDN + "\"");
|
||||
|
||||
returnValue = localSB.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ex);
|
||||
returnValue = "";
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public string ToCSV()
|
||||
{
|
||||
return this.ToDelimitedString(',');
|
||||
}
|
||||
|
||||
public string ToTSV()
|
||||
{
|
||||
return this.ToDelimitedString('\t');
|
||||
}
|
||||
|
||||
#if NET45_OR_GREATER
|
||||
private void LogException(Exception ex, [CallerMemberName]string methodName = "")
|
||||
{
|
||||
#else
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
|
||||
private void LogException(Exception ex)
|
||||
{
|
||||
string methodName = new System.Diagnostics.StackFrame(1, false).GetMethod().Name;
|
||||
#endif
|
||||
log.Error(string.Format("{0}: {1} - {2}", methodName, ex.GetType().Name, ex.Message), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace aaLogReader
|
||||
{
|
||||
/// <summary>
|
||||
/// A standard log record
|
||||
/// </summary>
|
||||
public class LogRecord : ILogRecord
|
||||
{
|
||||
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
// Default constructor
|
||||
public LogRecord()
|
||||
{
|
||||
this.ReturnCode.Status = false;
|
||||
this.ReturnCode.Message = "";
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public int RecordLength { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int OffsetToPrevRecord { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int OffsetToNextRecord { get; set; }
|
||||
|
||||
[Key]
|
||||
public ulong MessageNumber { get; set; }
|
||||
|
||||
public uint ProcessID { get; set; }
|
||||
|
||||
public uint ThreadID { get; set; }
|
||||
|
||||
private ulong _eventFileTime;
|
||||
private DateTimeOffset _eventDateTime;
|
||||
|
||||
public ulong EventFileTime
|
||||
{
|
||||
get { return _eventFileTime; }
|
||||
set
|
||||
{
|
||||
_eventFileTime = value;
|
||||
_eventDateTime = DateTimeOffset.FromFileTime((long)value);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add UTC Offset for Exact Timestamp
|
||||
// public int EventUTCOffset;
|
||||
|
||||
public DateTimeOffset EventDateTime
|
||||
{
|
||||
get { return _eventDateTime; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime EventDateTimeLocal
|
||||
{
|
||||
get { return _eventDateTime.LocalDateTime; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime EventDateTimeUtc
|
||||
{
|
||||
get { return _eventDateTime.UtcDateTime; }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime EventDate
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.EventDateTime.Date;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string EventTime
|
||||
{
|
||||
get { return this.EventDateTime.ToString("hh:mm:ss.fff tt"); }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public int EventMillisec
|
||||
{
|
||||
get { return this.EventDateTime.Millisecond; }
|
||||
}
|
||||
|
||||
public string LogFlag { get; set; }
|
||||
|
||||
public string Component { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string ProcessName { get; set; }
|
||||
|
||||
public string SessionID { get; set; }
|
||||
|
||||
public string HostFQDN { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public ReturnCodeStruct ReturnCode;
|
||||
|
||||
public string ToJSON()
|
||||
{
|
||||
return JsonConvert.SerializeObject(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the log record in the form of a Key-Value Pair
|
||||
/// </summary>
|
||||
/// <param name="format">Full or Minimal</param>
|
||||
/// <returns></returns>
|
||||
public string ToKVP(ExportFormat format = ExportFormat.Full)
|
||||
{
|
||||
string returnValue;
|
||||
StringBuilder localSB = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
localSB.AppendFormat("Timestamp=\"{0}\"", this.EventDateTime.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||
localSB.AppendFormat(", LogFlag=\"{0}\"", this.LogFlag);
|
||||
localSB.AppendFormat(", Message=\"{0}\"", this.Message);
|
||||
localSB.AppendFormat(", HostFQDN=\"{0}\"", this.HostFQDN);
|
||||
|
||||
if (format == ExportFormat.Full)
|
||||
{
|
||||
// Use all parameters if we want a full format
|
||||
localSB.AppendFormat(", MessageNumber=\"{0}\"", this.MessageNumber);
|
||||
localSB.AppendFormat(", ProcessID=\"{0}\"", this.ProcessID);
|
||||
localSB.AppendFormat(", ThreadID=\"{0}\"", this.ThreadID);
|
||||
localSB.AppendFormat(", Component=\"{0}\"", this.Component);
|
||||
localSB.AppendFormat(", ProcessName=\"{0}\"", this.ProcessName);
|
||||
localSB.AppendFormat(", SessionID=\"{0}\"", this.SessionID);
|
||||
localSB.AppendFormat(", EventFileTime=\"{0}\"", this.EventFileTime);
|
||||
}
|
||||
returnValue = localSB.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ex);
|
||||
returnValue = "";
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a header for a series of log records with a delimiter
|
||||
/// </summary>
|
||||
/// <param name="Delimiter"></param>
|
||||
/// <param name="format"></param>
|
||||
/// <returns></returns>
|
||||
private string localHeader(char Delimiter = ',', ExportFormat format = ExportFormat.Full)
|
||||
{
|
||||
string returnValue;
|
||||
StringBuilder localSB = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
localSB.Append("EventDateTime");
|
||||
localSB.Append(Delimiter + "LogFlag");
|
||||
localSB.Append(Delimiter + "Message");
|
||||
localSB.Append(Delimiter + "HostFQDN");
|
||||
|
||||
|
||||
if (format == ExportFormat.Full)
|
||||
{
|
||||
// Use all parameters if we want a full format
|
||||
localSB.Append(Delimiter + "MessageNumber");
|
||||
localSB.Append(Delimiter + "ProcessID");
|
||||
localSB.Append(Delimiter + "ThreadID");
|
||||
localSB.Append(Delimiter + "Component");
|
||||
localSB.Append(Delimiter + "ProcessName");
|
||||
localSB.Append(Delimiter + "SessionID");
|
||||
localSB.Append(Delimiter + "EventFileTime");
|
||||
}
|
||||
|
||||
returnValue = localSB.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ex);
|
||||
returnValue = "";
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public static string Header(char Delimiter = ',', ExportFormat format = ExportFormat.Full)
|
||||
{
|
||||
LogRecord lr = new LogRecord();
|
||||
return lr.localHeader(Delimiter, format);
|
||||
}
|
||||
|
||||
public static string HeaderCSV(ExportFormat format = ExportFormat.Full)
|
||||
{
|
||||
return LogRecord.Header(',', format);
|
||||
}
|
||||
|
||||
public static string HeaderTSV(ExportFormat format = ExportFormat.Full)
|
||||
{
|
||||
return LogRecord.Header('\t', format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the log record in the form of a delimited string
|
||||
/// </summary>
|
||||
/// <param name="Delimiter">Delimiter to Use</param>
|
||||
/// <param name="format">Full or Minimal</param>
|
||||
/// <returns></returns>
|
||||
public string ToDelimitedString(char Delimiter = ',', ExportFormat format = ExportFormat.Full, DateTimeKind kind = DateTimeKind.Unspecified)
|
||||
{
|
||||
|
||||
string returnValue;
|
||||
StringBuilder localSB = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
if (kind == DateTimeKind.Utc)
|
||||
localSB.Append("\"" + this.EventDateTimeUtc.ToString("yyyy-MM-dd HH:mm:ss.fffZ") + "\"");
|
||||
else
|
||||
localSB.Append("\"" + this.EventDateTime.ToString("yyyy-MM-dd HH:mm:ss.fff") + "\"");
|
||||
localSB.Append(Delimiter + this.LogFlag);
|
||||
localSB.Append(Delimiter + "\"" + this.Message + "\"");
|
||||
localSB.Append(Delimiter + this.HostFQDN);
|
||||
|
||||
if (format == ExportFormat.Full)
|
||||
{
|
||||
// Use all parameters if we want a full format
|
||||
localSB.Append(Delimiter + this.MessageNumber.ToString());
|
||||
localSB.Append(Delimiter + this.ProcessID.ToString());
|
||||
localSB.Append(Delimiter + this.ThreadID.ToString());
|
||||
localSB.Append(Delimiter + "\"" + this.Component + "\"");
|
||||
localSB.Append(Delimiter + "\"" + this.ProcessName + "\"");
|
||||
localSB.Append(Delimiter + this.SessionID);
|
||||
localSB.Append(Delimiter + this.EventFileTime.ToString());
|
||||
}
|
||||
|
||||
returnValue = localSB.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ex);
|
||||
returnValue = "";
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public string ToCSV(ExportFormat format = ExportFormat.Full)
|
||||
{
|
||||
return this.ToDelimitedString(',', format);
|
||||
}
|
||||
|
||||
public string ToTSV(ExportFormat format = ExportFormat.Full)
|
||||
{
|
||||
return this.ToDelimitedString('\t', format);
|
||||
}
|
||||
|
||||
#if NET45_OR_GREATER
|
||||
private void LogException(Exception ex, [CallerMemberName]string methodName = "")
|
||||
{
|
||||
#else
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
|
||||
private void LogException(Exception ex)
|
||||
{
|
||||
string methodName = new System.Diagnostics.StackFrame(1, false).GetMethod().Name;
|
||||
#endif
|
||||
log.Error(string.Format("{0}: {1} - {2}", methodName, ex.GetType().Name, ex.Message), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<Platforms>x86</Platforms>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<Prefer32Bit>true</Prefer32Bit>
|
||||
<RootNamespace>AaLog.Cli</RootNamespace>
|
||||
<AssemblyName>aalog</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliFx" Version="2.3.5" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="log4net" Version="2.0.15" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="aaLogReader">
|
||||
<HintPath>..\..\lib\aaLogReader.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,28 @@
|
||||
using CliFx.Attributes;
|
||||
|
||||
namespace AaLog.Cli.Commands
|
||||
{
|
||||
/// Shared option set inherited by every read command. Kept as an abstract base so
|
||||
/// CliFx still treats each subclass as a distinct command, but option declarations
|
||||
/// only live in one place.
|
||||
public abstract class ReadCommandBase
|
||||
{
|
||||
[CommandOption("log-dir", Description = "Override the log directory. Defaults to C:\\ProgramData\\ArchestrA\\LogFiles.")]
|
||||
public string LogDirectory { get; init; }
|
||||
|
||||
[CommandOption("component", Description = "Substring (or regex with --regex) to match against the Component field.")]
|
||||
public string Component { get; init; }
|
||||
|
||||
[CommandOption("level", Description = "Substring (or regex with --regex) to match against the Level / LogFlag field (Info, Warning, Error, ...).")]
|
||||
public string Level { get; init; }
|
||||
|
||||
[CommandOption("message", Description = "Substring (or regex with --regex) to match against the Message body.")]
|
||||
public string Message { get; init; }
|
||||
|
||||
[CommandOption("regex", Description = "Treat --component / --level / --message as regular expressions instead of substrings.")]
|
||||
public bool UseRegex { get; init; }
|
||||
|
||||
[CommandOption("llm-json", Description = "Emit a stable JSON envelope { query, count, records } instead of human-readable lines.")]
|
||||
public bool LlmJson { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace AaLog.Cli.Commands
|
||||
{
|
||||
/// Quick field-reference printout so an agent can discover output shape without
|
||||
/// having to read the docs/ folder. Mirrors LogRecordDto exactly.
|
||||
[Command("fields", Description = "Print the LogRecord JSON field reference and exit.")]
|
||||
public sealed class FieldsCommand : ICommand
|
||||
{
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
console.Output.WriteLine("LogRecord fields emitted by aalog (subset of aaLogReader.LogRecord):");
|
||||
console.Output.WriteLine();
|
||||
console.Output.WriteLine(" MessageNumber ulong Monotonic record id assigned by the logger.");
|
||||
console.Output.WriteLine(" TimestampUtc string Event time, ISO-8601 with Z suffix.");
|
||||
console.Output.WriteLine(" TimestampLocal string Event time in the host's local zone, ISO-8601.");
|
||||
console.Output.WriteLine(" Level string LogFlag value: Info, Warning, Error, Trace, ...");
|
||||
console.Output.WriteLine(" Component string Originating component (e.g. Bootstrap, aaEngine).");
|
||||
console.Output.WriteLine(" ProcessName string Process that emitted the record.");
|
||||
console.Output.WriteLine(" ProcessId uint OS process id.");
|
||||
console.Output.WriteLine(" ThreadId uint OS thread id.");
|
||||
console.Output.WriteLine(" SessionId string Session id, when present.");
|
||||
console.Output.WriteLine(" Host string Host FQDN at time of emission.");
|
||||
console.Output.WriteLine(" Message string Free-form message body.");
|
||||
console.Output.WriteLine();
|
||||
console.Output.WriteLine("LLM-JSON envelope shape: { query: {...}, count: N, records: [LogRecord, ...] }");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaLog.Cli.Filtering;
|
||||
using AaLog.Cli.Output;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace AaLog.Cli.Commands
|
||||
{
|
||||
[Command("last", Description = "Fetch the most recent N records ending at 'now' (or --until).")]
|
||||
public sealed class LastCommand : ReadCommandBase, ICommand
|
||||
{
|
||||
[CommandOption("count", 'n', Description = "Number of records to fetch (most recent first). Default 50.")]
|
||||
public int Count { get; init; } = 50;
|
||||
|
||||
[CommandOption("until", Description = "Anchor timestamp for the 'end' of the window (ISO-8601, local time). Defaults to now.")]
|
||||
public DateTime? Until { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var endTimestamp = Until ?? DateTime.Now;
|
||||
using var reader = LogReaderFactory.Open(LogDirectory);
|
||||
var raw = reader.GetRecordsByEndTimestampAndCount(endTimestamp, Count) ?? new List<aaLogReader.LogRecord>();
|
||||
|
||||
// Library returns newest-first when fetched by end-timestamp; keep that for human reading.
|
||||
var dtos = raw.Select(LogRecordDto.From);
|
||||
var filtered = RecordFilter.Apply(dtos, Component, Level, Message, UseRegex);
|
||||
|
||||
if (LlmJson)
|
||||
{
|
||||
var query = new
|
||||
{
|
||||
command = "last",
|
||||
count = Count,
|
||||
until = endTimestamp.ToString("yyyy-MM-ddTHH:mm:ss"),
|
||||
component = Component,
|
||||
level = Level,
|
||||
message = Message,
|
||||
regex = UseRegex,
|
||||
log_dir = reader.Options.LogDirectory,
|
||||
};
|
||||
OutputWriter.WriteLlmJson(console, query, filtered);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputWriter.WriteHuman(console, filtered);
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaLog.Cli.Filtering;
|
||||
using AaLog.Cli.Output;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace AaLog.Cli.Commands
|
||||
{
|
||||
[Command("range", Description = "Fetch records between explicit start and end timestamps.")]
|
||||
public sealed class RangeCommand : ReadCommandBase, ICommand
|
||||
{
|
||||
[CommandOption("from", IsRequired = true, Description = "Start timestamp (ISO-8601 local time, e.g. 2026-05-03T14:00:00).")]
|
||||
public DateTime From { get; init; }
|
||||
|
||||
[CommandOption("to", Description = "End timestamp (ISO-8601 local time). Defaults to now.")]
|
||||
public DateTime? To { get; init; }
|
||||
|
||||
[CommandOption("max", Description = "Hard cap on records returned. Default 1000.")]
|
||||
public int Max { get; init; } = 1000;
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
var to = To ?? DateTime.Now;
|
||||
if (to <= From)
|
||||
throw new CommandException("--to must be later than --from.", 2);
|
||||
|
||||
using var reader = LogReaderFactory.Open(LogDirectory);
|
||||
var raw = reader.GetRecordsByStartAndEndTimeStamp(From, to) ?? new List<aaLogReader.LogRecord>();
|
||||
|
||||
var ordered = raw.OrderByDescending(r => r.EventDateTimeUtc).Take(Max);
|
||||
var dtos = ordered.Select(LogRecordDto.From);
|
||||
var filtered = RecordFilter.Apply(dtos, Component, Level, Message, UseRegex);
|
||||
|
||||
if (LlmJson)
|
||||
{
|
||||
var query = new
|
||||
{
|
||||
command = "range",
|
||||
from = From.ToString("yyyy-MM-ddTHH:mm:ss"),
|
||||
to = to.ToString("yyyy-MM-ddTHH:mm:ss"),
|
||||
max = Max,
|
||||
component = Component,
|
||||
level = Level,
|
||||
message = Message,
|
||||
regex = UseRegex,
|
||||
log_dir = reader.Options.LogDirectory,
|
||||
};
|
||||
OutputWriter.WriteLlmJson(console, query, filtered);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputWriter.WriteHuman(console, filtered);
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaLog.Cli.Filtering;
|
||||
using AaLog.Cli.Output;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace AaLog.Cli.Commands
|
||||
{
|
||||
[Command("tail", Description = "Fetch records from the last N minutes.")]
|
||||
public sealed class TailCommand : ReadCommandBase, ICommand
|
||||
{
|
||||
[CommandOption("minutes", 'm', Description = "How many minutes back from now to read. Default 10.")]
|
||||
public int Minutes { get; init; } = 10;
|
||||
|
||||
[CommandOption("max", Description = "Hard cap on records returned to keep LLM payloads bounded. Default 1000.")]
|
||||
public int Max { get; init; } = 1000;
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
if (Minutes <= 0)
|
||||
throw new CommandException("--minutes must be a positive integer.", 2);
|
||||
|
||||
var end = DateTime.Now;
|
||||
var start = end.AddMinutes(-Minutes);
|
||||
|
||||
using var reader = LogReaderFactory.Open(LogDirectory);
|
||||
var raw = reader.GetRecordsByStartAndEndTimeStamp(start, end) ?? new List<aaLogReader.LogRecord>();
|
||||
|
||||
// Newest first, then cap.
|
||||
var ordered = raw.OrderByDescending(r => r.EventDateTimeUtc).Take(Max);
|
||||
var dtos = ordered.Select(LogRecordDto.From);
|
||||
var filtered = RecordFilter.Apply(dtos, Component, Level, Message, UseRegex);
|
||||
|
||||
if (LlmJson)
|
||||
{
|
||||
var query = new
|
||||
{
|
||||
command = "tail",
|
||||
minutes = Minutes,
|
||||
start = start.ToString("yyyy-MM-ddTHH:mm:ss"),
|
||||
end = end.ToString("yyyy-MM-ddTHH:mm:ss"),
|
||||
max = Max,
|
||||
component = Component,
|
||||
level = Level,
|
||||
message = Message,
|
||||
regex = UseRegex,
|
||||
log_dir = reader.Options.LogDirectory,
|
||||
};
|
||||
OutputWriter.WriteLlmJson(console, query, filtered);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputWriter.WriteHuman(console, filtered);
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AaLog.Cli.Filtering;
|
||||
using AaLog.Cli.Output;
|
||||
using CliFx;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
|
||||
namespace AaLog.Cli.Commands
|
||||
{
|
||||
[Command("unread", Description = "Fetch records the cache has not yet seen. Useful for incremental polling.")]
|
||||
public sealed class UnreadCommand : ReadCommandBase, ICommand
|
||||
{
|
||||
[CommandOption("max", Description = "Maximum number of unread records to return. Default 1000.")]
|
||||
public ulong Max { get; init; } = 1000;
|
||||
|
||||
[CommandOption("ignore-cache", Description = "Re-read regardless of the cache file. The next call will pick up from the new high-water mark.")]
|
||||
public bool IgnoreCache { get; init; }
|
||||
|
||||
[CommandOption("client-id", Description = "Optional client ID. Use distinct IDs to maintain independent cache files for parallel consumers.")]
|
||||
public string ClientId { get; init; }
|
||||
|
||||
public ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
using var reader = LogReaderFactory.Open(LogDirectory);
|
||||
var raw = reader.GetUnreadRecords(Max, "", IgnoreCache, ClientId)
|
||||
?? new List<aaLogReader.LogRecord>();
|
||||
|
||||
var dtos = raw.OrderByDescending(r => r.EventDateTimeUtc).Select(LogRecordDto.From);
|
||||
var filtered = RecordFilter.Apply(dtos, Component, Level, Message, UseRegex);
|
||||
|
||||
if (LlmJson)
|
||||
{
|
||||
var query = new
|
||||
{
|
||||
command = "unread",
|
||||
max = Max,
|
||||
ignore_cache = IgnoreCache,
|
||||
client_id = ClientId,
|
||||
component = Component,
|
||||
level = Level,
|
||||
message = Message,
|
||||
regex = UseRegex,
|
||||
log_dir = reader.Options.LogDirectory,
|
||||
};
|
||||
OutputWriter.WriteLlmJson(console, query, filtered);
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputWriter.WriteHuman(console, filtered);
|
||||
}
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AaLog.Cli.Output;
|
||||
|
||||
namespace AaLog.Cli.Filtering
|
||||
{
|
||||
/// Client-side filtering applied after fetch. The aaLogReader library has its own
|
||||
/// LogRecordPostFilters facility, but rolling our own keeps the surface flat and
|
||||
/// avoids leaking library structs into the CLI argument layer.
|
||||
public static class RecordFilter
|
||||
{
|
||||
public static IReadOnlyList<LogRecordDto> Apply(
|
||||
IEnumerable<LogRecordDto> records,
|
||||
string componentPattern,
|
||||
string levelPattern,
|
||||
string messagePattern,
|
||||
bool useRegex)
|
||||
{
|
||||
Predicate<string> componentMatch = Build(componentPattern, useRegex);
|
||||
Predicate<string> levelMatch = Build(levelPattern, useRegex);
|
||||
Predicate<string> messageMatch = Build(messagePattern, useRegex);
|
||||
|
||||
return records
|
||||
.Where(r => componentMatch(r.Component ?? string.Empty))
|
||||
.Where(r => levelMatch(r.Level ?? string.Empty))
|
||||
.Where(r => messageMatch(r.Message ?? string.Empty))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static Predicate<string> Build(string pattern, bool useRegex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pattern)) return _ => true;
|
||||
|
||||
if (useRegex)
|
||||
{
|
||||
var rx = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
return s => rx.IsMatch(s);
|
||||
}
|
||||
|
||||
return s => s.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Polyfill so C# 9.0 `init` accessors compile on net48.
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit { }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using aaLogReader;
|
||||
|
||||
namespace AaLog.Cli
|
||||
{
|
||||
/// One place to construct an aaLogReader so every command honors --log-dir the
|
||||
/// same way and inherits the library's defaults otherwise.
|
||||
internal static class LogReaderFactory
|
||||
{
|
||||
public static aaLogReader.aaLogReader Open(string logDirectoryOverride)
|
||||
{
|
||||
var options = new OptionsStruct();
|
||||
if (!string.IsNullOrWhiteSpace(logDirectoryOverride))
|
||||
{
|
||||
options.LogDirectory = logDirectoryOverride;
|
||||
}
|
||||
return new aaLogReader.aaLogReader(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using aaLogReader;
|
||||
|
||||
namespace AaLog.Cli.Output
|
||||
{
|
||||
/// LLM-friendly subset of the underlying aaLogReader record. Drops file-format
|
||||
/// internals (record length, offsets) and the date/time/millis triple that is
|
||||
/// redundant with the full ISO-8601 timestamps.
|
||||
public class LogRecordDto
|
||||
{
|
||||
public ulong MessageNumber { get; init; }
|
||||
public string TimestampUtc { get; init; }
|
||||
public string TimestampLocal { get; init; }
|
||||
public string Level { get; init; }
|
||||
public string Component { get; init; }
|
||||
public string ProcessName { get; init; }
|
||||
public uint ProcessId { get; init; }
|
||||
public uint ThreadId { get; init; }
|
||||
public string SessionId { get; init; }
|
||||
public string Host { get; init; }
|
||||
public string Message { get; init; }
|
||||
|
||||
public static LogRecordDto From(LogRecord r) => new LogRecordDto
|
||||
{
|
||||
MessageNumber = r.MessageNumber,
|
||||
TimestampUtc = r.EventDateTimeUtc.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
|
||||
TimestampLocal = r.EventDateTimeLocal.ToString("yyyy-MM-ddTHH:mm:ss.fff"),
|
||||
Level = r.LogFlag,
|
||||
Component = r.Component,
|
||||
ProcessName = r.ProcessName,
|
||||
ProcessId = r.ProcessID,
|
||||
ThreadId = r.ThreadID,
|
||||
SessionId = r.SessionID,
|
||||
Host = r.HostFQDN,
|
||||
Message = r.Message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Infrastructure;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AaLog.Cli.Output
|
||||
{
|
||||
/// Two output modes:
|
||||
/// - Human: single-line per record, easy to scan in a terminal.
|
||||
/// - LlmJson: stable envelope { query, count, records } for agent consumption.
|
||||
public static class OutputWriter
|
||||
{
|
||||
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Include,
|
||||
Formatting = Formatting.Indented,
|
||||
};
|
||||
|
||||
public static void WriteHuman(IConsole console, IReadOnlyList<LogRecordDto> records)
|
||||
{
|
||||
foreach (var r in records)
|
||||
{
|
||||
console.Output.WriteLine(
|
||||
$"[{r.TimestampLocal}] [{r.Level,-7}] {r.Component} ({r.ProcessName}#{r.ProcessId}/{r.ThreadId}) | {r.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteLlmJson(IConsole console, object query, IReadOnlyList<LogRecordDto> records)
|
||||
{
|
||||
var envelope = new
|
||||
{
|
||||
query,
|
||||
count = records.Count,
|
||||
records,
|
||||
};
|
||||
console.Output.WriteLine(JsonConvert.SerializeObject(envelope, JsonSettings));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx;
|
||||
|
||||
namespace AaLog.Cli
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args) =>
|
||||
await new CliApplicationBuilder()
|
||||
.SetTitle("aalog")
|
||||
.SetExecutableName("aalog")
|
||||
.SetDescription("Read AVEVA / Wonderware System Platform binary log records.")
|
||||
.AddCommandsFromThisAssembly()
|
||||
.Build()
|
||||
.RunAsync(args);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user