Skip to content

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

  1. 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). 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.
  2. 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).
  3. 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.
  4. 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).
  5. 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).
  6. 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

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).
  • 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).
  • 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.

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