File: log.cc

package info (click to toggle)
performous 1.1%2Bgit20181118-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 11,712 kB
  • sloc: cpp: 30,008; ansic: 2,751; sh: 801; xml: 464; python: 374; makefile: 22
file content (193 lines) | stat: -rw-r--r-- 7,130 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include "log.hh"

#include "fs.hh"
#include <boost/filesystem.hpp>
#include <boost/iostreams/device/file_descriptor.hpp>
#include <boost/iostreams/stream.hpp>
#include <iostream>
#include <memory>
#include <mutex>
#include <stdexcept>

/** \file
 * \brief The std::clog logger.
 *
 * General message format: <tt>subsystem/level: message</tt>
 *
 * Example:
 * \code
 * std::clog << "foo/info: Here's an info message from subsystem foo" << std::endl;
 * \endcode
 *
 * Each message may contain newlines and flushing the stream (i.e. by std::endl or std::flush) must be done
 * when and only when the message is complete.
 *
 * Any lower-case subsystem name including hyphens may be used. The levels, in descending order of priority
 * are as follows:
 *
 * error    A serious and rare message that usually means that a function requested by user cannot be completed.
 * warning  Less critical errors that should still be emitted sparingly (consider using "debug" for repeated warnings).
 * notice   A non-error situation that might still require user attention (the lowest level displayed by default).
 * info     Any information that might be of interest but that does not appear too often and glog the log output.
 * debug    Any information that is flooded so much that it should normally be suppressed.
 *
 * The user may either choose a desired level of messages to emit, or he may choose a specific subsystem (by
 * substring search) to be monitored all the way down to debug level, in which case only errors from any other
 * subsystems will be printed.
 *
 **/

/** \internal
 * Guard to ensure we're atomically printing to cerr.
 * \attention This only guards from multiple clog interleaving, not other console I/O.
 */
std::mutex log_lock;

// Capture stderr spam from other libraries and log it properly
// Note: std::cerr retains its normal functionality but other means of writing stderr get redirected to std::clog
#if defined(__unix__) || defined(__APPLE__)
#include <unistd.h>
#include <future>
struct StderrGrabber {
	boost::iostreams::stream<boost::iostreams::file_descriptor_sink> stream;
	std::streambuf* backup;
	std::future<void> logger;
	StderrGrabber(): stream(dup(STDERR_FILENO), boost::iostreams::close_handle), backup(std::cerr.rdbuf()) {
		std::cerr.rdbuf(stream.rdbuf());  // Make std::cerr write to our stream (which connects to normal stderr)
		int fd[2];
		pipe(fd);  // Create pipe fd[1]->fd[0]
		dup2(fd[1], STDERR_FILENO);  // Close stderr and replace it with a copy of pipe begin
		close(fd[1]);  // Close the original pipe begin
		std::clog << "stderr/info: Standard error output redirected here\n" << std::flush;
		logger = std::async(std::launch::async, [fdpipe = fd[0]] {
			std::string line;
			unsigned count = 0;
			for (char ch; read(fdpipe, &ch, 1) == 1;) {
				line += ch;
				if (ch != '\n') continue;
				std::clog << "stderr/info: " + line << std::flush;
				line.clear(); ++count;
			}
			close(fdpipe);  // Close this end of pipe
			if (count > 0) std::clog << "stderr/notice: " << count << " messages logged to stderr/info\n" << std::flush;
		});
	}
	~StderrGrabber() {
		dup2(stream->handle(), STDERR_FILENO);  // Restore stderr (closes the pipe, terminating the thread)
		std::cerr.rdbuf(backup);  // Restore original rdbuf (that writes to normal stderr)
	}
};
#else
struct StderrGrabber {};  // Not supported on Windows
#endif

std::unique_ptr<StderrGrabber> grabber;

/** \internal The implementation of the stream filter that handles the message filtering. **/
class VerboseMessageSink : public boost::iostreams::sink {
  public:
	std::streamsize write(const char* s, std::streamsize n);
};

// defining them in main() causes segfault at exit as they apparently got free'd before we're done using them
static boost::iostreams::stream_buffer<VerboseMessageSink> sb; //!< \internal
static VerboseMessageSink vsm; //!< \internal

//! \internal used to store the default/original clog buffer.
static std::streambuf* default_ClogBuf = nullptr;

fs::ofstream file;

std::string target;
int minLevel;

void writeLog(std::string const& msg) {
	std::lock_guard<std::mutex> l(log_lock);
	std::cerr << msg << std::flush;
	file << msg << std::flush;
}

int numeric(std::string const& level) {
	if (level == "debug") return 0;
	if (level == "info") return 1;
	if (level == "notice") return 2;
	if (level == "warning") return 3;
	if (level == "error") return 4;
	return -1;
}

std::streamsize VerboseMessageSink::write(const char* s, std::streamsize n) {
	std::string line(s, n);  // Note: s is *not* a c-string, thus we must stop after n chars.
	// Parse prefix as subsystem/level:...
	size_t slash = line.find('/');
	size_t colon = line.find(": ", slash);
	if (slash == std::string::npos || colon == std::string::npos) {
		std::string msg = "logger/error: Invalid log prefix on line [[[\n" + line + "]]]\n";
		write(msg.data(), msg.size());
		return n;
	}
	std::string subsystem(line, 0, slash);
	std::string level(line, slash + 1, colon - slash - 1);
	int lev = numeric(level);
	if (lev == -1) {
		std::string msg = "logger/error: Invalid level '" + level + "' line [[[\n" + line + "]]]\n";
		write(msg.data(), msg.size());
		return n;
	}
	if (lev >= minLevel || (!target.empty() && subsystem.find(target) != std::string::npos)) {
		writeLog(line);
	}
	return n;
}

Logger::Logger(std::string const& level) {
	if (default_ClogBuf) throw std::logic_error("Multiple loggers constructed. There can only be one.");
	if (level.find_first_of(":/_* ") != std::string::npos) throw std::runtime_error("Invalid logging level specified. Specify either a subsystem name (e.g. logger) or a level (debug, info, notice, warning, error).");
	pathBootstrap();  // So that log filename is known...
	std::string msg = "logger/notice: Logging ";
	{
		std::lock_guard<std::mutex> l(log_lock);
		if (level.empty()) {
			minLevel = 2;  // Display all notices, warnings and errors
			msg += "all notices, warnings and errors.";
		} else if (level == "none") {
			minLevel = 100;
			msg += "disabled.";  // No-one will see this, so what's the point? :)
		} else {
			minLevel = numeric(level);
			if (minLevel == -1 /* Not a valid level name */) {
				minLevel = 4;  // Display errors from any subsystem
				target = level;  // All messages from the given subsystem
				msg += "everything from subsystem " + target + " and all errors.";
			} else {
				msg += "any events of " + level + " or higher level.";
			}
		}
		if (minLevel < 100) {
			fs::path name = getLogFilename();
			fs::create_directories(name.parent_path());
			file.open(name);
			msg += " Log file: " + name.string();
		}
		sb.open(vsm);
		default_ClogBuf = std::clog.rdbuf();
		std::clog.rdbuf(&sb);
		atexit(Logger::teardown);
	}
	std::clog << msg << std::endl;
	grabber = std::make_unique<StderrGrabber>();
}

Logger::~Logger() { teardown(); }

void Logger::teardown() {
	grabber.reset();
	if (default_ClogBuf) std::clog << "logger/info: Exiting normally." << std::endl;
	std::lock_guard<std::mutex> l(log_lock);
	if (!default_ClogBuf) return;
	std::clog.rdbuf(default_ClogBuf);
	sb.close();
	file.close();
	default_ClogBuf = nullptr;
}