Skip to content

Commit dbfa135

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

File tree

8 files changed

+777
-74
lines changed

8 files changed

+777
-74
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 2004,2006 The FreeRADIUS server project
21+
* @copyright 2025 Ethan Thompson (ethan.thompson@inkbridge.io)
22+
*/
23+
24+
RCSID("$Id$")
25+
26+
#include <freeradius-devel/util/iovec.h>
27+
28+
#include "file.h"
29+
30+
uint32_t filename_hash(void const *data)
31+
{
32+
rlm_linelog_file_t const *file = data;
33+
34+
return fr_hash_case_string(file->filename);
35+
}
36+
37+
int8_t filename_cmp(void const *one, void const *two)
38+
{
39+
rlm_linelog_file_t const *a = one;
40+
rlm_linelog_file_t const *b = two;
41+
42+
return strcmp(a->filename, b->filename);
43+
}
44+
45+
unlang_action_t batching_mod_resume(UNUSED unlang_result_t *p_result, module_ctx_t const *mctx, request_t *request)
46+
{
47+
rlm_linelog_file_entry_t *entry = (rlm_linelog_file_entry_t *)mctx->rctx;
48+
49+
if (entry->failed) {
50+
RDEBUG2("Write failed");
51+
return UNLANG_ACTION_FAIL;
52+
}
53+
54+
return UNLANG_ACTION_EXECUTE_NEXT;
55+
}
56+
57+
xlat_action_t batching_xlat_resume(TALLOC_CTX *ctx, fr_dcursor_t *out, xlat_ctx_t const *xctx,
58+
request_t *request, UNUSED fr_value_box_list_t *in)
59+
{
60+
fr_value_box_t *wrote;
61+
rlm_linelog_file_entry_t *entry = (rlm_linelog_file_entry_t *)xctx->rctx;
62+
63+
if (entry->failed) {
64+
RDEBUG2("Write failed");
65+
return XLAT_ACTION_FAIL;
66+
}
67+
68+
MEM(wrote = fr_value_box_alloc(ctx, FR_TYPE_SIZE, NULL));
69+
wrote->vb_size = entry->data_len;
70+
71+
fr_dcursor_insert(out, wrote);
72+
73+
return XLAT_ACTION_DONE;
74+
}
75+
76+
static inline CC_HINT(always_inline) void _batching_handle_signal(request_t *request, fr_signal_t action, rlm_linelog_file_entry_t *entry)
77+
{
78+
fr_assert(action == FR_SIGNAL_CANCEL);
79+
fr_assert(entry->request == request);
80+
81+
RDEBUG("Request associated with buffered log has been cancelled");
82+
entry->request = NULL;
83+
}
84+
85+
void 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 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+
inline CC_HINT(always_inline) void batching_cleanup(rlm_linelog_file_t *file)
96+
{
97+
if (file->write) {
98+
fr_timer_delete(&file->write);
99+
}
100+
101+
if (file->expiry) {
102+
fr_timer_delete(&file->expiry);
103+
}
104+
105+
file->total_data_len = 0;
106+
talloc_free(file->log_header);
107+
108+
fr_hash_table_delete(file->thread_inst->file_table, file);
109+
talloc_free(file);
110+
}
111+
112+
static void _batching_cleanup_timer(UNUSED fr_timer_list_t *tl, UNUSED fr_time_t now, void *uctx)
113+
{
114+
batching_cleanup(talloc_get_type_abort(uctx, rlm_linelog_file_t));
115+
}
116+
117+
static inline CC_HINT(always_inline) void _batching_mark_entries_failed(rlm_linelog_file_t *file, rlm_linelog_file_entry_t *start_entry)
118+
{
119+
rlm_linelog_file_entry_t *entry;
120+
121+
for (entry = start_entry; entry < file->entry_last; entry++) {
122+
entry->failed = true;
123+
}
124+
}
125+
126+
static void _batch_write(rlm_linelog_file_t *file)
127+
{
128+
int ret, fd = -1;
129+
bool with_delim, failed = false;
130+
char *p;
131+
size_t len = 0;
132+
off_t offset;
133+
rlm_linelog_t const *inst = talloc_get_type_abort_const(file->mod_inst, rlm_linelog_t);
134+
135+
fr_assert(inst->file.buffer_write);
136+
fr_assert(file);
137+
fr_assert(file->filename);
138+
139+
if (file->write) {
140+
FR_TIMER_DISARM(file->write);
141+
}
142+
143+
with_delim = (inst->delimiter_len > 0);
144+
145+
/* check path and eventually create subdirs */
146+
p = strrchr(file->filename, '/');
147+
if (p) {
148+
*p = '\0';
149+
if (fr_mkdir(NULL, file->filename, -1, 0700, NULL, NULL) < 0) {
150+
PERROR("Failed to create directory %s", file->filename);
151+
152+
error:
153+
_batching_mark_entries_failed(file, file->entry);
154+
failed = true;
155+
goto done;
156+
}
157+
*p = '/';
158+
}
159+
160+
fd = exfile_open(inst->file.ef, file->filename, inst->file.permissions, 0, &offset);
161+
if (fd < 0) {
162+
PERROR("Failed to open %s", file->filename);
163+
goto error;
164+
}
165+
166+
if (inst->file.group_str && (chown(file->filename, -1, inst->file.group) == -1)) {
167+
PWARN("Unable to change system group of \"%s\"", file->filename);
168+
}
169+
170+
/*
171+
* If a header format is defined and we are at the beginning
172+
* of the file then expand the format and write it out before
173+
* writing the actual log entries.
174+
*/
175+
if (!fr_type_is_null(file->log_header->type) && (offset == 0)) {
176+
struct iovec head_vector_s[2];
177+
size_t head_vector_len;
178+
179+
memcpy(&head_vector_s[0].iov_base, &file->log_header->vb_strvalue, sizeof(head_vector_s[0].iov_base));
180+
head_vector_s[0].iov_len = file->log_header->vb_length;
181+
182+
if (!with_delim) {
183+
head_vector_len = 1;
184+
} else {
185+
memcpy(&head_vector_s[1].iov_base, &(inst->delimiter),sizeof(head_vector_s[1].iov_base));
186+
head_vector_s[1].iov_len = inst->delimiter_len;
187+
head_vector_len = 2;
188+
}
189+
190+
if (writev(fd, &head_vector_s[0], head_vector_len) < 0) {
191+
PERROR("Failed writing to \"%s\"", file->filename);
192+
/* Assert on the extra fatal errors */
193+
fr_assert((errno != EINVAL) && (errno != EFAULT));
194+
195+
close_and_error:
196+
if (exfile_close(inst->file.ef, fd) < 0) {
197+
PERROR("Failed closing file %s", file->filename);
198+
}
199+
goto error;
200+
}
201+
if (inst->file.fsync && (fsync(fd) < 0)) {
202+
PERROR("Failed syncing \"%s\" to persistent storage", file->filename);
203+
goto close_and_error;
204+
}
205+
}
206+
207+
fr_dbuff_set_end(&file->dbuff, fr_dbuff_current(&file->dbuff));
208+
fr_dbuff_set_to_start(&file->dbuff);
209+
do {
210+
ret = write(fd, fr_dbuff_current(&file->dbuff), fr_dbuff_remaining(&file->dbuff));
211+
if (ret > 0) {
212+
fr_dbuff_advance(&file->dbuff, ret);
213+
}
214+
} while ((fr_dbuff_remaining(&file->dbuff) && (ret > 0)) || ((ret < 0) && ((errno == EINTR) || (errno == EAGAIN))));
215+
216+
if (fr_dbuff_remaining(&file->dbuff) > 0) {
217+
// Check if no space left on device but some data was written (move to truncate later)
218+
if ((errno == ENOSPC) && ((size_t)fr_dbuff_used(&file->dbuff) > 0)) {
219+
PWARN("No space left on device when writing to \"%s\"", file->filename);
220+
} else {
221+
ERROR("Failed writing to \"%s\": %s", file->filename, fr_syserror(errno));
222+
goto error;
223+
}
224+
}
225+
226+
if (fsync(fd) < 0) {
227+
ERROR("Failed syncing \"%s\" to persistent storage: %s", file->filename, fr_syserror(errno));
228+
failed = true;
229+
goto error;
230+
}
231+
232+
// If we didn't write all data, keep the file open for truncation later
233+
if ((size_t)fr_dbuff_used(&file->dbuff) == file->total_data_len) {
234+
if (exfile_close(inst->file.ef, fd) < 0) {
235+
PERROR("Failed closing file %s", file->filename);
236+
}
237+
}
238+
239+
done:
240+
for (rlm_linelog_file_entry_t *entry = file->entry; entry < file->entry_p; entry++) {
241+
if (failed) {
242+
write_failed:
243+
entry->failed = true;
244+
goto mark_resume;
245+
}
246+
247+
len += entry->data_len;
248+
249+
if (len > fr_dbuff_used(&file->dbuff)) {
250+
request_t *request = entry->request;
251+
ROPTIONAL(RWARN, WARN, "Buffered log write failed. Expected %zu bytes, but only %zu bytes were written", len, fr_dbuff_used(&file->dbuff));
252+
ftruncate(fd, offset + (len - entry->data_len));
253+
if (exfile_close(inst->file.ef, fd) < 0) {
254+
PERROR("Failed closing file %s after truncation", file->filename);
255+
}
256+
goto write_failed;
257+
}
258+
mark_resume:
259+
/*
260+
* Needed because the write function could be called either from a timer or directly when the buffer is full
261+
* in which case the last entry would not have been yielded
262+
*/
263+
if (unlang_interpret_is_resumable(entry->request)) {
264+
unlang_interpret_mark_runnable(entry->request);
265+
}
266+
}
267+
268+
// Reset the buffer for future use
269+
fr_dbuff_reset_talloc(&file->dbuff);
270+
file->entry_p = file->entry;
271+
file->total_data_len = 0;
272+
273+
// Set up expiry timer
274+
if (fr_time_delta_gt(inst->file.buffer_expiry, fr_time_delta_wrap(0))) {
275+
fr_timer_in(file,file->thread_inst->tl, &file->expiry, inst->file.buffer_expiry, false, _batching_cleanup_timer, file);
276+
}
277+
}
278+
279+
static void _batching_handle_timeout(UNUSED fr_timer_list_t *tl, UNUSED fr_time_t now, void *uctx)
280+
{
281+
_batch_write((rlm_linelog_file_t *)uctx);
282+
}
283+
284+
linelog_buffer_action_t batch_update(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)
285+
{
286+
int ret;
287+
char const *path;
288+
rlm_linelog_file_t *file;
289+
rlm_linelog_thread_t *thread = talloc_get_type_abort(mctx->thread, rlm_linelog_thread_t);
290+
rlm_linelog_t const *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_linelog_t);
291+
292+
fr_assert(inst->file.buffer_write);
293+
294+
path = call_env->filename->vb_strvalue;
295+
296+
file = fr_hash_table_find_by_key(thread->file_table, fr_hash_case_string(path), &(rlm_linelog_file_t){ .filename = path });
297+
298+
if (!file) {
299+
file = talloc_size(mctx->thread, sizeof(rlm_linelog_file_t) + (sizeof(rlm_linelog_file_entry_t) * inst->file.buffer_count));
300+
talloc_set_name_const(file, "rlm_linelog_file_t");
301+
file->filename = talloc_strdup(file, path);
302+
file->log_header = fr_value_box_alloc_null(file);
303+
file->mod_inst = inst;
304+
file->thread_inst = thread;
305+
if (call_env->log_head && fr_value_box_copy(file, file->log_header, call_env->log_head) < 0) {
306+
RERROR("Failed to copy log header for buffered log file %pV", call_env->filename);
307+
error:
308+
talloc_free(file);
309+
return LINELOG_BUFFER_WRITE_FAIL;
310+
}
311+
file->entry_p = file->entry;
312+
file->entry_last = file->entry + inst->file.buffer_count;
313+
file->write = NULL;
314+
file->expiry = NULL;
315+
file->total_data_len = 0;
316+
fr_dbuff_init_talloc(file, &file->dbuff, &file->tctx, 1024, SIZE_MAX);
317+
318+
if (!fr_hash_table_insert(thread->file_table, file)) {
319+
RERROR("Failed tracking buffered log file %pV", call_env->filename);
320+
goto error;
321+
}
322+
}
323+
324+
if (file->expiry) {
325+
FR_TIMER_DISARM(file->expiry);
326+
}
327+
328+
fr_assert(file->entry_p < file->entry_last);
329+
*file->entry_p = (rlm_linelog_file_entry_t){ .request = request, .file = file, .failed = false, .data_len = 0 };
330+
ret = fr_concatv(&file->dbuff, vector_p, vector_len);
331+
332+
if (ret < 0) {
333+
RERROR("Failed to buffer log entry for %pV", call_env->filename);
334+
file->entry_p->failed = true;
335+
return LINELOG_BUFFER_WRITE_FAIL;
336+
}
337+
338+
*entry_p = file->entry_p;
339+
340+
// Set the timer to write out after delay
341+
if (fr_time_delta_gt(inst->file.buffer_delay, fr_time_delta_wrap(0)) && !fr_timer_armed(file->write) ) {
342+
fr_timer_in(file, file->thread_inst->tl, &file->write, inst->file.buffer_delay, false, _batching_handle_timeout, file);
343+
}
344+
345+
file->entry_p->data_len = ret;
346+
file->total_data_len += ret;
347+
file->entry_p++;
348+
349+
if (file->entry_p == file->entry_last) {
350+
_batch_write(file);
351+
352+
return LINELOG_BUFFER_WRITE_DONE;
353+
}
354+
355+
return LINELOG_BUFFER_WRITE_YIELD;
356+
}

0 commit comments

Comments
 (0)