MARLEY (Model of Argon Reaction Low Energy Yields) v2.0.0
A Monte Carlo event generator for tens-of-MeV neutrino interactions
Loading...
Searching...
No Matches
Logger.cc
1
4//
5// This file is part of MARLEY (Model of Argon Reaction Low Energy Yields)
6//
7// MARLEY is free software: you can redistribute it and/or modify it under the
8// terms of version 3 of the GNU General Public License as published by the
9// Free Software Foundation.
10//
11// For the full text of the license please see COPYING or
12// visit http://opensource.org/licenses/GPL-3.0
13//
14// Please respect the MCnet academic usage guidelines. See GUIDELINES
15// or visit https://www.montecarlonet.org/GUIDELINES for details.
16
17#include <iostream>
18
19#include "marley/Error.hh"
20#include "marley/JSON.hh"
21#include "marley/Logger.hh"
22#include "marley/marley_utils.hh"
23
24const char* marley::Logger::loglevel_to_str(LogLevel lev)
25{
26 switch (lev) {
27 case LogLevel::FATAL:
28 return "[FATAL]: ";
29 break;
30 case LogLevel::ERROR:
31 return "[ERROR]: ";
32 break;
33 case LogLevel::WARN:
34 return "[WARNING]: ";
35 break;
36 case LogLevel::DEBUG:
37 return "[DEBUG]: ";
38 break;
39 case LogLevel::TRACE:
40 return "[TRACE]: ";
41 break;
42 default:
43 return "";
44 break;
45 }
46}
47marley::Logger::LogLevel marley::Logger::string_to_loglevel(
48 const std::string& str )
49{
50 LogLevel result = LogLevel::INFO;
51
52 // Convert the string to all-lowercase to achieve case-insensitive matching
53 std::string low = marley_utils::to_lowercase( str );
54 static const std::unordered_map< std::string, LogLevel > conversion_map = {
55 { "trace", LogLevel::TRACE },
56 { "debug", LogLevel::DEBUG },
57 { "info", LogLevel::INFO },
58 { "notice", LogLevel::NOTICE },
59 { "warn", LogLevel::WARN },
60 { "error", LogLevel::ERROR },
61 { "fatal", LogLevel::FATAL }
62 };
63 auto iter = conversion_map.find( low );
64 if ( iter != conversion_map.end() ) result = iter->second;
65 else throw marley::Error( "Unrecognized marley::Logger logging level \""
66 + str + '\"' );
67 return result;
68}
69
70marley::Logger::OutStream::OutStream( std::shared_ptr< std::ostream > os,
71 LogLevel min, LogLevel max ) : stream_( os )
72{
73 if ( min > max ) throw marley::Error( "Minimum severity more than maximum"
74 " severity in constructor of marley::Logger::OutStream" );
75 min_level_ = min;
76 max_level_ = max;
77}
78
79// Alternative constructor that takes a reference to the std::ostream. Problems
80// when the std::shared_ptr goes out of scope are avoided by providing a custom
81// deleter that does nothing.
82marley::Logger::OutStream::OutStream( std::ostream& os, LogLevel min,
83 LogLevel max ) : OutStream( std::shared_ptr< std::ostream >( &os,
84 [](std::ostream*) -> void {} ), min, max )
85{
86}
87
89
90 if ( config.has_key("out") ) {
91 auto out_array = config.at( "out" );
92 if ( !out_array.is_array() ) {
93 throw marley::Error( "JSON array expected for \"out\" key in"
94 " marley::Logger JSON configuration. Read \""
95 + out_array.dump_string() + '\"' );
96 }
97 auto streams = out_array.array_range();
98 for ( const auto& s : streams ) {
99
100 // Set defaults here that may be overwritten by the configuration
101 LogLevel min = LogLevel::TRACE;
102 LogLevel max = LogLevel::FATAL;
103
104 if ( s.has_key("min") ) {
105 auto jmin = s.at( "min" );
106 if ( !jmin.is_string() ) throw marley::Error("Invalid marley::Logger"
107 " minimum logging level specification " + jmin.dump_string() );
108 min = this->string_to_loglevel( jmin.to_string() );
109 }
110
111 if ( s.has_key("max") ) {
112 auto jmax = s.at( "max" );
113 if ( !jmax.is_string() ) throw marley::Error("Invalid marley::Logger"
114 " maximum logging level specification " + jmax.dump_string() );
115 max = this->string_to_loglevel( jmax.to_string() );
116 }
117
118 bool is_file = s.has_key( "file" );
119 bool is_stream = s.has_key( "stream" );
120 if ( is_file ) {
121 if ( is_stream ) {
122 throw marley::Error( "marley::Logger output stream definition uses"
123 "both the \"stream\" and \"file\" keys" );
124 }
125 auto fname = s.at( "file" );
126 if ( !fname.is_string() ) throw marley::Error("Invalid marley::Logger"
127 " output file specification " + fname.dump_string() );
128
129 // If the user specified a value for the "overwrite" key, use it
130 // to determine whether we should append to the file (false) or
131 // overwrite it (true). Otherwise, assume we want to append to it.
132 auto file_mode = std::ios::out;
133 if ( s.has_key("overwrite") ) {
134
135 auto ow = s.at( "overwrite" );
136
137 bool ok;
138 bool overwrite = ow.to_bool( ok );
139 if ( !ok ) throw marley::Error( "Invalid log file overwrite"
140 " setting \"" + ow.dump_string() + '\"' );
141
142 if ( overwrite ) file_mode |= std::ios::trunc;
143 else file_mode |= std::ios::app;
144 }
145 else file_mode |= std::ios::app;
146
147 auto outfile = std::make_shared< std::ofstream >( fname.to_string(),
148 file_mode );
149 if ( !outfile || (!outfile->good()) ) throw marley::Error( "Unable"
150 " to open the log file \"" + fname.to_string() + "\"" );
151
152 // Create the output stream for the file
153 this->add_stream( outfile, min, max );
154 }
155 else {
156 auto st = s.at( "stream" );
157 if ( !st.is_string() ) throw marley::Error("Invalid marley::Logger"
158 " output stream specification " + st.dump_string() );
159 auto stream_name = st.to_string();
160 if ( stream_name == "stdout" ) {
161 this->add_stream( std::cout, min, max );
162 }
163 else if ( stream_name == "stderr" ) {
164 this->add_stream( std::cerr, min, max );
165 }
166 else throw marley::Error( "Unrecognized stream name \"" + stream_name
167 + "\" encountered in marley::Logger::configure()" );
168 }
169
170 } // stream definitions
171 } // handling of "out" key
172
173 bool set_default_categ = false;
174 if ( !config.has_key("categories") ) {
175 // If the user has not configured any categories then globally set the
176 // logging level to INFO and move on
177 default_level_ = LogLevel::INFO;
178 return;
179 }
180
181 auto categ_spec = config.at( "categories" );
182 if ( !categ_spec.is_object() ) {
183 throw marley::Error( "marley::Logger categories must be specified as"
184 " a JSON object." );
185 }
186 auto categs = categ_spec.object_range();
187 for ( const auto& [ cat, lev ] : categs ) {
188 if ( !lev.is_string() ) {
189 throw marley::Error( "Invalid marley::Logger level specification \""
190 + lev.dump_string() + '\"' );
191 }
192 LogLevel ll = this->string_to_loglevel( lev.to_string() );
193 // The "default" category doesn't appear in the internal map. Instead,
194 // it has a dedicated class member to use as the ultimate fallback.
195 if ( cat == "default" ) {
196 set_default_categ = true;
197 default_level_ = ll;
198 }
199 else if ( cat.empty() ) {
200 throw marley::Error( "Empty name encountered in the category"
201 " configuration for marley::Logger" );
202 }
203 else category_map_[ cat ] = ll;
204 }
205
206 if ( !set_default_categ ) {
207 throw marley::Error( "Missing \"default\" logging level in the category"
208 " configuration for marley::Logger" );
209 }
210
211}
212
214 // This is usually done with the FileManager, but we need to avoid logging
215 // messages in this constructor (to avoid recursive initialization). So we
216 // do it without the FileManager here.
217 char* mar = std::getenv( "MARLEY" );
218 if ( !mar ) throw marley::Error( "The MARLEY environment variable is not"
219 " set. Please set it (e.g., by sourcing the setup_marley.sh script) and"
220 " try again." );
221
222 // This works OK because the marley::JSON class does not use the Logger
223 std::string config_file_name = std::string( mar ) + "/data/config/logger.js";
224 auto json_config = marley::JSON::load_file( config_file_name );
225 this->configure( json_config );
226}
227
229 static Logger instance;
230 return instance;
231}
232
233bool marley::Logger::has_stream( const std::ostream& os ) const {
234 auto stream = get_stream( &os );
235 if ( stream ) return true;
236 // A nullptr was returned, so the stream couldn't be found
237 else return false;
238}
239
240const marley::Logger::OutStream* marley::Logger::get_stream(
241 const std::ostream* os ) const
242{
243 auto end = streams_.end();
244 auto iter = std::find_if( streams_.begin(), end,
245 [ os ]( const OutStream& s ) -> bool { return s.stream_.get() == os; }
246 );
247 if ( iter == end ) return nullptr;
248 else return &( *iter );
249}
250
251marley::Logger::OutStream* marley::Logger::get_stream(
252 const std::ostream* os )
253{
254 auto end = streams_.end();
255 auto iter = std::find_if( streams_.begin(), end,
256 [ os ]( const OutStream& s ) -> bool { return s.stream_.get() == os; }
257 );
258 if ( iter == end ) return nullptr;
259 else return &( *iter );
260}
261
262void marley::Logger::add_stream( std::shared_ptr< std::ostream > stream,
263 LogLevel min, LogLevel max )
264{
265 // Check to see whether we have already added this stream to the logger
266 marley::Logger::OutStream* os = get_stream( stream.get() );
267
268 // If we don't have it yet, then add it with the requested logging level
269 if ( !os ) streams_.emplace_back( stream, min, max );
270
271 // Otherwise, just update the level of the existing one
272 else {
273 os->min_level_ = min;
274 os->max_level_ = max;
275 }
276}
277
278void marley::Logger::add_stream( std::ostream& stream,
279 LogLevel min, LogLevel max )
280{
281 // Check to see whether we have already added this stream to the logger
282 marley::Logger::OutStream* os = get_stream( &stream );
283
284 // If we don't have it yet, then add it with the requested logging level
285 if ( !os ) streams_.emplace_back( stream, min, max );
286
287 // Otherwise, just update the level of the existing one
288 else {
289 os->min_level_ = min;
290 os->max_level_ = max;
291 }
292}
293
295 const std::string& category )
296{
297 std::vector< std::ostream* > active_streams;
298 // The current severity level determines whether the output Message will
299 // accept streamed content at all
300 if ( this->should_emit( category, lev) ) {
301 for( auto& s : streams_ ) {
302 // The Message will send the streamed content only to OutStreams whose
303 // severity levels are configured to accept it
304 if ( lev <= s.max_level_ && lev >= s.min_level_ ) {
305 // Store a pointer to the std::ostream object that will receive output
306 active_streams.push_back( s.stream_.get() );
307 }
308 }
309 }
310
311 // Returns a Message object to receive the output and relay it to the active
312 // OutStreams. Preprends a prefix based on the relevant logging level before
313 // accepting other output.
314 marley::Logger::Message msg( active_streams, &mutex_ );
315 msg << loglevel_to_str( lev );
316 return msg;
317}
318
319marley::Logger::Message& marley::Logger::Message::operator<<( std::ostream&
320 (*manip)(std::ostream&) )
321{
322 buffer_ << manip;
323 return *this;
324}
325
326marley::Logger::Message& marley::Logger::Message::operator<<( std::ios_base&
327 (*manip)(std::ios_base&) )
328{
329 buffer_ << manip;
330 return *this;
331}
332
334 const std::string& category )
335{
336 // First check the cache. If we've already resolved the level for this
337 // category, then just use the result. This avoids unnecessary string
338 // splitting to check parent category settings.
339 auto iter = resolved_level_cache_.find( category );
340 if ( iter != resolved_level_cache_.end() ) return iter->second;
341
342 // Position of the delimiter used to mark category hierarchy separations
343 size_t delim_pos = std::string::npos;
344
345 // We need to resolve the severity level for a new category. Copy the
346 // input so that we can iteratively trim it to scan up the hierarchy.
347 std::string categ( category );
348 do {
349
350 // A value of delim_pos other than std::string::npos signals that we
351 // need to erase the rightmost category name so that we can look up
352 // the severity setting for the immediate parent category below.
353 if ( delim_pos != std::string::npos ) categ.erase( delim_pos );
354
355 // Check for a setting for the current category. The most specific setting
356 // wins, so cache the result and return immediately if one is found
357 const auto cit = category_map_.find( categ );
358 if ( cit != category_map_.cend() ) {
359 LogLevel resolved = cit->second;
360 resolved_level_cache_[ categ ] = resolved;
361 return resolved;
362 }
363
364 // Search for the last category delimiter in the current category string
365 delim_pos = categ.rfind( CATEG_DELIM_ );
366
367 // If one was not found, then exit the loop so we can fall back to the
368 // default severity level
369 } while( delim_pos != std::string::npos );
370
371 // A specific category setting was not found, so fall back to the default
372 // severity level
373 return default_level_;
374}
Base class for all exceptions thrown by MARLEY functions.
Definition Error.hh:26
Temporary object used for forming logger messages.
Definition Logger.hh:172
std::ostringstream buffer_
Definition Logger.hh:228
Simple singleton logging class.
Definition Logger.hh:94
static Logger & Instance()
Get the singleton instance of the Logger class.
Definition Logger.cc:228
bool should_emit(const std::string &category, LogLevel lev)
Returns whether the logger should emit a message for the given category and level.
Definition Logger.hh:337
Logger()
Create the singleton Logger.
Definition Logger.cc:213
LogLevel category_level(const std::string &category)
Looks up the severity setting for the input category, including inheritance from the hierarchy.
Definition Logger.cc:333
void configure(const marley::JSON &json)
Initialize the Logger using settings expressed as a JSON object.
Definition Logger.cc:88
Message log(LogLevel lev, const std::string &category="")
Prepare the Logger to receive a log message via the << stream operator.
Definition Logger.cc:294
LogLevel
Defines the logging levels recognized by the marley::Logger.
Definition Logger.hh:102
bool has_stream(const std::ostream &stream) const
Returns true if stream is already registered with the Logger, or false otherwise.
Definition Logger.cc:233