E-MailRelay
glogoutput.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file glogoutput.cpp
19///
20
21#include "gdef.h"
22#include "glogoutput.h"
23#include "gdatetime.h"
24#include "gscope.h"
25#include "gfile.h"
26#include "ggettext.h"
27#include "gstringview.h"
28#include "groot.h"
29#include "gtest.h"
30#include <algorithm>
31#include <sstream>
32#include <iostream>
33#include <stdexcept>
34#include <string>
35#include <cstring>
36#include <array>
37
38G_LOG_THREAD_LOCAL G::LogOutput * G::LogOutput::m_instance = nullptr ;
39
40namespace G
41{
42 namespace LogOutputImp
43 {
44 constexpr int stdout_fileno = 1 ; // STDOUT_FILENO
45 constexpr int stderr_fileno = 2 ; // STDERR_FILENO
46 std::size_t tellp( LogStream & log_stream )
47 {
48 if( log_stream.m_ostream == nullptr ) return 0U ;
49 log_stream.m_ostream->clear() ;
50 return static_cast<std::size_t>( std::max( std::streampos(0) , log_stream.m_ostream->tellp() ) ) ;
51 }
52 std::string_view info()
53 {
54 static const std::string s( txt("info: ") ) ;
55 return {s.data(),s.size()} ;
56 }
57 std::string_view warning()
58 {
59 static const std::string s( txt("warning: ") ) ;
60 return {s.data(),s.size()} ;
61 }
62 std::string_view error()
63 {
64 static const std::string s( txt("error: ") ) ;
65 return {s.data(),s.size()} ;
66 }
67 std::string_view assertion()
68 {
69 static const std::string s( txt("assertion error: ") ) ;
70 return {s.data(),s.size()} ;
71 }
72 }
73}
74
75G::LogOutput::LogOutput( Private , const std::string & exename , const Config & config ) :
76 m_exename(exename) ,
77 m_config(config) ,
78 m_buffer(m_buffer_size) ,
79 m_streambuf(m_buffer.data(),m_buffer.size()) ,
80 m_stream(&m_streambuf) ,
81 m_fd(m_config.m_stdout?LogOutputImp::stdout_fileno:LogOutputImp::stderr_fileno)
82{
83 updateTime() ;
84}
85
86G::LogOutput::LogOutput( const std::string & exename , const Config & config , const Path & path ) :
87 LogOutput({},exename,config)
88{
89 m_path = path ; // NOLINT initialiser list
90 init() ;
91}
92
93
94#ifndef G_LIB_SMALL
95G::LogOutput::LogOutput( const std::string & exename , const Config & config , int fd ) :
96 LogOutput({},exename,config)
97{
98 m_fd = fd ; // NOLINT initialiser list
99 init() ;
100}
101#endif
102
103#ifndef G_LIB_SMALL
104G::LogOutput::LogOutput( bool enabled , bool verbose , const Path & path ) :
105 LogOutput({},"",{enabled,verbose})
106{
107 m_path = path ; // NOLINT initialiser list
108 init() ;
109}
110#endif
111
112void G::LogOutput::init()
113{
114 updatePath( m_path , m_real_path ) ;
115 open( m_real_path , /*do_throw=*/true ) ;
116 osinit() ;
117 if( m_instance == nullptr )
118 m_instance = this ;
119}
120
122{
123 return m_config ;
124}
125
126#ifndef G_LIB_SMALL
127int G::LogOutput::fd() const noexcept
128{
129 return m_fd ;
130}
131#endif
132
133void G::LogOutput::configure( const Config & config )
134{
135 m_config = config ;
136}
137
139{
140 static_assert( noexcept(m_path.empty()) , "" ) ;
141 static_assert( noexcept(G::File::close(m_fd)) , "" ) ;
142 static_assert( noexcept(oscleanup()) , "" ) ;
143
144 if( m_instance == this )
145 {
146 m_instance = nullptr ;
147 }
148 if( !m_path.empty() && m_fd >= 0 &&
149 m_fd != LogOutputImp::stderr_fileno &&
150 m_fd != LogOutputImp::stdout_fileno )
151 {
152 G::File::close( m_fd ) ;
153 }
154 oscleanup() ;
155}
156
158{
159 return m_instance ;
160}
161
162void G::LogOutput::context( std::string_view (*fn)(void *) , void * fn_arg ) noexcept
163{
164 LogOutput * p = instance() ;
165 if( p )
166 {
167 p->m_context_fn = fn ;
168 p->m_context_fn_arg = fn_arg ;
169 }
170}
171
172#ifndef G_LIB_SMALL
174{
175 LogOutput * p = instance() ;
176 return p ? p->m_context_fn_arg : nullptr ;
177}
178#endif
179
180bool G::LogOutput::at( Severity severity ) const noexcept
181{
182 bool do_output = m_config.m_output_enabled ;
183 if( severity == Severity::Debug )
184 do_output = m_config.m_output_enabled && m_config.m_debug ;
185 else if( severity == Severity::InfoSummary )
186 do_output = m_config.m_output_enabled && m_config.m_summary_info ;
187 else if( severity == Severity::InfoVerbose )
188 do_output = m_config.m_output_enabled && m_config.m_verbose_info ;
189 else if( severity == Severity::InfoMoreVerbose )
190 do_output = m_config.m_output_enabled && m_config.m_more_verbose_info ;
191 return do_output ;
192}
193
194G::LogStream G::LogOutput::start( Severity severity , const char * , int ) noexcept
195{
196 try
197 {
198 if( instance() )
199 return instance()->start( severity ) ; // not noexcept
200 else
201 return LogStream( nullptr ) ;
202 }
203 catch(...)
204 {
205 return LogStream( nullptr ) ; // is noexcept
206 }
207}
208
209void G::LogOutput::output( LogStream & s ) noexcept
210{
211 try
212 {
213 if( instance() )
214 instance()->output( s , 0 ) ; // not noexcept
215 }
216 catch(...)
217 {
218 }
219}
220
221bool G::LogOutput::updatePath( const Path & path_in , Path & path_out ) const
222{
223 bool changed = false ;
224 if( !path_in.empty() )
225 {
226 Path new_path_out = makePath( path_in ) ;
227 changed = new_path_out != path_out ;
228 path_out.swap( new_path_out ) ;
229 }
230 return changed ;
231}
232
233G::Path G::LogOutput::makePath( const Path & path_in ) const
234{
235 // this is called at most hourly (see updateTime()), so not optimised
236 std::string_view yyyymmdd( m_time_buffer.data() , 8U ) ;
237 std::string_view hh( m_time_buffer.data()+9 , 2U ) ;
238 Path path_out = path_in ;
239 path_out.replace( "%d" , yyyymmdd , /*ex_root=*/true ) ;
240 path_out.replace( "%h" , hh , /*ex_root=*/true ) ;
241 return path_out ;
242}
243
244void G::LogOutput::open( const Path & path , bool do_throw )
245{
246 if( !path.empty() )
247 {
248 int fd = -1 ;
249 {
250 Process::Umask set_umask( m_config.m_umask ) ;
251 Root claim_root ;
252 fd = File::open( path , File::InOutAppend::Append ) ;
253 if( fd < 0 && do_throw )
254 throw LogFileError( path.str() ) ;
255 }
256 if( fd >= 0 )
257 {
258 if( m_fd >= 0 && m_fd != LogOutputImp::stderr_fileno && m_fd != LogOutputImp::stdout_fileno )
259 G::File::close( m_fd ) ;
260 m_fd = fd ;
261 }
262 }
263}
264
265G::LogStream G::LogOutput::start( Severity severity )
266{
267 m_depth++ ;
268 if( m_depth > 1 )
269 return LogStream( nullptr ) ;
270
271 if( updateTime() && updatePath(m_path,m_real_path) )
272 open( m_real_path , false ) ;
273
274 m_stream.reset() ;
275 LogStream log_stream( &m_stream ) ;
276
277 log_stream << std::dec ;
278 if( !m_exename.empty() )
279 log_stream << m_exename << ": " ;
280 if( m_config.m_with_timestamp )
281 appendTimeTo( log_stream ) ;
282 if( m_config.m_with_level )
283 log_stream << levelString( severity ) ;
284 if( m_config.m_with_context && m_context_fn )
285 log_stream << (*m_context_fn)( m_context_fn_arg ) ;
286
287 m_start_pos = LogOutputImp::tellp( log_stream ) ;
288 m_severity = severity ;
289 return log_stream ;
290}
291
292void G::LogOutput::output( LogStream & log_stream , int )
293{
294 // reject nested logging
295 if( m_depth ) m_depth-- ;
296 if( m_depth ) return ;
297
298 char * buffer = m_buffer.data() ;
299 std::size_t n = LogOutputImp::tellp( log_stream ) ;
300
301 // elipsis on overflow
302 if( n >= m_buffer_base_size )
303 {
304 static_assert( m_rhs_margin > 4U , "" ) ;
305 char * margin = buffer + m_buffer_base_size ;
306 margin[0] = ' ' ;
307 margin[1] = '.' ;
308 margin[2] = '.' ;
309 margin[3] = '.' ;
310 n = m_buffer_base_size + 4U ;
311 }
312
313 // strip the first word from the text - expected to be the method name
314 char * p = buffer ;
315 if( m_config.m_strip )
316 {
317 char * end = buffer + n ;
318 char * text = buffer + m_start_pos ;
319 char * space = std::find( text , end , ' ' ) ;
320 if( space != end && (space+1) != end )
321 {
322 // (move the preamble forwards)
323 p = std::copy_backward( buffer , text , space+1 ) ;
324 n -= (p-buffer) ;
325 }
326 }
327
328 // last-ditch removal of ansi escape sequences
329 p[n] = '\0' ;
330 for( char * pp = std::strchr(buffer,'\033') ; pp ; pp = std::strchr(p+1,'\033') )
331 *pp = '.' ;
332
333 if( m_fd == LogOutputImp::stdout_fileno )
334 std::cout.flush() ;
335
336 // do the actual output in an o/s-specific manner -- the margin
337 // allows the implementation to extend the text with eg. a newline
338 osoutput( m_fd , m_severity , p , n ) ;
339}
340
341void G::LogOutput::assertionFailure( LogOutput * instance , const char * file , int line , const char * test_expression ) noexcept
342{
343 // ('noexcept' on this fn so we std::terminate() if any of this throws)
344 if( instance )
345 {
346 // (not strictly thread-safe, but we are std::abort()ing anyway)
347 static std::array<char,m_buffer_size> buffer ;
348 omembuf streambuf( buffer.data() , buffer.size() ) ;
349 Stream stream( &streambuf ) ;
350 LogStream log_stream( &stream ) ;
351
352 log_stream << LogOutputImp::assertion() << basename(file) << "(" << line << "): " << test_expression ;
353 char * p = buffer.data() ;
354 std::size_t n = std::min( std::size_t(m_buffer_base_size) , LogOutputImp::tellp(log_stream) ) ; // (size_t cast is gcc workround)
355 instance->osoutput( instance->m_fd , Severity::Assertion , p , n ) ;
356 }
357 else
358 {
359 std::cerr << LogOutputImp::assertion() << basename(file) << "(" << line << "): " << test_expression << std::endl ;
360 }
361}
362
364{
365 std::abort() ;
366}
367
368bool G::LogOutput::updateTime()
369{
371 m_time_us = now.us() ;
372 bool new_hour = false ;
373 if( m_time_s == 0 || m_time_s != now.s() || m_time_buffer[0] == '\0' )
374 {
375 m_time_s = now.s() ;
376 m_time_buffer[0] = '\0' ;
377 now.local().format( m_time_buffer.data() , m_time_buffer.size() , "%Y%m%d.%H%M%S." ) ;
378 m_time_buffer[16U] = '\0' ;
379
380 new_hour = 0 != std::memcmp( m_time_change_buffer.data() , m_time_buffer.data() , 11U ) ;
381
382 static_assert( sizeof(m_time_change_buffer) == sizeof(m_time_buffer) , "" ) ;
383 std::memcpy( m_time_change_buffer.data() , m_time_buffer.data() , m_time_buffer.size() ) ;
384 }
385 return new_hour ;
386}
387
388void G::LogOutput::appendTimeTo( LogStream & log_stream )
389{
390 log_stream
391 << m_time_buffer.data()
392 << static_cast<char>( '0' + ( ( m_time_us / 100000U ) % 10U ) )
393 << static_cast<char>( '0' + ( ( m_time_us / 10000U ) % 10U ) )
394 << static_cast<char>( '0' + ( ( m_time_us / 1000U ) % 10U ) )
395 << ": " ;
396}
397
398const char * G::LogOutput::basename( const char * file ) noexcept
399{
400 if( file == nullptr ) return "" ;
401 const char * p1 = std::strrchr( file , '/' ) ;
402 const char * p2 = std::strrchr( file , '\\' ) ;
403 return p1 > p2 ? (p1+1) : (p2?(p2+1):file) ;
404}
405
406std::string_view G::LogOutput::levelString( Severity s ) noexcept
407{
408 namespace imp = LogOutputImp ;
409 if( s == Severity::Debug ) return "debug: " ;
410 else if( s == Severity::InfoSummary ) return imp::info() ;
411 else if( s == Severity::InfoVerbose ) return imp::info() ;
412 else if( s == Severity::InfoMoreVerbose ) return imp::info() ;
413 else if( s == Severity::Warning ) return imp::warning() ;
414 else if( s == Severity::Error ) return imp::error() ;
415 else if( s == Severity::Assertion ) return imp::assertion() ;
416 return "" ;
417}
418
419// ==
420
421G::LogOutput::Config::Config() noexcept
422= default;
423
424G::LogOutput::Config::Config( bool enabled , bool verbose ) noexcept :
425 m_output_enabled(enabled),
426 m_summary_info(enabled),
427 m_verbose_info(verbose),
428 m_more_verbose_info(verbose),
429 m_debug(verbose)
430{
431}
432
bool format(char *out, std::size_t out_size, const char *fmt) const
Puts the formatted date, including a terminating null character, into the given output buffer.
Definition: gdatetime.cpp:232
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:56
static void close(int fd) noexcept
Calls ::close() or equivalent.
Definition: gfile_unix.cpp:159
Controls and implements low-level logging output, as used by G::Log.
Definition: glogoutput.h:62
LogStream start(Severity, const char *file, int line) noexcept
Returns an ostream for a new log line.
Definition: glogoutput.cpp:194
bool at(Severity) const noexcept
Returns true if logging should occur for the given severity level.
Definition: glogoutput.cpp:180
static GDEF_NORETURN_LHS void assertionAbort() GDEF_NORETURN_RHS
Aborts the program when an assertion has failed.
Definition: glogoutput.cpp:363
LogOutput(const std::string &exename, const Config &config, const Path &logfile={})
Constructor.
Definition: glogoutput.cpp:86
static void assertionFailure(LogOutput *, const char *file, int line, const char *test_string) noexcept
Reports an assertion failure.
Definition: glogoutput.cpp:341
void context(std::string_view(*fn)(void *)=nullptr, void *fn_arg=nullptr) noexcept
Sets a functor that is used to provide a context string for every log line, if configured.
Definition: glogoutput.cpp:162
Config config() const noexcept
Returns the current configuration.
Definition: glogoutput.cpp:121
void * contextarg() noexcept
Returns the functor argument as set by the last call to context().
Definition: glogoutput.cpp:173
void output(LogStream &) noexcept
Emits the current log line (see start()).
Definition: glogoutput.cpp:209
~LogOutput()
Destructor.
Definition: glogoutput.cpp:138
void configure(const Config &)
Updates the current configuration.
Definition: glogoutput.cpp:133
static LogOutput * instance() noexcept
Returns a pointer to the controlling LogOutput object.
Definition: glogoutput.cpp:157
int fd() const noexcept
Returns the output file descriptor.
Definition: glogoutput.cpp:127
A Path object represents a file system path.
Definition: gpath.h:82
void swap(Path &other) noexcept
Swaps this with other.
Definition: gpath.cpp:506
bool replace(const std::string_view &from, const std::string_view &to, bool ex_root=false)
Replaces the first occurrence of 'from' with 'to', optionally excluding the root part.
Definition: gpath.cpp:420
bool empty() const noexcept
Returns true if the path is empty.
Definition: gpath.h:237
Represents a unix-epoch time with microsecond resolution.
Definition: gdatetime.h:140
static SystemTime now()
Factory function for the current time.
Definition: gdatetime.cpp:328
std::time_t s() const noexcept
Returns the number of seconds since the start of the epoch.
Definition: gdatetime.cpp:380
unsigned int us() const
Returns the microsecond fraction.
Definition: gdatetime.cpp:374
BrokenDownTime local() const
Returns the locale-dependent local broken-down time.
Definition: gdatetime.cpp:356
bool enabled() noexcept
Returns true if pop code is built in.
Low-level classes.
Definition: garg.h:36
const char * txt(const char *p) noexcept
A briefer alternative to G::gettext().
Definition: ggettext.h:74
A configuration structure for G::LogOutput.
Definition: glogoutput.h:95
A non-throwing copyable wrapper for std::ostream, used by G::LogOutput and associated logging macros.
Definition: glogstream.h:38