---
title: "OP-08: Post a log entry to the Logging API"
summary: The outbound HTTP delivery path to the central SAIF Logging API.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.Logging
reference_type: package
operation_id: OP-08
trigger: Serilog dispatches an emitted log event to the attached LoggingApiSink.
---

# OP-08: Post a log entry to the Logging API

| Field | Value |
| --- | --- |
| Operation ID | OP-08 |
| Trigger | The Serilog pipeline dispatches a log event to `LoggingApiSink` (attached for web and Windows Service hosts only). |
| Inputs | The `LogEvent` being emitted; the configured base URL, route, error route, and output template. |
| Outputs | An HTTP POST to the Logging API; on an unsuccessful HTTP result, a Console/Debug-only diagnostic. |
| Primary components | `LoggingApiSink` |
| Entry point | `LoggingApiSink.Emit` |

## Steps
0. Before any event is emitted, the sink's constructor runs `_httpJsonClient.SetClientInfo(baseUrl, "SaifLog", Environment.MachineName, _applicationId)` and then builds `_messageFormatter = new MessageTemplateTextFormatter(logPattern, null)`, both unconditionally and with no surrounding try/catch in this library's code ([BR-LOG-14](../business-rules.md#br-log-14-message-formatter-built-once-at-construction)). If either call throws — for example, if the specific `IHttpJsonClient` implementation rejects an empty/invalid `baseUrl`, or `MessageTemplateTextFormatter` rejects a `null`/malformed `logPattern` — the sink object itself is never fully constructed, so it is never attached to the Serilog pipeline and `Emit` (and therefore neither `Send` nor `SendException`) ever runs for that construction attempt.
1. `Emit(LogEvent logEvent)` (an `async void` method with no try/catch of its own) checks `IsException(logEvent)` — `logEvent.Exception != null && logEvent.Properties.ContainsKey(StringValues.LogExceptionProperty)` — not a switch on an incoming DTO type ([BR-LOG-13](../business-rules.md#br-log-13-sink-routes-exceptions-by-event-properties)).
2. When `IsException` is `true`, `SendException` builds the error-route URI (suffixed `?errorId={LogId}` when `logEvent.LogId()` is non-blank) and POSTs `logEvent.LogExceptionDetail()` — the `ErrorLogItem` extracted from the event's properties, not a rendered message string; this branch never calls `_messageFormatter`.
3. When `IsException` is `false`, `Send` renders the event through `_messageFormatter` (built once in step 0, from the configured output template — standard or `appsettings.json`-overridden) and POSTs the resulting `LogItem` to the non-error route ([BR-LOG-14](../business-rules.md#br-log-14-message-formatter-built-once-at-construction)).
4. After either branch completes, `Emit` unconditionally runs `HandleHttpPostError`, which checks `IHttpJsonClient.RequestResult.IsSuccessful` and writes a diagnostic to `Console`/`Debug` only when it is `false` ([BR-LOG-15](../business-rules.md#br-log-15-emit-is-async-void-with-no-exception-containment)).
5. An exception thrown by the `PostAsync` call itself (as opposed to an unsuccessful result) is not caught anywhere in this sink. Because `Emit` is `async void` rather than `async Task`, that exception is not captured on a `Task` for later observation — it propagates directly to the SynchronizationContext captured when `Emit` starts, or the thread pool if none was captured, rather than being reported by this library at all.

## Process flow

```mermaid
flowchart TD
    A[LogEvent dispatched to LoggingApiSink.Emit] --> B{"IsException: Exception != null AND<br>LogExceptionDetails property present?"}
    B -->|Yes| C["SendException: POST logEvent.LogExceptionDetail()<br>to error route + errorId"]
    B -->|No| D["Send: render via output template,<br>POST LogItem to non-error route"]
    C --> E[HandleHttpPostError checks RequestResult.IsSuccessful]
    D --> E
    E -->|Unsuccessful| F[Write diagnostic to Console/Debug only]
    E -->|Successful| G[No further action]
```

## Failure and edge-case behavior
- An empty base URL results in a POST to a potentially invalid endpoint only if the sink's construction in step 0 succeeds; `SetClientInfo` and the `MessageTemplateTextFormatter` constructor run first, with no surrounding try/catch, and if either throws the sink is never fully constructed, so no POST is attempted for that instance at all. `SetClientInfo` receives only the base URL and client identity — not either route — so an empty base URL is the only construction-time input that can affect the final endpoint; the route and error route are stored and passed separately as the `PostAsync` URI argument, and final endpoint validity for an empty route depends entirely on how the injected `IHttpJsonClient` combines it with the base URL, which this library's source does not determine or confirm. When construction does succeed, an unsuccessful result from the resulting POST is reported only to Console/Debug, never surfaced to the original caller ([BR-LOG-15](../business-rules.md#br-log-15-emit-is-async-void-with-no-exception-containment)).
- A thrown exception from `PostAsync` (network failure, serialization error, etc.) is not caught anywhere in `LoggingApiSink`; there is no universal failure-containment guarantee — only unsuccessful HTTP *results* are inspected and reported.
- `SaifWindowsServiceLog` always posts errors with a `null` route component, since it never reads `logerrorroute` ([BR-LOG-09](../business-rules.md#br-log-09-windows-service-logger-never-sets-error-route)).
- A `null` output template (from an override entry missing `LogPattern`) is passed into the sink's constructor at construction time (`new MessageTemplateTextFormatter(logPattern, null)`), before either branch runs, not merely into `Send`'s per-event rendering step; whether `MessageTemplateTextFormatter`'s own constructor accepts a `null` template without throwing is not confirmed in this library's source.** If that constructor call throws, the sink is never fully constructed, so neither `Send` nor `SendException` runs at all for that attempt — there is no case where construction fails but `SendException` still executes unaffected. If construction instead succeeds (a `null` template is tolerated), `SendException` never applies `_messageFormatter` and so its payload shape is unaffected by the template's value, same as always.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

> **Note:** Statements marked with `**` could not be confirmed; treat as unknown until verified.

## Sources
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Loggers/LoggingApiSink.cs`
