LOGGING: configure logging from the vty

We can now configure logging to (multiple) files, stderr and syslog
from the vty command line in a persistent way (config file)
diff --git a/configure.in b/configure.in
index 309aa03..165cecb 100644
--- a/configure.in
+++ b/configure.in
@@ -18,7 +18,7 @@
 
 dnl checks for header files
 AC_HEADER_STDC
-AC_CHECK_HEADERS(execinfo.h sys/select.h)
+AC_CHECK_HEADERS(execinfo.h sys/select.h syslog.h ctype.h)
 
 # The following test is taken from WebKit's webkit.m4
 saved_CFLAGS="$CFLAGS"
diff --git a/include/osmocom/vty/command.h b/include/osmocom/vty/command.h
index 69e9e77..caf0414 100644
--- a/include/osmocom/vty/command.h
+++ b/include/osmocom/vty/command.h
@@ -70,6 +70,7 @@
 	CONFIG_NODE,		/* Config node. Default mode of config file. */
 	SERVICE_NODE,		/* Service node. */
 	DEBUG_NODE,		/* Debug node. */
+	CFG_LOG_NODE,		/* Configure the logging */
 
 	VTY_NODE,		/* Vty node. */
 
diff --git a/include/osmocore/logging.h b/include/osmocore/logging.h
index d4d632d..a2c63e9 100644
--- a/include/osmocore/logging.h
+++ b/include/osmocore/logging.h
@@ -69,6 +69,13 @@
 	unsigned int num_cat;
 };
 
+enum log_target_type {
+	LOG_TGT_TYPE_VTY,
+	LOG_TGT_TYPE_SYSLOG,
+	LOG_TGT_TYPE_FILE,
+	LOG_TGT_TYPE_STDERR,
+};
+
 struct log_target {
         struct llist_head entry;
 
@@ -80,6 +87,8 @@
 	int use_color:1;
 	int print_timestamp:1;
 
+	enum log_target_type type;
+
 	union {
 		struct {
 			FILE *out;
@@ -88,6 +97,7 @@
 
 		struct {
 			int priority;
+			int facility;
 		} tgt_syslog;
 
 		struct {
@@ -138,4 +148,7 @@
 const char *log_vty_category_string(struct log_info *info);
 const char *log_vty_level_string(struct log_info *info);
 
+struct log_target *log_target_find(int type, const char *fname);
+extern struct llist_head osmo_log_target_list;
+
 #endif /* _OSMOCORE_LOGGING_H */
diff --git a/include/osmocore/utils.h b/include/osmocore/utils.h
index 6fe1b38..0cdf03b 100644
--- a/include/osmocore/utils.h
+++ b/include/osmocore/utils.h
@@ -24,4 +24,7 @@
 
 #define static_assert(exp, name) typedef int dummy##name [(exp) ? 1 : -1];
 
+void osmo_str2lower(char *out, const char *in);
+void osmo_str2upper(char *out, const char *in);
+
 #endif
diff --git a/src/logging.c b/src/logging.c
index 4452862..876a352 100644
--- a/src/logging.c
+++ b/src/logging.c
@@ -41,7 +41,7 @@
 
 static struct log_context log_context;
 static void *tall_log_ctx = NULL;
-static LLIST_HEAD(target_list);
+LLIST_HEAD(osmo_log_target_list);
 
 static const struct value_string loglevel_strs[] = {
 	{ 0,		"EVERYTHING" },
@@ -176,7 +176,7 @@
 {
 	struct log_target *tar;
 
-	llist_for_each_entry(tar, &target_list, entry) {
+	llist_for_each_entry(tar, &osmo_log_target_list, entry) {
 		struct log_category *category;
 		int output = 0;
 
@@ -239,7 +239,7 @@
 
 void log_add_target(struct log_target *target)
 {
-	llist_add_tail(&target->entry, &target_list);
+	llist_add_tail(&target->entry, &osmo_log_target_list);
 }
 
 void log_del_target(struct log_target *target)
@@ -338,6 +338,7 @@
 	if (!target)
 		return NULL;
 
+	target->type = LOG_TGT_TYPE_STDERR;
 	target->tgt_file.out = stderr;
 	target->output = _file_output;
 	return target;
@@ -354,6 +355,7 @@
 	if (!target)
 		return NULL;
 
+	target->type = LOG_TGT_TYPE_FILE;
 	target->tgt_file.out = fopen(fname, "a");
 	if (!target->tgt_file.out)
 		return NULL;
@@ -365,6 +367,22 @@
 	return target;
 }
 
+struct log_target *log_target_find(int type, const char *fname)
+{
+	struct log_target *tgt;
+
+	llist_for_each_entry(tgt, &osmo_log_target_list, entry) {
+		if (tgt->type != type)
+			continue;
+		if (tgt->type == LOG_TGT_TYPE_FILE) {
+			if (!strcmp(fname, tgt->tgt_file.fname))
+				return tgt;
+		} else
+			return tgt;
+	}
+	return NULL;
+}
+
 void log_target_destroy(struct log_target *target)
 {
 
diff --git a/src/logging_syslog.c b/src/logging_syslog.c
index b65e819..b558fc0 100644
--- a/src/logging_syslog.c
+++ b/src/logging_syslog.c
@@ -21,6 +21,8 @@
 
 #include "../config.h"
 
+#ifdef HAVE_SYSLOG_H
+
 #include <stdarg.h>
 #include <stdlib.h>
 #include <stdio.h>
@@ -64,9 +66,13 @@
 	if (!target)
 		return NULL;
 
+	target->tgt_syslog.facility = facility;
+	target->type = LOG_TGT_TYPE_SYSLOG;
 	target->output = _syslog_output;
 
 	openlog(ident, option, facility);
 
 	return target;
 }
+
+#endif /* HAVE_SYSLOG_H */
diff --git a/src/utils.c b/src/utils.c
index 49c210e..354fce5 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -134,3 +134,25 @@
 {
 	return _hexdump(buf, len, "");
 }
+
+#include "../config.h"
+#ifdef HAVE_CTYPE_H
+#include <ctype.h>
+void osmo_str2lower(char *out, const char *in)
+{
+	unsigned int i;
+
+	for (i = 0; i < strlen(in); i++)
+		out[i] = tolower(in[i]);
+	out[strlen(in)] = '\0';
+}
+
+void osmo_str2upper(char *out, const char *in)
+{
+	unsigned int i;
+
+	for (i = 0; i < strlen(in); i++)
+		out[i] = toupper(in[i]);
+	out[strlen(in)] = '\0';
+}
+#endif /* HAVE_CTYPE_H */
diff --git a/src/vty/Makefile.am b/src/vty/Makefile.am
index 7353ab8..8d730c3 100644
--- a/src/vty/Makefile.am
+++ b/src/vty/Makefile.am
@@ -10,5 +10,5 @@
 
 libosmovty_la_SOURCES = buffer.c command.c vty.c vector.c utils.c \
 			telnet_interface.c logging_vty.c
-libosmovty_la_LIBADD = $(top_builddir)/src/libosmocore.la
+libosmovty_la_LIBADD = -losmocore $(top_builddir)/src/libosmocore.la
 endif
diff --git a/src/vty/command.c b/src/vty/command.c
index 7525df6..0f65224 100644
--- a/src/vty/command.c
+++ b/src/vty/command.c
@@ -2180,6 +2180,9 @@
 	case VTY_NODE:
 		vty->node = CONFIG_NODE;
 		break;
+	case CFG_LOG_NODE:
+		vty->node = CONFIG_NODE;
+		break;
 	default:
 		break;
 	}
@@ -2195,6 +2198,7 @@
 	case ENABLE_NODE:
 		/* Nothing to do. */
 		break;
+	case CFG_LOG_NODE:
 	case CONFIG_NODE:
 	case VTY_NODE:
 		vty_config_unlock(vty);
diff --git a/src/vty/logging_vty.c b/src/vty/logging_vty.c
index 55882a7..b51be7d 100644
--- a/src/vty/logging_vty.c
+++ b/src/vty/logging_vty.c
@@ -22,8 +22,11 @@
 #include <stdlib.h>
 #include <string.h>
 
+#include "../../config.h"
+
 #include <osmocore/talloc.h>
 #include <osmocore/logging.h>
+#include <osmocore/utils.h>
 
 //#include <openbsc/vty.h>
 
@@ -33,6 +36,8 @@
 #include <osmocom/vty/telnet_interface.h>
 #include <osmocom/vty/logging.h>
 
+#define LOG_STR "Configure logging sub-system\n"
+
 extern const struct log_info *osmo_log_info;
 
 static void _vty_output(struct log_target *tgt,
@@ -179,6 +184,35 @@
 	"Log noticable messages and higher levels\n"		\
 	"Log error messages and higher levels\n"		\
 	"Log only fatal messages\n"
+
+static int _logging_level(struct vty *vty, struct log_target *dbg,
+			  const char *cat_str, const char *lvl_str)
+{
+	int category = log_parse_category(cat_str);
+	int level = log_parse_level(lvl_str);
+
+	if (level < 0) {
+		vty_out(vty, "Invalid level `%s'%s", lvl_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	/* Check for special case where we want to set global log level */
+	if (!strcmp(cat_str, "all")) {
+		log_set_log_level(dbg, level);
+		return CMD_SUCCESS;
+	}
+
+	if (category < 0) {
+		vty_out(vty, "Invalid category `%s'%s", cat_str, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	dbg->categories[category].enabled = 1;
+	dbg->categories[category].loglevel = level;
+
+	return CMD_SUCCESS;
+}
+
 DEFUN(logging_level,
       logging_level_cmd,
       "logging level " VTY_DEBUG_CATEGORIES " " VTY_DEBUG_LEVELS,
@@ -188,8 +222,6 @@
       LEVELS_HELP)
 {
 	struct telnet_connection *conn;
-	int category = log_parse_category(argv[0]);
-	int level = log_parse_level(argv[1]);
 
 	conn = (struct telnet_connection *) vty->priv;
 	if (!conn->dbg) {
@@ -197,26 +229,7 @@
 		return CMD_WARNING;
 	}
 
-	if (level < 0) {
-		vty_out(vty, "Invalid level `%s'%s", argv[1], VTY_NEWLINE);
-		return CMD_WARNING;
-	}
-
-	/* Check for special case where we want to set global log level */
-	if (!strcmp(argv[0], "all")) {
-		log_set_log_level(conn->dbg, level);
-		return CMD_SUCCESS;
-	}
-
-	if (category < 0) {
-		vty_out(vty, "Invalid category `%s'%s", argv[0], VTY_NEWLINE);
-		return CMD_WARNING;
-	}
-
-	conn->dbg->categories[category].enabled = 1;
-	conn->dbg->categories[category].loglevel = level;
-
-	return CMD_SUCCESS;
+	return _logging_level(vty, conn->dbg, argv[0], argv[1]);
 }
 
 DEFUN(logging_set_category_mask,
@@ -338,6 +351,310 @@
 	return CMD_SUCCESS;
 }
 
+/* Support for configuration of log targets != the current vty */
+
+struct cmd_node cfg_log_node = {
+	CFG_LOG_NODE,
+	"%s(config-log)# ",
+	1
+};
+
+DEFUN(cfg_log_fltr_all,
+      cfg_log_fltr_all_cmd,
+      "logging filter all (0|1)",
+	LOGGING_STR FILTER_STR
+	"Do you want to log all messages?\n"
+	"Only print messages matched by other filters\n"
+	"Bypass filter and print all messages\n")
+{
+	struct log_target *dbg = vty->index;
+
+	log_set_all_filter(dbg, atoi(argv[0]));
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_log_use_clr,
+      cfg_log_use_clr_cmd,
+      "logging color (0|1)",
+	LOGGING_STR "Configure color-printing for log messages\n"
+      "Don't use color for printing messages\n"
+      "Use color for printing messages\n")
+{
+	struct log_target *dbg = vty->index;
+
+	log_set_use_color(dbg, atoi(argv[0]));
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_log_timestamp,
+      cfg_log_timestamp_cmd,
+      "logging timestamp (0|1)",
+	LOGGING_STR "Configure log message timestamping\n"
+	"Don't prefix each log message\n"
+	"Prefix each log message with current timestamp\n")
+{
+	struct log_target *dbg = vty->index;
+
+	log_set_print_timestamp(dbg, atoi(argv[0]));
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_log_level,
+      cfg_log_level_cmd,
+      "logging level " VTY_DEBUG_CATEGORIES " " VTY_DEBUG_LEVELS,
+      LOGGING_STR
+      "Set the log level for a specified category\n"
+      CATEGORIES_HELP
+      LEVELS_HELP)
+{
+	struct log_target *dbg = vty->index;
+
+	return _logging_level(vty, dbg, argv[0], argv[1]);
+}
+
+#ifdef HAVE_SYSLOG_H
+
+#include <syslog.h>
+
+static const int local_sysl_map[] = {
+	[0] = LOG_LOCAL0,
+	[1] = LOG_LOCAL1,
+	[2] = LOG_LOCAL2,
+	[3] = LOG_LOCAL3,
+	[4] = LOG_LOCAL4,
+	[5] = LOG_LOCAL5,
+	[6] = LOG_LOCAL6,
+	[7] = LOG_LOCAL7
+};
+
+static int _cfg_log_syslog(struct vty *vty, int facility)
+{
+	struct log_target *tgt;
+
+	/* First delete the old syslog target, if any */
+	tgt = log_target_find(LOG_TGT_TYPE_SYSLOG, NULL);
+	if (tgt)
+		log_target_destroy(tgt);
+
+	tgt = log_target_create_syslog("FIXME", 0, facility);
+	if (!tgt) {
+		vty_out(vty, "%% Unable to open syslog%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	log_add_target(tgt);
+
+	vty->index = tgt;
+	vty->node = CFG_LOG_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_log_syslog_local, cfg_log_syslog_local_cmd,
+      "log syslog local <0-7>",
+	LOG_STR "Logging via syslog\n" "Syslog LOCAL facility\n"
+	"Local facility number\n")
+{
+	int local = atoi(argv[0]);
+	int facility = local_sysl_map[local];
+
+	return _cfg_log_syslog(vty, facility);
+}
+
+static struct value_string sysl_level_names[] = {
+	{ LOG_AUTHPRIV, "authpriv" },
+	{ LOG_CRON, 	"cron" },
+	{ LOG_DAEMON,	"daemon" },
+	{ LOG_FTP,	"ftp" },
+	{ LOG_LPR,	"lpr" },
+	{ LOG_MAIL,	"mail" },
+	{ LOG_NEWS,	"news" },
+	{ LOG_USER,	"user" },
+	{ LOG_UUCP,	"uucp" },
+	/* only for value -> string conversion */
+	{ LOG_LOCAL0,	"local 0" },
+	{ LOG_LOCAL1,	"local 1" },
+	{ LOG_LOCAL2,	"local 2" },
+	{ LOG_LOCAL3,	"local 3" },
+	{ LOG_LOCAL4,	"local 4" },
+	{ LOG_LOCAL5,	"local 5" },
+	{ LOG_LOCAL6,	"local 6" },
+	{ LOG_LOCAL7,	"local 7" },
+	{ 0, NULL }
+};
+
+DEFUN(cfg_log_syslog, cfg_log_syslog_cmd,
+      "log syslog (authpriv|cron|daemon|ftp|lpr|mail|news|user|uucp)",
+	LOG_STR "Logging via syslog\n")
+{
+	int facility = get_string_value(sysl_level_names, argv[0]);
+
+	return _cfg_log_syslog(vty, facility);
+}
+
+DEFUN(cfg_no_log_syslog, cfg_no_log_syslog_cmd,
+	"no log syslog",
+	NO_STR LOG_STR "Logging via syslog\n")
+{
+	struct log_target *tgt;
+
+	tgt = log_target_find(LOG_TGT_TYPE_SYSLOG, NULL);
+	if (!tgt) {
+		vty_out(vty, "%% No syslog target found%s",
+			VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	log_target_destroy(tgt);
+
+	return CMD_SUCCESS;
+}
+#endif /* HAVE_SYSLOG_H */
+
+DEFUN(cfg_log_stderr, cfg_log_stderr_cmd,
+	"log stderr",
+	LOG_STR "Logging via STDERR of the process\n")
+{
+	struct log_target *tgt;
+
+	tgt = log_target_find(LOG_TGT_TYPE_STDERR, NULL);
+	if (!tgt) {
+		tgt = log_target_create_stderr();
+		if (!tgt) {
+			vty_out(vty, "%% Unable to create stderr log%s",
+				VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		log_add_target(tgt);
+	}
+
+	vty->index = tgt;
+	vty->node = CFG_LOG_NODE;
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_log_stderr, cfg_no_log_stderr_cmd,
+	"no log stderr",
+	NO_STR LOG_STR "Logging via STDERR of the process\n")
+{
+	struct log_target *tgt;
+
+	tgt = log_target_find(LOG_TGT_TYPE_STDERR, NULL);
+	if (!tgt) {
+		vty_out(vty, "%% No stderr logging active%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	log_target_destroy(tgt);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_log_file, cfg_log_file_cmd,
+	"log file .FILENAME",
+	LOG_STR "Logging to text file\n" "Filename\n")
+{
+	const char *fname = argv[0];
+	struct log_target *tgt;
+
+	tgt = log_target_find(LOG_TGT_TYPE_FILE, fname);
+	if (!tgt) {
+		tgt = log_target_create_file(fname);
+		if (!tgt) {
+			vty_out(vty, "%% Unable to create file `%s'%s",
+				fname, VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+		log_add_target(tgt);
+	}
+
+	vty->index = tgt;
+	vty->node = CFG_LOG_NODE;
+
+	return CMD_SUCCESS;
+}
+
+
+DEFUN(cfg_no_log_file, cfg_no_log_file_cmd,
+	"no log file .FILENAME",
+	NO_STR LOG_STR "Logging to text file\n" "Filename\n")
+{
+	const char *fname = argv[0];
+	struct log_target *tgt;
+
+	tgt = log_target_find(LOG_TGT_TYPE_FILE, fname);
+	if (!tgt) {
+		vty_out(vty, "%% No such log file `%s'%s",
+			fname, VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+
+	log_target_destroy(tgt);
+
+	return CMD_SUCCESS;
+}
+
+static int config_write_log_single(struct vty *vty, struct log_target *tgt)
+{
+	int i;
+	char level_lower[32];
+
+	switch (tgt->type) {
+	case LOG_TGT_TYPE_VTY:
+		return 1;
+		break;
+	case LOG_TGT_TYPE_STDERR:
+		vty_out(vty, "log stderr%s", VTY_NEWLINE);
+		break;
+	case LOG_TGT_TYPE_SYSLOG:
+#ifdef HAVE_SYSLOG_H
+		vty_out(vty, "log syslog %s%s",
+			get_value_string(sysl_level_names,
+					 tgt->tgt_syslog.facility),
+			VTY_NEWLINE);
+#endif
+		break;
+	case LOG_TGT_TYPE_FILE:
+		vty_out(vty, "log file %s%s", tgt->tgt_file.fname, VTY_NEWLINE);
+		break;
+	}
+
+	vty_out(vty, "  logging color %u%s", tgt->use_color ? 1 : 0,
+		VTY_NEWLINE);
+	vty_out(vty, "  logging timestamp %u%s", tgt->print_timestamp ? 1 : 0,
+		VTY_NEWLINE);
+
+	/* stupid old osmo logging API uses uppercase strings... */
+	osmo_str2lower(level_lower, log_level_str(tgt->loglevel));
+	vty_out(vty, "  logging level all %s%s", level_lower, VTY_NEWLINE);
+
+	for (i = 0; i < osmo_log_info->num_cat; i++) {
+		const struct log_category *cat = &tgt->categories[i];
+		char cat_lower[32];
+
+		/* stupid old osmo logging API uses uppercase strings... */
+		osmo_str2lower(cat_lower, osmo_log_info->cat[i].name+1);
+		osmo_str2lower(level_lower, log_level_str(cat->loglevel));
+
+		vty_out(vty, "  logging level %s %s%s", cat_lower, level_lower,
+			VTY_NEWLINE);
+	}
+
+	/* FIXME: levels */
+
+	return 1;
+}
+
+static int config_write_log(struct vty *vty)
+{
+	struct log_target *dbg = vty->index;
+
+	llist_for_each_entry(dbg, &osmo_log_target_list, entry)
+		config_write_log_single(vty, dbg);
+
+	return 1;
+}
+
 void logging_vty_add_cmds()
 {
 	install_element_ve(&enable_logging_cmd);
@@ -348,4 +665,20 @@
 	install_element_ve(&logging_set_category_mask_cmd);
 	install_element_ve(&logging_level_cmd);
 	install_element_ve(&show_logging_vty_cmd);
+
+	install_node(&cfg_log_node, config_write_log);
+	install_element(CFG_LOG_NODE, &cfg_log_fltr_all_cmd);
+	install_element(CFG_LOG_NODE, &cfg_log_use_clr_cmd);
+	install_element(CFG_LOG_NODE, &cfg_log_timestamp_cmd);
+	install_element(CFG_LOG_NODE, &cfg_log_level_cmd);
+
+	install_element(CONFIG_NODE, &cfg_log_stderr_cmd);
+	install_element(CONFIG_NODE, &cfg_no_log_stderr_cmd);
+	install_element(CONFIG_NODE, &cfg_log_file_cmd);
+	install_element(CONFIG_NODE, &cfg_no_log_file_cmd);
+#ifdef HAVE_SYSLOG_H
+	install_element(CONFIG_NODE, &cfg_log_syslog_cmd);
+	install_element(CONFIG_NODE, &cfg_log_syslog_local_cmd);
+	install_element(CONFIG_NODE, &cfg_no_log_syslog_cmd);
+#endif
 }
diff --git a/src/vty/vty.c b/src/vty/vty.c
index a5b16dc..c1a9b3a 100644
--- a/src/vty/vty.c
+++ b/src/vty/vty.c
@@ -765,6 +765,9 @@
 		vty_config_unlock(vty);
 		vty->node = ENABLE_NODE;
 		break;
+	case CFG_LOG_NODE:
+		vty->node = CONFIG_NODE;
+		break;
 	default:
 		/* Unknown node, we have to ignore it. */
 		break;
@@ -1129,6 +1132,9 @@
 		vty_config_unlock(vty);
 		vty->node = ENABLE_NODE;
 		break;
+	case CFG_LOG_NODE:
+		vty->node = CONFIG_NODE;
+		break;
 	default:
 		/* Unknown node, we have to ignore it. */
 		break;