Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/components/mw_log/detailed_design/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The backend composition and recorder relationships are shown below:
:maxdepth: 1

file_output_backend
syslog_backend
datarouter_backend/README


Expand Down
1 change: 1 addition & 0 deletions docs/components/mw_log/detailed_design/syslog_backend.md
4 changes: 3 additions & 1 deletion docs/components/mw_log/requirements/requirements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ System Backend

.. comp_req:: Forward to System Logger
:id: comp_req__log__forward_to_system_logger
:version: 1
:version: 2
:reqtype: Functional
:security: NO
:safety: QM
Expand All @@ -144,6 +144,8 @@ System Backend

Note: Under QNX, slogger2 shall be used.

Note: Under Linux, syslog(3) shall be used.

.. comp_req:: System Backend Activation
:id: comp_req__log__system_backend_activation
:version: 1
Expand Down
16 changes: 16 additions & 0 deletions score/mw/log/backend/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ cc_library(
alwayslink = True,
)

# Plugin: Linux syslog(3) System Logging
# Automatically included on Linux (HGY aarch64 / x86 host) builds.
cc_library(
name = "syslog",
srcs = ["syslog_registrant.cpp"],
features = COMPILER_WARNING_FEATURES,
tags = ["FFI"],
target_compatible_with = ["@platforms//os:linux"],
visibility = ["//visibility:public"], # platform_only
deps = [
"//score/mw/log/detail/syslog:syslog_recorder_factory",
"@score_baselibs//score/mw/log:minimal",
],
alwayslink = True,
)

# Plugin: Custom user-provided Logging backend
# Opt-in. Build with --@score_logging//score/mw/log/flags:KCustom_Logging=True
# and --@score_logging//score/mw/log/flags:custom_recorder_impl=//your:target.
Expand Down
62 changes: 62 additions & 0 deletions score/mw/log/backend/syslog_registrant.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
#include "score/mw/log/backend_table.h"
#include "score/mw/log/detail/syslog/syslog_recorder_factory.h"

namespace score
{
namespace mw
{
namespace log
{
namespace detail
{
namespace
{

std::unique_ptr<Recorder> CreateSyslogRecorder(const Configuration& config,
score::cpp::pmr::memory_resource* memory_resource)
{
SyslogRecorderFactory factory;
return factory.CreateLogRecorder(config, memory_resource);
}

/*
Deviation from Rule A3-3-2:
- Static and thread-local objects shall be constant-initialized.
Justification:
- BackendRegistrant constructor executes during dynamic initialization to write a function
pointer into gBackendCreators[]. The target array is constant-initialized (zero-init
at load time), so it is valid before this constructor runs. The registrant struct itself
is trivially destructible. This follows the established pattern used by Runtime::Instance().
Deviation from Rule M0-1-3:
- A project shall not contain unused variables.
Deviation from Rule M0-1-9:
- There shall be no dead code.
Justification:
- The variable IS used via its constructor's side effect during static initialization.
BackendRegistrant's constructor registers the CreateSyslogRecorder function pointer into
gBackendCreators[] at program startup. The variable itself doesn't need to be referenced
elsewhere - its purpose is fulfilled by the constructor's execution. This is an intentional
static registration pattern.
*/
// coverity[autosar_cpp14_a3_3_2_violation] See above
// coverity[autosar_cpp14_m0_1_3_violation] See above
// coverity[autosar_cpp14_m0_1_9_violation] See above
const BackendRegistrant kSyslogRegistrant{LogMode::kSystem, &CreateSyslogRecorder};

} // namespace
} // namespace detail
} // namespace log
} // namespace mw
} // namespace score
2 changes: 2 additions & 0 deletions score/mw/log/design/backend/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ architectural_design(
"datarouter_backend/shared_memory_reader_read.puml",
"datarouter_backend/shared_memory_writer_allocandwrite.puml",
"datarouter_backend/verbose_logging_sequence.puml",
"syslog_backend_sequence.puml",
],
static = [
"datarouter_backend/README.md",
Expand All @@ -36,6 +37,7 @@ architectural_design(
"file_output_backend.md",
"mw_log_file_backend.puml",
"mw_log_recorders.puml",
"syslog_backend.md",
],
visibility = ["//visibility:public"],
)
16 changes: 16 additions & 0 deletions score/mw/log/design/backend/mw_log_recorders.puml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ class "mw::log::detail::SlogBackend" as SlogBackend {
- Init(verbosity: std::uint8_t) : void
}

class "mw::log::detail::SyslogBackend" as SyslogBackend {
- app_id_: std::string
- buffer_: CircularAllocator<mw::log::detail::LogRecord>
- syslog_instance_: score::cpp::pmr::unique_ptr<score::os::Syslog>
__
+ SyslogBackend(const std::size_t,\n const LogRecord&,\n const std::string_view,\n score::cpp::pmr::unique_ptr<score::os::Syslog>)
+ ReserveSlot(): score::cpp::optional<SlotHandle>
+ FlushSlot(const SlotHandle&): void
+ GetLogRecord(const SlotHandle&): LogRecord&
__
- Init(): void
}

class "mw::log::detail::DataRouterBackend" as DataRouterBackend {
}

Expand Down Expand Up @@ -133,6 +146,7 @@ RecorderMock .up.|> Recorder
' Relationships - Backend interface
FileOutputBackend .up.|> Backend
SlogBackend .up.|> Backend
SyslogBackend .up.|> Backend
DataRouterBackend .up.|> Backend
BackendMock .up.|> Backend

Expand All @@ -153,6 +167,8 @@ TextRecorder ..> DLTFormat : uses
' Backend composition
SlogBackend *-- CircularAllocator
SlogBackend --> LogRecord : uses
SyslogBackend *-- CircularAllocator
SyslogBackend --> LogRecord : uses

' External dependencies
FileRecorder ..> fcntl : open\nSetNonBlocking / fctrl call
Expand Down
33 changes: 33 additions & 0 deletions score/mw/log/design/backend/syslog_backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SyslogBackend

`SyslogBackend` implements `mw::log::detail::Backend` and is the Linux counterpart
of the QNX `SlogBackend`: it is selected by `SyslogRecorderFactory` when the
configured log mode contains `kSystem` and the target platform is Linux
(`target_compatible_with = ["@platforms//os:linux"]`), wrapping it in a
`TextRecorder` the same way `SlogBackend` is wrapped on QNX.

Like `SlogBackend`, it stores `LogRecord`s in a `CircularAllocator` between
`ReserveSlot()` and `FlushSlot()`. `ReserveSlot()` and `GetLogRecord()` only
hand out and look up slots in that buffer; no data leaves the process until
`FlushSlot()` is called.

`FlushSlot()` converts the `LogRecord`'s `LogLevel` to a syslog(3) priority via
`ConvertMwLogLevelToSyslogPriority()` and forwards the record's app ID, context
ID and payload to the injected `score::os::Syslog` seam as a single
`syslog(priority, "%.*s,%.*s: %.*s", ...)` call, which glibc delivers through
`vsyslog(3)`. `LogLevel::kOff` (and any out-of-range level) maps to
`SyslogPriority::kInvalid` and is dropped instead of being forwarded, mirroring
`SlogBackend`'s handling of its own invalid level.

The `score::os::Syslog` seam is opened once, in the constructor's `Init()`, via
`openlog(app_id, LOG_PID | LOG_NDELAY, LOG_USER)`; the object-seam wrapper
(`Syslog` interface / `SyslogImpl` / `MockSyslog`) is what makes `SyslogBackend`
host-unit-testable without a real syslog daemon.

<img alt="MW_LOG_RECORDERS" src="https://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/eclipse-score/logging/refs/heads/main/score/mw/log/design/backend/mw_log_recorders.puml">

The sequence below shows a single log call from `LogStream` construction
through to the `syslog(3)` call made when the stream is destroyed and the slot
is flushed:

<img alt="SyslogBackendSequenceDesign" src="https://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/eclipse-score/logging/refs/heads/main/score/mw/log/design/backend/syslog_backend_sequence.puml">
100 changes: 100 additions & 0 deletions score/mw/log/design/backend/syslog_backend_sequence.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
@startuml syslog_backend_sequence

participant "_:App_" as App
participant "FreeFunctions" as FreeFunctions
participant "LogStreamFactory" as LogStreamFactory
participant "Runtime" as Runtime
participant "_:LogStream_" as LogStream
participant "_:TextRecorder_" as TextRecorder
participant "_:SyslogBackend_" as SyslogBackend
participant "score::os::Syslog" as Syslog

activate App

App -> FreeFunctions : score::mw::log::Info()

FreeFunctions -> LogStreamFactory : GetStream(LogLevel)

activate LogStreamFactory

LogStreamFactory -> Runtime : GetRecorder()
Runtime -> TextRecorder : creates
LogStreamFactory <-- Runtime : Recorder

LogStreamFactory -> LogStream : construct

activate LogStream

LogStream -> TextRecorder : StartRecord(ctx, LogLevel)
activate TextRecorder

TextRecorder -> SyslogBackend : ReserveSlot()
activate SyslogBackend
TextRecorder <-- SyslogBackend : score::cpp::optional<SlotHandle>
deactivate SyslogBackend

LogStream <-- TextRecorder : score::cpp::optional<SlotHandle>
deactivate TextRecorder

LogStreamFactory <-- LogStream : LogStream
deactivate LogStream

FreeFunctions <-- LogStreamFactory : LogStream
deactivate LogStreamFactory

App <-- FreeFunctions : LogStream

App -> LogStream : << Some Data

activate LogStream

LogStream -> TextRecorder : Log(SlotHandle, Data)
activate TextRecorder

TextRecorder -> SyslogBackend : GetLogRecord(SlotHandle)
activate SyslogBackend
TextRecorder <-- SyslogBackend : LogRecord&
deactivate SyslogBackend

TextRecorder -> TextRecorder : format payload\ninto LogRecord

LogStream <-- TextRecorder
deactivate TextRecorder

App <-- LogStream : LogStream
deactivate LogStream

App -> LogStream : Destruct

activate LogStream

LogStream -> TextRecorder : StopRecord(SlotHandle)
activate TextRecorder

TextRecorder -> SyslogBackend : FlushSlot(SlotHandle)
activate SyslogBackend

SyslogBackend -> Syslog : syslog(priority, "%.*s,%.*s: %.*s",\n app_id, ctx_id, payload)

Syslog -> Syslog : glibc vsyslog(3)

TextRecorder <-- SyslogBackend
deactivate SyslogBackend

LogStream <-- TextRecorder
deactivate TextRecorder

destroy LogStream

App <-- LogStream

deactivate App

note right of Syslog #red
Priority is derived from LogLevel via
ConvertMwLogLevelToSyslogPriority(). LogLevel::kOff
(and any out-of-range level) maps to kInvalid and is
not forwarded to syslog(3).
end note

@enduml
Loading
Loading