ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
syslog.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file syslog.cpp
6 * @brief RFC 5424 syslog client: line formatter + UDP send.
7 */
8
10
11#if PC_ENABLE_SYSLOG
12
14#include <stdio.h>
15#include <string.h>
16
17// All syslog client state, owned by one instance (internal linkage): the collector endpoint,
18// the host/app identity, the facility, the ready flag, and the format scratch (all BSS, no
19// heap), grouped so it is one named owner, unreachable from any other translation unit.
20struct SyslogCtx
21{
22 char server_ip[16] = {0}; // "255.255.255.255" + NUL
23 uint16_t port = PC_SYSLOG_DEFAULT_PORT;
24 char hostname[PC_SYSLOG_FIELD_MAX] = {0};
25 char appname[PC_SYSLOG_FIELD_MAX] = {0};
26 SyslogFacility facility = SyslogFacility::SYSLOG_FAC_LOCAL0;
27 bool ready = false;
28 char buf[PC_SYSLOG_MSG_MAX];
29};
30static SyslogCtx s_syslog;
31
32static void copy_field(char *dst, size_t cap, const char *src)
33{
34 if (!src || !src[0])
35 {
36 dst[0] = '\0';
37 return;
38 }
39 strncpy(dst, src, cap - 1);
40 dst[cap - 1] = '\0';
41}
42
43void pc_syslog_init(const char *server_ip, uint16_t port, const char *hostname, const char *appname,
44 SyslogFacility facility)
45{
46 copy_field(s_syslog.server_ip, sizeof(s_syslog.server_ip), server_ip);
47 s_syslog.port = port;
48 copy_field(s_syslog.hostname, sizeof(s_syslog.hostname), hostname);
49 copy_field(s_syslog.appname, sizeof(s_syslog.appname), appname);
50 s_syslog.facility = facility;
51 s_syslog.ready = (s_syslog.server_ip[0] != '\0');
52}
53
54// Append `len` bytes at *pos if the line still leaves room for a trailing NUL (content fits in cap-1).
55// Returns false the moment it would not fit, so an oversized line reports 0 (never a truncated datagram).
56static inline bool sl_append(char *out, size_t cap, size_t *pos, const char *src, size_t len)
57{
58 if (*pos + len > cap - 1)
59 {
60 return false;
61 }
62 memcpy(out + *pos, src, len);
63 *pos += len;
64 return true;
65}
66
67size_t pc_syslog_format(char *out, size_t cap, SyslogFacility facility, SyslogSeverity severity, const char *hostname,
68 const char *appname, const char *msg)
69{
70 if (!out || cap == 0)
71 {
72 return 0;
73 }
74 // RFC 5424 6.2.1: PRIVAL = facility*8 + severity, range 0..191. The numeric priority is the wire
75 // encoding of the level pair, so cast to int here (the boundary). facility/severity are unsigned
76 // enums, so PRI is never negative; clamp only the top so an over-range value cast in by the caller
77 // can never emit a malformed PRI (e.g. <400>).
78 int pri = (int)facility * 8 + (int)severity;
79 if (pri > 191)
80 {
81 pri = 191;
82 }
83 const char *h = (hostname && hostname[0]) ? hostname : "-";
84 const char *a = (appname && appname[0]) ? appname : "-";
85 const char *m = msg ? msg : "";
86 // <PRI>VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID SP SD SP MSG, with
87 // TIMESTAMP/PROCID/MSGID/STRUCTURED-DATA as NILVALUE ("-"). A branchless memcpy framer (fixed spans +
88 // strnlen/memcpy of each field + a small decimal PRI writer) instead of snprintf("%d ... %s %s ... %s"),
89 // which avoids the expensive Xtensa vsnprintf path on this per-log-line hot op. Byte-identical output.
90 char prib[3]; // PRI is 0..191 -> 1..3 decimal digits
91 size_t pl = 0;
92 if (pri >= 100)
93 {
94 prib[pl++] = (char)('0' + pri / 100);
95 }
96 if (pri >= 10)
97 {
98 prib[pl++] = (char)('0' + (pri / 10) % 10);
99 }
100 prib[pl++] = (char)('0' + pri % 10);
101
102 // Bounded lengths: a field can never exceed the output buffer (an over-long field makes the append
103 // below fail and the whole line report 0), and strnlen never reads past `cap` even if a field is not
104 // NUL-terminated within it.
105 size_t pos = 0;
106 if (!sl_append(out, cap, &pos, "<", 1) || !sl_append(out, cap, &pos, prib, pl) ||
107 !sl_append(out, cap, &pos, ">1 - ", 5) || !sl_append(out, cap, &pos, h, strnlen(h, cap)) ||
108 !sl_append(out, cap, &pos, " ", 1) || !sl_append(out, cap, &pos, a, strnlen(a, cap)) ||
109 !sl_append(out, cap, &pos, " - - - ", 7) || !sl_append(out, cap, &pos, m, strnlen(m, cap)))
110 {
111 return 0;
112 }
113 out[pos] = '\0'; // pos <= cap-1 by construction
114 return pos;
115}
116
117bool pc_syslog_log(SyslogSeverity severity, const char *msg)
118{
119 if (!s_syslog.ready)
120 {
121 return false;
122 }
123 size_t n = pc_syslog_format(s_syslog.buf, sizeof(s_syslog.buf), s_syslog.facility, severity, s_syslog.hostname,
124 s_syslog.appname, msg);
125 if (n == 0)
126 {
127 return false;
128 }
129 return pc_udp_sendto(s_syslog.server_ip, s_syslog.port, (const uint8_t *)s_syslog.buf, n);
130}
131
132#endif // PC_ENABLE_SYSLOG
#define PC_SYSLOG_MSG_MAX
Maximum formatted syslog datagram length in bytes (RFC 5424 line).
#define PC_SYSLOG_FIELD_MAX
Maximum syslog HOSTNAME / APP-NAME field length (including NUL).
#define PC_SYSLOG_DEFAULT_PORT
Default syslog collector UDP port (RFC 5426 well-known 514; overridable at runtime via pc_syslog_init...
Zero-heap RFC 5424 syslog client over UDP.
bool pc_udp_sendto(const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a UDP datagram to an arbitrary destination (outbound client).
Definition udp.cpp:358
Layer 4 (Transport) - connectionless UDP datagram service.