diff --git a/src/modules/file_out/Makefile b/src/modules/file_out/Makefile
new file mode 100644
index 00000000000..4940a61c37c
--- /dev/null
+++ b/src/modules/file_out/Makefile
@@ -0,0 +1,8 @@
+# WARNING: do not run this directly, it should be run by the main Makefile
+
+include ../../Makefile.defs
+auto_gen=
+NAME=file_out.so
+LIBS=
+
+include ../../Makefile.modules
diff --git a/src/modules/file_out/doc/Makefile b/src/modules/file_out/doc/Makefile
new file mode 100644
index 00000000000..374e84f8e08
--- /dev/null
+++ b/src/modules/file_out/doc/Makefile
@@ -0,0 +1,4 @@
+docs = file_out.xml
+
+docbook_dir = ../../../../doc/docbook
+include $(docbook_dir)/Makefile.module
diff --git a/src/modules/file_out/doc/file_out.xml b/src/modules/file_out/doc/file_out.xml
new file mode 100644
index 00000000000..16f15f7f9c5
--- /dev/null
+++ b/src/modules/file_out/doc/file_out.xml
@@ -0,0 +1,39 @@
+
+
+
+%docentities;
+
+]>
+
+
+
+ file_out Module
+ &kamailioname;
+
+
+ Xenofon
+ Karamanos
+
+ GILAWA Ltd
+
+ xk@gilawa.com
+
+
+ https://gilawa.com/
+
+
+
+
+
+ 2024
+ GILAWA Ltd
+
+
+
+
+
+
+
diff --git a/src/modules/file_out/doc/file_out_admin.xml b/src/modules/file_out/doc/file_out_admin.xml
new file mode 100644
index 00000000000..ed6b18268d0
--- /dev/null
+++ b/src/modules/file_out/doc/file_out_admin.xml
@@ -0,0 +1,191 @@
+
+
+
+%docentities;
+
+]>
+
+
+
+
+
+ &adminguide;
+
+
+ Overview
+
+ This is a simple module to support fast streaming output to file name
+ and handle that changes on an interval. It implements only one function
+ that streams a chunk of text to the current output file handle.
+
+
+ The module can be used to write logs for up to 10 different log files.
+ Each log file can be configured to have a different name and extension.
+ String can contain pseudo-variables. The module will replace the
+ pseudo-variables with the actual values. The module will also rotate
+ the log files at a specified interval. The interval is specified in seconds.
+
+
+
+ Dependencies
+
+ &kamailio; Modules
+
+ The following modules must be loaded before this module:
+
+
+
+ none.
+
+
+
+
+
+
+ External Libraries or Applications
+
+ The following libraries or applications must be installed before running
+ &kamailio; with this module loaded:
+
+
+
+ none.
+
+
+
+
+
+
+
+ Parameters
+
+
+ base_folder (string)
+
+ Absolute path to the folder where log files should be saved.
+
+
+
+
+ Default value is /var/log/kamailio/file_out/
.
+
+
+
+ Set base_folder parameter
+
+ ...
+ modparam("file_out", "base_folder", "/tmp/file_out/")
+ ...
+
+
+
+
+ base_filename (string)
+
+ The filename to be used for each file. Don't include the extension. Required.
+
+
+
+ Default value is null
.
+
+
+
+ Set base_filename parameter
+
+ ...
+ modparam("file_out", "base_filename", "accounting")
+ ...
+
+
+
+
+ extension (string)
+
+ The extension to be used for each file.
+
+
+
+ Default value is .out
.
+
+
+
+ Set extension parameter
+
+ ...
+ modparam("file_out", "extension", ".txt")
+ ...
+
+
+
+
+ interval_seconds (int)
+
+ The interval in seconds between file rotation.
+
+
+
+ Default value is 600
(10 minutes).
+
+
+
+ Set interval_seconds parameter
+
+ ...
+ modparam("file_out", "interval_seconds", "300")
+ ...
+
+
+
+
+
+
+
+
+ Functions
+
+
+ file_out(index, string)
+
+
+ This function is used to write a string to a file. The file is
+ determined by the index parameter. The string parameter is the
+ string to be written to the file.
+
+ Index order is the same as the order in which the log files are
+ defined in the configuration file starting from 0.
+
+
+
+ file_out usage
+
+ ...
+ modparam("file_out", "base_filename", "accounting")
+ modparam("file_out", "base_filename", "missed_calls")
+ ...
+ request_route {
+
+ file_out("0", "Writing to accounting.out file $rm from $fu (IP:$si:$sp)");
+ file_out("1", "Writing to missed_calls.out file $rm from $fu (IP:$si:$sp)");
+ ...
+ }
+
+
+
+
+
+
+
+ Exported pseudo-variables
+
+
+
+ none.
+
+
+
+
+
+
diff --git a/src/modules/file_out/file_out.c b/src/modules/file_out/file_out.c
new file mode 100644
index 00000000000..04cc041aa86
--- /dev/null
+++ b/src/modules/file_out/file_out.c
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2024 GILAWA Ltd
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio 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
+ *
+ * Kamailio 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "../../core/sr_module.h"
+#include "../../core/route_struct.h"
+#include "../../core/str.h"
+#include "../../core/mod_fix.h"
+#include "../../core/locking.h"
+#include "../../core/cfg/cfg_struct.h"
+#include "types.h"
+
+#include
+#include
+#include
+#include
+#include /* usleep() */
+
+
+MODULE_VERSION
+
+
+#define FO_MAX_PATH_LEN 2048
+#define FO_MAX_FILES 10 /* Maximum number of files */
+
+static int mod_init(void);
+static int child_init(int rank);
+static void destroy(void);
+
+static int fo_write_to_file(sip_msg_t *msg, char *index, char *log_message);
+
+static FILE *fo_get_file_handle(const int index);
+static int fo_get_full_path(const int index, char *full_path);
+static int fo_init_file(const int index);
+static int fo_close_file(const int index);
+static int fo_check_interval();
+static int fo_fixup_int_pvar(void **param, int param_no);
+static int fo_count_assigned_files();
+static void fo_log_writer_process(int rank);
+static int fo_add_filename(modparam_t type, void *val);
+
+/* Default parameters */
+char *fo_base_folder = "/var/log/kamailio/file_out/";
+char *fo_base_filename[FO_MAX_FILES] = {""};
+char *fo_extension = ".out";
+int fo_interval_seconds = 10 * 60;
+
+/* Shared variables */
+fo_queue_t *fo_queue = NULL;
+int *fo_number_of_files = NULL;
+
+time_t fo_stored_timestamp = 0;
+time_t fo_current_timestamp = 0;
+FILE *fo_file_output[FO_MAX_FILES];
+
+static cmd_export_t cmds[] = {{"file_out", (cmd_function)fo_write_to_file, 2,
+ fo_fixup_int_pvar, 0, ANY_ROUTE},
+ {0, 0, 0, 0, 0, 0}};
+
+static param_export_t params[] = {
+ {"base_folder", PARAM_STRING, &fo_base_folder},
+ {"base_filename", PARAM_STRING | PARAM_USE_FUNC, &fo_add_filename},
+ {"interval_seconds", PARAM_INT, &fo_interval_seconds},
+ {"extension", PARAM_STRING, &fo_extension}, {0, 0, 0}};
+
+struct module_exports exports = {
+ "file_out", /* module name */
+ DEFAULT_DLFLAGS, /* dlopen flags */
+ cmds, /* exported functions */
+ params, /* exported parameters */
+ 0, /* RPC method exports */
+ 0, /* exported pseudo-variables */
+ 0, /* response handling function */
+ mod_init, /* module initialization function */
+ child_init, /* per-child init function */
+ destroy /* module destroy function */
+};
+
+
+static int mod_init(void)
+{
+ LM_DBG("initializing\n");
+ LM_DBG("base_folder = %s\n", fo_base_folder);
+ LM_DBG("base_filename_path= %s\n", fo_base_filename[0]);
+ LM_DBG("interval_seconds = %d\n", fo_interval_seconds);
+ LM_DBG("extension = %s\n", fo_extension);
+
+ //* Create shared variables */
+ fo_queue = (fo_queue_t *)shm_malloc(sizeof(fo_queue_t));
+ if(!fo_queue) {
+ SHM_MEM_ERROR;
+ return -1;
+ }
+ fo_queue->front = NULL;
+ fo_queue->rear = NULL;
+ if(lock_init(&fo_queue->lock) == 0) {
+ /* error initializing the lock */
+ LM_ERR("error initializing the lock\n");
+ return -1;
+ }
+
+ /* Count the given files */
+ *fo_number_of_files = fo_count_assigned_files();
+
+ /* Initialize per process vars */
+ fo_stored_timestamp = time(NULL);
+
+ /* Register worker process */
+ register_procs(1);
+ cfg_register_child(1);
+ LM_DBG("Initialization done\n");
+ return 0;
+}
+
+/**
+ * per-child init function
+ */
+static int child_init(int rank)
+{
+ int pid;
+ if(rank != PROC_MAIN) {
+ return 0;
+ }
+
+ pid = fork_process(PROC_NOCHLDINIT, "log_writ", 1);
+ if(pid < 0) {
+ LM_ERR("fork failed\n");
+ return -1; /* error */
+ }
+ if(pid == 0) {
+ /* child */
+ /* initialize the config framework */
+ if(cfg_child_init())
+ return -1;
+
+ /* Initialize and open files */
+ for(int i = 0; i < *fo_number_of_files; i++) {
+ fo_init_file(i);
+ }
+
+ for(;;) {
+ /* update the local config framework structures */
+ cfg_update();
+
+ usleep(10000);
+ fo_log_writer_process(rank);
+ }
+ // return 0;
+ }
+ return 0;
+}
+
+/**
+ * module destroy function
+ */
+static void destroy(void)
+{
+ int result = 0;
+ if(fo_file_output[0] != NULL) {
+ result = fclose(fo_file_output[0]);
+ if(result != 0) {
+ ERR("Failed to close output file");
+ }
+ }
+}
+
+static void fo_log_writer_process(int rank)
+{
+ fo_log_message_t log_message;
+ int result = 0;
+ while(!fo_is_queue_empty(fo_queue)) {
+ result = fo_dequeue(fo_queue, &log_message);
+ if(result < 0) {
+ LM_ERR("deque error\n");
+ return;
+ }
+ FILE *out = fo_get_file_handle(log_message.dest_file);
+ if(out == NULL) {
+ LM_ERR("out is NULL\n");
+ return;
+ }
+
+ fprintf(out, "%s\n", log_message.message);
+ fflush(out);
+ }
+}
+
+/*
+* fixup function for two parameters
+* 1st param: int
+* 2nd param: string containing PVs
+*/
+static int fo_fixup_int_pvar(void **param, int param_no)
+{
+ if(param_no == 1) {
+ return fixup_igp_null(param, param_no);
+ } else if(param_no == 2) {
+ return fixup_var_pve_str_12(param, param_no);
+ }
+ return 0;
+}
+
+static int fo_add_filename(modparam_t type, void *val)
+{
+ if(fo_number_of_files == NULL) {
+ LM_ERR("fo_number_of_files is NULL\n");
+ fo_number_of_files = (int *)shm_malloc(sizeof(int));
+ if(!fo_number_of_files) {
+ SHM_MEM_ERROR;
+ return -1;
+ }
+ *fo_number_of_files = 0;
+ }
+
+ if((type & PARAM_STRING) == 0) {
+ LM_ERR("bad parameter type %d\n", type);
+ return -1;
+ }
+
+ if(fo_number_of_files != NULL && *fo_number_of_files >= FO_MAX_FILES) {
+ LM_ERR("Maximum number of files [%d] reached. The rest will not be "
+ "processed \n",
+ FO_MAX_FILES);
+ return 0;
+ }
+ fo_base_filename[*fo_number_of_files] = (char *)val;
+ LM_DBG("fo_base_filename[%d] = %s\n", *fo_number_of_files,
+ fo_base_filename[*fo_number_of_files]);
+ (*fo_number_of_files)++;
+ return 0;
+}
+
+/*
+* Count the number of files that are assigned
+* return the number of files (saved also in shared fo_number_of_files)
+*/
+static int fo_count_assigned_files()
+{
+ return *fo_number_of_files;
+}
+
+static int fo_init_file(const int index)
+{
+ char full_path[FO_MAX_PATH_LEN];
+ fo_get_full_path(index, full_path);
+ fo_file_output[index] = fopen(full_path, "a");
+ if(fo_file_output[index] == NULL) {
+ LM_ERR("Couldn't open file %s\n", strerror(errno));
+ return -1;
+ }
+ return 1;
+}
+
+static int fo_close_file(const int index)
+{
+ int result = 0;
+ if(fo_file_output[index] != NULL) {
+ result = fclose(fo_file_output[index]);
+ if(result != 0) {
+ ERR("Failed to close output file");
+ return -1;
+ }
+ }
+ return 1;
+}
+
+/*
+* Check if the interval has passed
+* return 1 if interval has passed
+* return 0 if interval has not passed
+*/
+static int fo_check_interval()
+{
+ fo_current_timestamp = time(NULL);
+
+ // Calculate the difference between the current timestamp and the stored timestamp
+ int difference = difftime(fo_current_timestamp, fo_stored_timestamp);
+ if(difference >= fo_interval_seconds) {
+ LM_ERR("interval has passed\n");
+ return 1;
+ }
+ // LM_ERR("interval has not passed\n");
+ return 0;
+}
+/**
+ * maintain file handle
+ */
+
+static FILE *fo_get_file_handle(const int index)
+{
+ int result = 0;
+ if(fo_check_interval()) {
+ /* Interval passed. Close all files and open new ones */
+ for(int i = 0; i < *fo_number_of_files; i++) {
+ result = fo_close_file(i);
+ if(result != 1) {
+ LM_ERR("Failed to close output file");
+ return NULL;
+ }
+ }
+ fo_stored_timestamp = fo_current_timestamp;
+
+ LM_DBG("Opening new files due to interval passed\n");
+ /* Make sure we know how many files we need */
+ if(fo_number_of_files == NULL) {
+ *fo_number_of_files = fo_count_assigned_files();
+ }
+ /* Initialize and open files */
+ for(int i = 0; i < *fo_number_of_files; i++) {
+ result = fo_init_file(i);
+ if(result != 1) {
+ LM_ERR("Failed to initialize output file");
+ return NULL;
+ }
+ }
+ return fo_file_output[index];
+ } else {
+ /* Interval has not passed */
+ /* Assume files are correct */
+ return fo_file_output[index];
+ }
+}
+
+/**
+ * Determine full file path
+ */
+static int fo_get_full_path(const int index, char *full_path)
+{
+ snprintf(full_path, FO_MAX_PATH_LEN, "%s/%s_%.f%s", fo_base_folder,
+ fo_base_filename[index], difftime(fo_stored_timestamp, (time_t)0),
+ fo_extension);
+ LM_INFO("Path to write to: %s\n", full_path);
+ return 1;
+}
+
+static int fo_write_to_file(sip_msg_t *msg, char *index, char *log_message)
+{
+ int result, file_index;
+ if(index == NULL || log_message == NULL) {
+ LM_ERR("index or log_messsage is NULL\n");
+ return -1;
+ }
+
+ result = get_int_fparam(&file_index, msg, (fparam_t *)index);
+ if(result < 0) {
+ LM_ERR("Failed to get int from param 0: %d\n", result);
+ return -1;
+ }
+
+ str value;
+ result = get_str_fparam(&value, msg, (fparam_t *)log_message);
+ if(result < 0) {
+ LM_ERR("Failed to string from param 1: %d\n", result);
+ return -1;
+ }
+
+ /* Add the logging string to the global gueue */
+ fo_log_message_t logMessage;
+ logMessage.message = value.s;
+ logMessage.dest_file = file_index;
+ fo_enqueue(fo_queue, logMessage);
+
+ return 1;
+}
diff --git a/src/modules/file_out/types.c b/src/modules/file_out/types.c
new file mode 100644
index 00000000000..a80566dfc8f
--- /dev/null
+++ b/src/modules/file_out/types.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 GILAWA Ltd
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio 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
+ *
+ * Kamailio 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include
+#include
+
+#include "types.h"
+
+static fo_node_t *fo_new_node(fo_log_message_t data)
+{
+ fo_node_t *temp = (fo_node_t *)shm_malloc(sizeof(fo_node_t));
+ temp->data = data;
+ temp->next = NULL;
+ return temp;
+}
+
+
+int fo_enqueue(fo_queue_t *q, fo_log_message_t data)
+{
+ /*
+ Copy the contents of data.message
+ */
+ char *message_copy = (char *)shm_malloc(strlen(data.message) + 1);
+ strcpy(message_copy, data.message);
+ data.message = message_copy;
+ fo_node_t *temp = fo_new_node(data);
+
+ lock_get(&(q->lock));
+
+ if(q->rear == NULL) {
+ q->front = q->rear = temp;
+ lock_release(&(q->lock));
+ return 1;
+ }
+
+ q->rear->next = temp;
+ q->rear = temp;
+
+ lock_release(&(q->lock));
+ return 1;
+}
+
+int fo_dequeue(fo_queue_t *q, fo_log_message_t *data)
+{
+ lock_get(&(q->lock));
+
+ if(q->front == NULL) {
+ lock_release(&(q->lock));
+ return -1;
+ }
+ fo_node_t *temp = q->front;
+ *data = temp->data;
+ q->front = q->front->next;
+
+ if(q->front == NULL)
+ q->rear = NULL;
+
+
+ if(temp != NULL) {
+ if(temp->data.message != NULL) {
+ shm_free(temp->data.message);
+ temp->data.message = NULL;
+ }
+ shm_free(temp);
+ temp = NULL;
+ }
+ lock_release(&(q->lock));
+
+ return 1;
+}
+
+int fo_is_queue_empty(fo_queue_t *q)
+{
+ lock_get(&(q->lock));
+ int result = (q->front == NULL);
+ lock_release(&(q->lock));
+ return result;
+}
+
+int fo_queue_size(fo_queue_t *q)
+{
+ lock_get(&(q->lock));
+ int count = 0;
+ fo_node_t *temp = q->front;
+ while(temp != NULL) {
+ count++;
+ temp = temp->next;
+ }
+ lock_release(&(q->lock));
+ return count;
+}
diff --git a/src/modules/file_out/types.h b/src/modules/file_out/types.h
new file mode 100644
index 00000000000..0c81cb20a7b
--- /dev/null
+++ b/src/modules/file_out/types.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 GILAWA Ltd
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio 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
+ *
+ * Kamailio 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "../../core/locking.h"
+
+typedef struct log_message
+{
+ char *message;
+ int dest_file;
+} fo_log_message_t;
+
+typedef struct node
+{
+ struct log_message data;
+ struct node *next;
+} fo_node_t;
+
+typedef struct queue
+{
+ struct node *front;
+ struct node *rear;
+ gen_lock_t lock;
+} fo_queue_t;
+
+int fo_enqueue(fo_queue_t *q, fo_log_message_t data);
+int fo_dequeue(fo_queue_t *q, fo_log_message_t *data);
+int fo_is_queue_empty(fo_queue_t *q);
+int fo_queue_size(fo_queue_t *q);