Skip to content

Commit c8359cd

Browse files
feat: Added batch logging to the linelog module
Signed-off-by: ethan-thompson <ethan.thompson@networkradius.com>
1 parent 4924888 commit c8359cd

File tree

8 files changed

+763
-73
lines changed

8 files changed

+763
-73
lines changed

src/modules/rlm_linelog/all.mk

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
TARGETNAME := rlm_linelog
22

33
TARGET := $(TARGETNAME)$(L)
4-
SOURCES := $(TARGETNAME).c
4+
SOURCES := $(TARGETNAME).c \
5+
file.c
56

67
LOG_ID_LIB = 27

src/modules/rlm_linelog/file.c

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
/*
2+
* file.c
3+
*
4+
* Version: $Id$
5+
*
6+
* This program is free software; you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation; either version 2 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19+
*
20+
* @copyright 2025 Network RADIUS SAS (legal@networkradius.com)
21+
*/
22+
23+
RCSID("$Id$")
24+
25+
#include <freeradius-devel/util/iovec.h>
26+
27+
#include <sys/stat.h>
28+
29+
#include "file.h"
30+
31+
static uint32_t filename_hash(void const *data)
32+
{
33+
rlm_linelog_file_t const *file = data;
34+
35+
return fr_hash_case_string(file->filename);
36+
}
37+
38+
static int8_t filename_cmp(void const *one, void const *two)
39+
{
40+
rlm_linelog_file_t const *a = one;
41+
rlm_linelog_file_t const *b = two;
42+
43+
return CMP(strcmp(a->filename, b->filename), 0);
44+
}
45+
46+
unlang_action_t file_batching_mod_resume(UNUSED unlang_result_t *p_result, module_ctx_t const *mctx, request_t *request)
47+
{
48+
rlm_linelog_file_entry_t *entry = (rlm_linelog_file_entry_t *)mctx->rctx;
49+
50+
if (entry->failed) {
51+
REDEBUG("Write failed with error %s", fr_syserror(entry->error));
52+
return UNLANG_ACTION_FAIL;
53+
}
54+
55+
return UNLANG_ACTION_EXECUTE_NEXT;
56+
}
57+
58+
xlat_action_t file_batching_xlat_resume(TALLOC_CTX *ctx, fr_dcursor_t *out, xlat_ctx_t const *xctx,
59+
request_t *request, UNUSED fr_value_box_list_t *in)
60+
{
61+
fr_value_box_t *wrote;
62+
rlm_linelog_file_entry_t *entry = (rlm_linelog_file_entry_t *)xctx->rctx;
63+
64+
if (entry->failed) {
65+
REDEBUG("Write failed with error %s", fr_syserror(entry->error));
66+
return XLAT_ACTION_FAIL;
67+
}
68+
69+
MEM(wrote = fr_value_box_alloc(ctx, FR_TYPE_SIZE, NULL));
70+
wrote->vb_size = entry->data_len;
71+
72+
fr_dcursor_insert(out, wrote);
73+
74+
return XLAT_ACTION_DONE;
75+
}
76+
77+
static inline CC_HINT(always_inline) void _batching_handle_signal(NDEBUG_UNUSED request_t *request, NDEBUG_UNUSED fr_signal_t action, rlm_linelog_file_entry_t *entry)
78+
{
79+
fr_assert(action == FR_SIGNAL_CANCEL);
80+
fr_assert(entry->request == request);
81+
82+
entry->request = NULL;
83+
}
84+
85+
void file_batching_mod_handle_signal(module_ctx_t const *mctx, request_t *request, fr_signal_t action)
86+
{
87+
_batching_handle_signal(request, action, (rlm_linelog_file_entry_t *)mctx->rctx);
88+
}
89+
90+
void file_batching_xlat_handle_signal(xlat_ctx_t const *xctx, request_t *request, fr_signal_t action)
91+
{
92+
_batching_handle_signal(request, action, (rlm_linelog_file_entry_t *)xctx->rctx);
93+
}
94+
95+
static int _file_free(rlm_linelog_file_t *file)
96+
{
97+
fr_hash_table_delete(file->thread_inst->file_table, file);
98+
talloc_free(file);
99+
100+
return 0;
101+
}
102+
103+
static void _batching_cleanup_timer(UNUSED fr_timer_list_t *tl, UNUSED fr_time_t now, void *uctx)
104+
{
105+
talloc_free(uctx);
106+
}
107+
108+
static inline CC_HINT(always_inline) void batching_mark_entries_failed(rlm_linelog_file_t *file, rlm_linelog_file_entry_t *start_entry, int error)
109+
{
110+
rlm_linelog_file_entry_t *entry;
111+
112+
for (entry = start_entry; entry < file->entry_p; entry++) {
113+
entry->failed = true;
114+
entry->error = error;
115+
116+
if (unlang_interpret_is_resumable(entry->request)) {
117+
unlang_interpret_mark_runnable(entry->request);
118+
}
119+
}
120+
}
121+
122+
static void _batch_write(rlm_linelog_file_t *file)
123+
{
124+
int ret, written, fd = -1;
125+
bool with_delim;
126+
size_t len = 0, header_len = 0;
127+
off_t offset, file_size;
128+
int write_error = 0;
129+
struct stat stat_buf;
130+
struct iovec to_write[3];
131+
struct iovec *to_write_p = to_write;
132+
rlm_linelog_t const *inst = talloc_get_type_abort_const(file->mod_inst, rlm_linelog_t);
133+
134+
fr_assert(inst->file.buffer_write);
135+
fr_assert(file);
136+
fr_assert(file->filename);
137+
138+
if (file->write) {
139+
FR_TIMER_DISARM(file->write);
140+
}
141+
142+
with_delim = (inst->delimiter_len > 0);
143+
144+
fd = exfile_open(inst->file.ef, file->filename, inst->file.permissions, 0, &offset);
145+
if (fd < 0) {
146+
PERROR("Failed to open %s", file->filename);
147+
error:
148+
batching_mark_entries_failed(file, file->entry, errno);
149+
goto done;
150+
}
151+
152+
if (inst->file.group_str && (chown(file->filename, -1, inst->file.group) == -1)) {
153+
PERROR("Unable to change system group of \"%s\": %s", file->filename, fr_syserror(errno));
154+
goto error;
155+
}
156+
157+
/*
158+
* If a header format is defined and we are at the beginning
159+
* of the file then expand the format and write it out before
160+
* writing the actual log entries.
161+
*/
162+
if (!fr_type_is_null(file->log_header->type) && (offset == 0)) {
163+
to_write_p->iov_base = UNCONST(char *, file->log_header->vb_strvalue);
164+
header_len = to_write_p->iov_len = file->log_header->vb_length;
165+
to_write_p++;
166+
167+
if (with_delim) {
168+
to_write_p->iov_base = UNCONST(char *, inst->delimiter);
169+
header_len += to_write_p->iov_len = inst->delimiter_len;
170+
to_write_p++;
171+
}
172+
}
173+
174+
to_write_p->iov_base = fr_dbuff_start(&file->dbuff);
175+
to_write_p->iov_len = fr_dbuff_used(&file->dbuff);
176+
to_write_p++;
177+
178+
if (fstat(fd, &stat_buf) < 0) {
179+
ERROR("Failed to stat file %s: %s", file->filename, fr_syserror(errno));
180+
goto error;
181+
}
182+
file_size = stat_buf.st_size;
183+
184+
errno = 0;
185+
ret = fr_writev(fd, to_write, to_write_p - to_write, fr_time_delta_from_sec(0));
186+
written = ret - header_len;
187+
write_error = errno;
188+
if (ret >= 0) {
189+
if (ret < (ssize_t)(header_len)) {
190+
ret = ftruncate(fd, 0);
191+
if (ret < 0) {
192+
PERROR("Failed truncating file \"%s\" after partial header write: %s", file->filename, fr_syserror(errno));
193+
}
194+
195+
PERROR("Failed writing header to \"%s\"", file->filename);
196+
goto error;
197+
}
198+
}
199+
else {
200+
/*
201+
* Check for partial write
202+
*/
203+
if (fstat(fd, &stat_buf) < 0) {
204+
ERROR("Failed to stat file %s: %s", file->filename, fr_syserror(errno));
205+
goto error;
206+
}
207+
208+
written = stat_buf.st_size - file_size - header_len;
209+
210+
if ((errno == ENOSPC) && (written > 0)) {
211+
ERROR("No space left on device when writing to \"%s\". Not all data was written", file->filename);
212+
}
213+
else {
214+
ERROR("Failed writing to \"%s\": %s", file->filename, fr_syserror(errno));
215+
goto error;
216+
}
217+
}
218+
219+
if (fsync(fd) < 0) {
220+
ERROR("Failed syncing \"%s\" to persistent storage: %s", file->filename, fr_syserror(errno));
221+
goto error;
222+
}
223+
224+
for (rlm_linelog_file_entry_t *entry = file->entry; entry < file->entry_p; entry++) {
225+
len += entry->data_len;
226+
227+
if (len > (size_t)written) {
228+
request_t *request = entry->request;
229+
ROPTIONAL(RWARN, WARN, "Buffered log write failed. Expected %zu bytes, but only %zu bytes were written", len, (size_t)written);
230+
ret = ftruncate(fd, offset + (len - entry->data_len + header_len));
231+
if (ret < 0) {
232+
PERROR("Failed truncating file \"%s\" after partial write: %s", file->filename, fr_syserror(errno));
233+
}
234+
235+
batching_mark_entries_failed(file, entry, write_error);
236+
break;
237+
}
238+
239+
/*
240+
* Needed because the write function could be called either from a timer or directly when the buffer is full
241+
* in which case the last entry would not have been yielded
242+
*/
243+
if (unlang_interpret_is_resumable(entry->request)) {
244+
unlang_interpret_mark_runnable(entry->request);
245+
}
246+
}
247+
248+
done:
249+
if (exfile_close(inst->file.ef, fd) < 0) {
250+
PERROR("Failed closing file %s", file->filename);
251+
}
252+
253+
fr_dbuff_reset_talloc(&file->dbuff);
254+
file->entry_p = file->entry;
255+
256+
if (fr_time_delta_gt(inst->file.buffer_expiry, fr_time_delta_wrap(0))) {
257+
fr_timer_in(file,file->thread_inst->tl, &file->expiry, inst->file.buffer_expiry, false, _batching_cleanup_timer, file);
258+
}
259+
}
260+
261+
static void _batching_handle_timeout(UNUSED fr_timer_list_t *tl, UNUSED fr_time_t now, void *uctx)
262+
{
263+
_batch_write((rlm_linelog_file_t *)uctx);
264+
}
265+
266+
linelog_buffer_action_t file_enqueue_write(rlm_linelog_file_entry_t **entry_p, module_ctx_t const *mctx, linelog_call_env_t const *call_env, request_t *request, struct iovec *vector_p, size_t vector_len)
267+
{
268+
int ret;
269+
char const *path;
270+
rlm_linelog_file_t *file;
271+
rlm_linelog_thread_t *thread = talloc_get_type_abort(mctx->thread, rlm_linelog_thread_t);
272+
rlm_linelog_t const *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_linelog_t);
273+
274+
fr_assert(inst->file.buffer_write);
275+
276+
path = call_env->filename->vb_strvalue;
277+
278+
file = fr_hash_table_find_by_key(thread->file_table, fr_hash_case_string(path), &(rlm_linelog_file_t){ .filename = path });
279+
280+
if (!file) {
281+
file = talloc_size(mctx->thread, sizeof(rlm_linelog_file_t) + (sizeof(rlm_linelog_file_entry_t) * inst->file.buffer_count));
282+
talloc_set_name_const(file, "rlm_linelog_file_t");
283+
file->filename = talloc_strdup(file, path);
284+
file->log_header = fr_value_box_alloc_null(file);
285+
file->mod_inst = inst;
286+
file->thread_inst = thread;
287+
if (call_env->log_head && fr_value_box_copy(file, file->log_header, call_env->log_head) < 0) {
288+
RERROR("Failed to copy log header for buffered log file %pV", call_env->filename);
289+
error:
290+
talloc_free(file);
291+
return LINELOG_BUFFER_WRITE_FAIL;
292+
}
293+
file->entry_p = file->entry;
294+
file->entry_last = file->entry + inst->file.buffer_count;
295+
file->write = NULL;
296+
file->expiry = NULL;
297+
fr_dbuff_init_talloc(file, &file->dbuff, &file->tctx, 1024, SIZE_MAX);
298+
299+
if (!fr_hash_table_insert(thread->file_table, file)) {
300+
RERROR("Failed tracking buffered log file %pV", call_env->filename);
301+
goto error;
302+
}
303+
304+
talloc_set_destructor(file, _file_free);
305+
}
306+
307+
if (file->expiry) {
308+
FR_TIMER_DISARM(file->expiry);
309+
}
310+
311+
fr_assert(file->entry_p < file->entry_last);
312+
*file->entry_p = (rlm_linelog_file_entry_t){ .request = request, .file = file, .failed = false, .data_len = 0, .error = 0 };
313+
ret = fr_concatv(&file->dbuff, vector_p, vector_len);
314+
315+
if (ret < 0) {
316+
RERROR("Failed to buffer log entry for %pV", call_env->filename);
317+
file->entry_p->failed = true;
318+
return LINELOG_BUFFER_WRITE_FAIL;
319+
}
320+
321+
*entry_p = file->entry_p;
322+
323+
if (fr_time_delta_gt(inst->file.buffer_delay, fr_time_delta_wrap(0)) && !fr_timer_armed(file->write) ) {
324+
fr_timer_in(file, file->thread_inst->tl, &file->write, inst->file.buffer_delay, false, _batching_handle_timeout, file);
325+
}
326+
327+
file->entry_p->data_len = ret;
328+
file->entry_p++;
329+
330+
if (file->entry_p == file->entry_last) {
331+
_batch_write(file);
332+
333+
return LINELOG_BUFFER_WRITE_DONE;
334+
}
335+
336+
return LINELOG_BUFFER_WRITE_YIELD;
337+
}
338+
339+
void CC_HINT(nonnull) file_thread_init(rlm_linelog_thread_t *thread, fr_timer_list_t *tl)
340+
{
341+
thread->file_table = fr_hash_table_alloc(thread, filename_hash, filename_cmp, NULL);
342+
thread->tl = tl;
343+
}
344+
345+
void CC_HINT(nonnull) file_thread_detach(rlm_linelog_thread_t *thread)
346+
{
347+
fr_hash_iter_t iter;
348+
rlm_linelog_file_t *file = NULL;
349+
350+
for (file = fr_hash_table_iter_init(thread->file_table, &iter); file;
351+
file = fr_hash_table_iter_next(thread->file_table, &iter)) {
352+
talloc_free(file);
353+
}
354+
355+
talloc_free(thread->file_table);
356+
}

0 commit comments

Comments
 (0)