Skip to content

Commit f2f67c2

Browse files
author
Lukas Hutak
committed
JSON output: introduce Syslog component
1 parent a5103f9 commit f2f67c2

File tree

2 files changed

+407
-0
lines changed

2 files changed

+407
-0
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* \file
3+
* \author Lukas Hutak <[email protected]>
4+
* \brief Syslog output
5+
* \date 2023
6+
*/
7+
8+
/* Copyright (C) 2023 CESNET, z.s.p.o.
9+
*
10+
* Redistribution and use in source and binary forms, with or without
11+
* modification, are permitted provided that the following conditions
12+
* are met:
13+
* 1. Redistributions of source code must retain the above copyright
14+
* notice, this list of conditions and the following disclaimer.
15+
* 2. Redistributions in binary form must reproduce the above copyright
16+
* notice, this list of conditions and the following disclaimer in
17+
* the documentation and/or other materials provided with the
18+
* distribution.
19+
* 3. Neither the name of the Company nor the names of its contributors
20+
* may be used to endorse or promote products derived from this
21+
* software without specific prior written permission.
22+
*
23+
* ALTERNATIVELY, provided that this notice is retained in full, this
24+
* product may be distributed under the terms of the GNU General Public
25+
* License (GPL) version 2 or later, in which case the provisions
26+
* of the GPL apply INSTEAD OF those given above.
27+
*
28+
* This software is provided ``as is'', and any express or implied
29+
* warranties, including, but not limited to, the implied warranties of
30+
* merchantability and fitness for a particular purpose are disclaimed.
31+
* In no event shall the company or contributors be liable for any
32+
* direct, indirect, incidental, special, exemplary, or consequential
33+
* damages (including, but not limited to, procurement of substitute
34+
* goods or services; loss of use, data, or profits; or business
35+
* interruption) however caused and on any theory of liability, whether
36+
* in contract, strict liability, or tort (including negligence or
37+
* otherwise) arising in any way out of the use of this software, even
38+
* if advised of the possibility of such damage.
39+
*
40+
*/
41+
42+
#include <limits.h>
43+
#include <ctime>
44+
#include <unistd.h>
45+
#include <inttypes.h>
46+
#include <stdexcept>
47+
48+
#include <libfds/converters.h>
49+
50+
#include "Syslog.hpp"
51+
52+
/** Delay between reconnection attempts (seconds) */
53+
#define RECONN_DELAY (5)
54+
/** How ofter to report statistics (seconds) */
55+
#define STATS_DELAY (1)
56+
57+
static void
58+
get_time(timespec &ts)
59+
{
60+
if (clock_gettime(CLOCK_REALTIME_COARSE, &ts) != 0) {
61+
throw std::runtime_error("clock_gettime(CLOCK_REALTIME_COARSE) has failed");
62+
}
63+
}
64+
65+
static std::string
66+
get_hostname(syslog_hostname type)
67+
{
68+
switch (type) {
69+
case syslog_hostname::NONE:
70+
return std::string("-");
71+
case syslog_hostname::LOCAL: {
72+
char buffer[HOST_NAME_MAX + 1];
73+
int ret;
74+
75+
ret = gethostname(buffer, sizeof(buffer));
76+
if (ret < 0) {
77+
throw std::runtime_error("gethostname() has failed");
78+
}
79+
80+
buffer[HOST_NAME_MAX] = '\0';
81+
return std::string(buffer);
82+
}
83+
}
84+
85+
// Unimplemented option
86+
assert(false && "not implemented option");
87+
return "-";
88+
}
89+
90+
static void
91+
get_timestamp(const timespec &ts, char *buffer, size_t buffer_size)
92+
{
93+
const int milliseconds = (ts.tv_nsec / 1000000);
94+
struct tm timestamp;
95+
size_t size_used;
96+
int ret;
97+
98+
if (gmtime_r(&ts.tv_sec, &timestamp) == nullptr) {
99+
throw std::runtime_error("gmtime_r() has failed");
100+
}
101+
102+
size_used = strftime(buffer, buffer_size, "%FT%T", &timestamp);
103+
if (size_used == 0) {
104+
throw std::runtime_error("strftime() has failed");
105+
}
106+
107+
size_t remain_size = buffer_size - size_used;
108+
char *remain = buffer + size_used;
109+
110+
ret = snprintf(remain, remain_size, ".%03d", milliseconds);
111+
if (ret < 0 || ret >= (int) remain_size) {
112+
throw std::runtime_error("snprintf() has failed");
113+
}
114+
115+
remain_size -= ret;
116+
remain += ret;
117+
118+
if (remain_size < 2) { // We need space for 'Z' and '\0'
119+
throw std::runtime_error("get_timestamp() has failed");
120+
}
121+
122+
remain[0] = 'Z';
123+
remain[1] = '\0';
124+
}
125+
126+
/**
127+
* \brief Class constructor
128+
* \param[in] cfg Syslog configuration
129+
* \param[in] ctx Instance context
130+
*/
131+
Syslog::Syslog(struct cfg_syslog &cfg, ipx_ctx_t *ctx)
132+
: Output(cfg.name, ctx)
133+
, m_socket(std::move(cfg.transport))
134+
{
135+
timespec now;
136+
137+
m_connection_time.tv_sec = 0;
138+
m_connection_time.tv_nsec = 0;
139+
m_is_stream = (m_socket->type() == SyslogType::STREAM);
140+
m_cnt_sent = 0;
141+
m_cnt_dropped = 0;
142+
143+
prepare_hdr(cfg);
144+
get_time(now);
145+
connect(now);
146+
147+
m_stats_time = now;
148+
}
149+
150+
/** Destructor */
151+
Syslog::~Syslog()
152+
{
153+
}
154+
155+
int
156+
Syslog::process(const char *str, size_t len)
157+
{
158+
timespec now;
159+
int ret;
160+
161+
get_time(now);
162+
report_stats(now);
163+
164+
if (!m_socket->is_ready()) {
165+
// Not connected -> try to reconnect
166+
ret = connect(now);
167+
if (ret != IPX_READY) {
168+
// Just ignore the record and reconnect later
169+
++m_cnt_dropped;
170+
return IPX_OK;
171+
}
172+
}
173+
174+
ret = send(now, str, len);
175+
if (ret < 0) {
176+
std::string description = m_socket->description();
177+
const char *err_str;
178+
ipx_strerror(-ret, err_str);
179+
180+
IPX_CTX_ERROR(
181+
_ctx,
182+
"Connection to '%s' has failed: %s (%d)",
183+
description.c_str(),
184+
err_str,
185+
-ret);
186+
} else if (ret == 0) {
187+
++m_cnt_dropped;
188+
} else if (ret > 0) {
189+
++m_cnt_sent;
190+
}
191+
192+
return IPX_OK;
193+
}
194+
195+
void
196+
Syslog::prepare_hdr(const struct cfg_syslog &cfg)
197+
{
198+
const int priority = (cfg.priority.facility * 8) + cfg.priority.severity;
199+
200+
m_hdr_prio.clear();
201+
m_hdr_prio += "<";
202+
m_hdr_prio += std::to_string(priority);
203+
m_hdr_prio += ">1 ";
204+
205+
m_hdr_rest.clear();
206+
m_hdr_rest += " ";
207+
m_hdr_rest += get_hostname(cfg.hostname);
208+
m_hdr_rest += " ";
209+
m_hdr_rest += (!cfg.program.empty()) ? cfg.program : "-";
210+
m_hdr_rest += " ";
211+
m_hdr_rest += (cfg.proc_id) ? std::to_string(getpid()) : "-";
212+
m_hdr_rest += " - -"; // No MSGID and no structured data
213+
m_hdr_rest += " \xEF\xBB\xBF"; // "BOM" (for UTF-8 string)
214+
}
215+
216+
int
217+
Syslog::connect(const timespec &now)
218+
{
219+
const std::string description = m_socket->description();
220+
int ret;
221+
222+
// Try only one reconnection per second
223+
if (m_connection_time.tv_sec + RECONN_DELAY > now.tv_sec) {
224+
return IPX_ERR_LIMIT;
225+
}
226+
227+
m_connection_time = now;
228+
229+
ret = m_socket->open();
230+
if (ret < 0) {
231+
const char *err_str;
232+
ipx_strerror(-ret, err_str);
233+
IPX_CTX_WARNING(
234+
_ctx,
235+
"(Syslog output) Unable to connect to '%s': %s. Trying again in %d seconds.",
236+
description.c_str(),
237+
err_str,
238+
int(RECONN_DELAY));
239+
return IPX_OK;
240+
}
241+
242+
IPX_CTX_INFO(_ctx, "(Syslog output) Connected to '%s'.", description.c_str());
243+
return IPX_READY;
244+
}
245+
246+
int
247+
Syslog::send(const timespec &now, const char *str, size_t len)
248+
{
249+
char timestamp[128];
250+
char length[32];
251+
struct msghdr msg;
252+
struct iovec iovec[8];
253+
int iovec_idx = 0;
254+
255+
memset(&msg, 0, sizeof(msg));
256+
get_timestamp(now, timestamp, sizeof(timestamp));
257+
258+
if (m_is_stream) {
259+
// Add syslog message length before syslog header (will be filled later)
260+
iovec[iovec_idx].iov_base = length;
261+
iovec_idx++;
262+
}
263+
264+
iovec[iovec_idx].iov_base = (void *) m_hdr_prio.data();
265+
iovec[iovec_idx].iov_len = m_hdr_prio.size();
266+
iovec_idx++;
267+
268+
iovec[iovec_idx].iov_base = timestamp;
269+
iovec[iovec_idx].iov_len = strlen(timestamp);
270+
iovec_idx++;
271+
272+
iovec[iovec_idx].iov_base = (void *) m_hdr_rest.data();
273+
iovec[iovec_idx].iov_len = m_hdr_rest.size();
274+
iovec_idx++;
275+
276+
iovec[iovec_idx].iov_base = (void *) str;
277+
iovec[iovec_idx].iov_len = len;
278+
iovec_idx++;
279+
280+
if (m_is_stream) {
281+
// Fill real syslog message length
282+
size_t length_size;
283+
uint32_t sum = 0;
284+
285+
for (int i = 1; i < iovec_idx; ++i) {
286+
sum += iovec[i].iov_len;
287+
}
288+
289+
// Convert number to string using very fast libfds function
290+
sum = htonl(sum);
291+
if (fds_uint2str_be(&sum, sizeof(sum), length, sizeof(length)) < 0) {
292+
throw "fds_uint2str_be() has failed";
293+
}
294+
295+
length_size = strlen(length);
296+
length[length_size++] = ' ';
297+
iovec[0].iov_len = length_size;
298+
}
299+
300+
msg.msg_iov = iovec;
301+
msg.msg_iovlen = iovec_idx;
302+
303+
return m_socket->write(&msg);
304+
}
305+
306+
void
307+
Syslog::report_stats(const timespec &now)
308+
{
309+
if (m_stats_time.tv_sec + STATS_DELAY > now.tv_sec) {
310+
return;
311+
}
312+
313+
m_stats_time = now;
314+
315+
IPX_CTX_INFO(
316+
_ctx,
317+
"STATS: sent: %" PRIu64 ", dropped: %" PRIu64,
318+
m_cnt_sent,
319+
m_cnt_dropped);
320+
321+
m_cnt_sent = 0;
322+
m_cnt_dropped = 0;
323+
}

0 commit comments

Comments
 (0)