/*! \file logging_file.c * File & stderr logging support code. */ /* * (C) 2025-2026 by sysmocom - s.f.m.c. GmbH * (C) 2008-2010 by Harald Welte * (C) 2008 by Holger Hans Peter Freyther * All Rights Reserved * * SPDX-License-Identifier: GPL-2.0+ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ /*! \addtogroup logging * @{ * \file logging_file.c */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include /* maximum number of log statements we queue in file/stderr target write queue */ #define LOG_WQUEUE_LEN 156 /*! close and re-open a log file (for log file rotation) * \param[in] target log target to re-open * \returns 0 in case of success; negative otherwise */ int log_target_file_reopen(struct log_target *target) { struct osmo_wqueue *wq; int rc; OSMO_ASSERT(target->type == LOG_TGT_TYPE_FILE || target->type == LOG_TGT_TYPE_STDERR); OSMO_ASSERT(target->tgt_file.out || target->tgt_file.wqueue); if (target->type == LOG_TGT_TYPE_STDERR) return -ENOTSUP; if (target->tgt_file.out) { fclose(target->tgt_file.out); target->tgt_file.out = fopen(target->tgt_file.fname, "a"); if (!target->tgt_file.out) return -errno; } else { wq = target->tgt_file.wqueue; if (wq->bfd.fd >= 0) { osmo_fd_unregister(&wq->bfd); close(wq->bfd.fd); wq->bfd.fd = -1; } rc = open(target->tgt_file.fname, O_WRONLY|O_APPEND|O_CREAT|O_NONBLOCK, 0660); if (rc < 0) return -errno; wq->bfd.fd = rc; rc = osmo_fd_register(&wq->bfd); if (rc < 0) return rc; } return 0; } #if (!EMBEDDED) /* This is the file-specific subclass destructor logic, called from * log_target_destroy(). User should call log_target_destroy() to destroy this * object. */ void log_target_file_destroy(struct log_target *target) { struct osmo_wqueue *wq; OSMO_ASSERT(target->type == LOG_TGT_TYPE_FILE || target->type == LOG_TGT_TYPE_STDERR); if (target->tgt_file.out) { if (target->type == LOG_TGT_TYPE_FILE) fclose(target->tgt_file.out); target->tgt_file.out = NULL; } wq = target->tgt_file.wqueue; if (wq) { if (wq->bfd.fd >= 0) { osmo_fd_unregister(&wq->bfd); if (target->type == LOG_TGT_TYPE_FILE) close(wq->bfd.fd); wq->bfd.fd = -1; } osmo_wqueue_clear(wq); talloc_free(wq); target->tgt_file.wqueue = NULL; } talloc_free((void *)target->tgt_file.fname); target->tgt_file.fname = NULL; } /* write-queue tells us we should write another msgb (log line) to the output fd */ static int _file_wq_write_cb(struct osmo_fd *ofd, struct msgb *msg) { int rc; rc = write(ofd->fd, msgb_data(msg), msgb_length(msg)); if (rc < 0) return rc; if (rc != msgb_length(msg)) { /* pull the number of bytes we have already written */ msgb_pull(msg, rc); /* ask write_queue to re-insert the msgb at the head of the queue */ return -EAGAIN; } return 0; } /* output via buffered, blocking stdio streams */ static void _file_output_stream(struct log_target *target, unsigned int level, const char *log) { OSMO_ASSERT(target->tgt_file.out); fputs(log, target->tgt_file.out); fflush(target->tgt_file.out); } /* output via non-blocking write_queue, doing internal buffering */ static void _file_raw_output(struct log_target *target, int subsys, unsigned int level, const char *file, int line, int cont, const char *format, va_list ap) { struct msgb *msg; int rc; OSMO_ASSERT(target->tgt_file.wqueue); msg = msgb_alloc_c(target->tgt_file.wqueue, MAX_LOG_SIZE, "log_file_msg"); if (!msg) return; /* we simply enqueue the log message to a write queue here, to avoid any blocking * writes on the output file. The write queue will tell us once the file is writable * and call _file_wq_write_cb() */ rc = log_output_buf((char *)msgb_data(msg), msgb_tailroom(msg), target, subsys, level, file, line, cont, format, ap); msgb_put(msg, rc); /* attempt a synchronous, non-blocking write, if the write queue is empty */ if (target->tgt_file.wqueue->current_length == 0) { rc = _file_wq_write_cb(&target->tgt_file.wqueue->bfd, msg); if (rc == 0) { /* the write was complete, we can exit early */ msgb_free(msg); return; } } /* if we reach here, either we already had elements in the write_queue, or the synchronous write * failed: enqueue the message to the write_queue (backlog) */ if (osmo_wqueue_enqueue_quiet(target->tgt_file.wqueue, msg) < 0) { msgb_free(msg); /* TODO: increment some counter so we can see that messages were dropped */ } } /*! switch from non-blocking/write-queue to blocking + buffered stream output * \param[in] target log target which we should switch * \return 0 on success; 1 if already switched before; negative on error * Must be called with mutex osmo_log_tgt_mutex held, see log_tgt_mutex_lock. */ int log_target_file_switch_to_stream(struct log_target *target) { struct osmo_wqueue *wq; if (!target) return -ENODEV; if (target->tgt_file.out) { /* target has already been switched over */ return 1; } wq = target->tgt_file.wqueue; OSMO_ASSERT(wq); /* re-open output as stream */ if (target->type == LOG_TGT_TYPE_STDERR) target->tgt_file.out = stderr; else target->tgt_file.out = fopen(target->tgt_file.fname, "a"); if (!target->tgt_file.out) return -EIO; /* synchronously write anything left in the queue */ while (!llist_empty(&wq->msg_queue)) { struct msgb *msg = msgb_dequeue(&wq->msg_queue); fwrite(msgb_data(msg), msgb_length(msg), 1, target->tgt_file.out); msgb_free(msg); } /* now that everything succeeded, we can finally close the old output fd */ osmo_fd_unregister(&wq->bfd); if (target->type == LOG_TGT_TYPE_FILE) close(wq->bfd.fd); wq->bfd.fd = -1; /* release the queue itself */ talloc_free(wq); target->tgt_file.wqueue = NULL; target->output = _file_output_stream; target->raw_output = NULL; return 0; } /*! switch from blocking + buffered file output to non-blocking write-queue based output. * \param[in] target log target which we should switch * \return 0 on success; 1 if already switched before; negative on error * Must be called with mutex osmo_log_tgt_mutex held, see log_tgt_mutex_lock. */ int log_target_file_switch_to_wqueue(struct log_target *target) { struct osmo_wqueue *wq; int rc; if (!target) return -ENODEV; if (!target->tgt_file.out) { /* target has already been switched over */ return 1; } /* we create a ~640kB sized talloc pool within the write-queue to ensure individual * log lines (stored as msgbs) will not put result in malloc() calls, and also to * reduce the OOM probability within logging, as the pool is already allocated */ wq = talloc_pooled_object(target, struct osmo_wqueue, LOG_WQUEUE_LEN, LOG_WQUEUE_LEN*(sizeof(struct msgb)+MAX_LOG_SIZE)); if (!wq) return -ENOMEM; osmo_wqueue_init(wq, LOG_WQUEUE_LEN); fflush(target->tgt_file.out); if (target->type == LOG_TGT_TYPE_FILE) { rc = open(target->tgt_file.fname, O_WRONLY|O_APPEND|O_CREAT|O_NONBLOCK, 0660); if (rc < 0) { talloc_free(wq); return -errno; } } else { rc = STDERR_FILENO; } wq->bfd.fd = rc; wq->bfd.when = OSMO_FD_WRITE; wq->write_cb = _file_wq_write_cb; rc = osmo_fd_register(&wq->bfd); if (rc < 0) { talloc_free(wq); return -EIO; } target->tgt_file.wqueue = wq; target->raw_output = _file_raw_output; target->output = NULL; /* now that everything succeeded, we can finally close the old output stream */ if (target->type == LOG_TGT_TYPE_FILE) fclose(target->tgt_file.out); target->tgt_file.out = NULL; return 0; } /*! Create a new file-based log target using non-blocking write_queue * \param[in] fname File name of the new log file * \returns Log target in case of success, NULL otherwise */ struct log_target *log_target_create_file(const char *fname) { struct log_target *target; struct osmo_wqueue *wq; int rc; target = log_target_create(); if (!target) return NULL; target->type = LOG_TGT_TYPE_FILE; /* we create a ~640kB sized talloc pool within the write-queue to ensure individual * log lines (stored as msgbs) will not put result in malloc() calls, and also to * reduce the OOM probability within logging, as the pool is already allocated */ wq = talloc_pooled_object(target, struct osmo_wqueue, LOG_WQUEUE_LEN, LOG_WQUEUE_LEN*(sizeof(struct msgb)+MAX_LOG_SIZE)); if (!wq) { log_target_destroy(target); return NULL; } osmo_wqueue_init(wq, LOG_WQUEUE_LEN); wq->bfd.fd = open(fname, O_WRONLY|O_APPEND|O_CREAT|O_NONBLOCK, 0660); if (wq->bfd.fd < 0) { talloc_free(wq); log_target_destroy(target); return NULL; } wq->bfd.when = OSMO_FD_WRITE; wq->write_cb = _file_wq_write_cb; rc = osmo_fd_register(&wq->bfd); if (rc < 0) { talloc_free(wq); log_target_destroy(target); return NULL; } target->tgt_file.wqueue = wq; target->raw_output = _file_raw_output; target->tgt_file.fname = talloc_strdup(target, fname); return target; } #endif /*! Create the STDERR log target * \returns dynamically-allocated \ref log_target for STDERR */ struct log_target *log_target_create_stderr(void) { /* since C89/C99 says stderr is a macro, we can safely do this! */ #if !EMBEDDED && defined(stderr) struct log_target *target; target = log_target_create(); if (!target) return NULL; target->type = LOG_TGT_TYPE_STDERR; target->tgt_file.out = stderr; target->output = _file_output_stream; return target; #else return NULL; #endif /* stderr */ } /* @} */