diff --git a/Changes b/Changes new file mode 100644 index 0000000..9f692c2 --- /dev/null +++ b/Changes @@ -0,0 +1,20 @@ +Version history: + +1.5.1 2019-04-24 'the new small features' + - add asynchronous implementation of handle and writer + - add parameters for %trait placeholder + - add %color placeholder + - add reusing handles from previous config file read + - add possibility to backup and restore NDC and MDC values + - add out-buffer parameter to std and file handle in configuration file + - improve internal exceptions behaviour + +1.4.2 2019-04-10 'better config file and zero length' + - add length=0 support in %level placeholder + - add support of using 'custom' arguments in custom's args in configuration file + +1.4.1 2019-04-08 'callframe in writer pattern' + - add callframe support in writer pattern + +1.3.1 2019-04-07 'The first public release' + - Production ready log library \ No newline at end of file diff --git a/META6.json b/META6.json index 26516af..f85eab8 100644 --- a/META6.json +++ b/META6.json @@ -6,14 +6,14 @@ ], "description": "Full customisable logging library inspired by idea of separate logging and its configuration.", "resources": [], - "version": "1.4.2", + "version": "1.5.1", "tags": [ "LOG", "LOGGING", "LOGGER" ], "license": "Artistic-2.0", - "source-url": "https://github.com/atroxaper/p6-LogP6", + "source-url": "git://github.com/atroxaper/p6-LogP6.git", "provides": { "LogP6::Wrapper::SyncAbstract": "lib/LogP6/Wrapper/SyncAbstract.pm6", "LogP6::LoggerPure": "lib/LogP6/LoggerPure.pm6", diff --git a/README.md b/README.md index 56f29e1..9502232 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ even in your own libraries. - [WriterConf](#writerconf) - [Standard WriterConf](#standard-writerconf) - [Pattern](#pattern) + - [Async writing](#async-writing) - [Writer factory subroutines](#writer-factory-subroutines) - [Writer configuration file](#writer-configuration-file) - [Filter configuration](#filter-configuration) @@ -210,8 +211,11 @@ current date-time and so from the context. Logger has the following methods: - `trait()` - returns logger trait; -- `ndc-push($obj)`, `ndc-pop()`, `ndc-clean()` - work with NDC; -- `mdc-put($key, $obj)`, `mdc-remove($key)`, `mdc-clean()` - work with MDC; +- `ndc-push($obj)`, `ndc-pop()`, `ndc-clean()` - work with `NDC`; +- `mdc-put($key, $obj)`, `mdc-remove($key)`, `mdc-clean()` - work with `MDC`; +- `dc-copy()`, `dc-restore($dc-copy)` - make copy of `NDC` and `MDC` and restore +them from copy. The methods are useful when you want to share NDC and MDC values +across multiple threads. - `trace(*@args, :$x)`, `debug(*@args, :$x)`, `info(*@args, :$x)`, `warn(*@args, :$x)`, `error(*@args, :$x)` - logging the arguments with specified importance log level. `:$x` is an optional exception argument. `@args` - data @@ -364,7 +368,19 @@ placeholder name. Pattern can has the following placeholders: -- `%trait` - for name of logger trait; +- `%trait`, `%trait{short=[package-delimeter]number}`, `%trait{sprintf=pattern}` - +for name of logger trait. Additionally you can specify one of two options of +trait representation. `sprintf` option is useful for traits like `database`, +`audit` or so, when you want to represent all traits with the same length. For +example, `[%trait{sprintf=%s7}]` can be converted into `[ audit]`. `short` +option is useful for traits like `Module::Packge1::Package2::Class`. You can +specify package delimiter (instead of `::`) and how many packages will be +displayed. For example, `%trait{short=[.]1` can be converted into +`Class`, `%trait{short=[.]-1` - into `Packge1.Package2.Class` and +`%trait{short=[.]2.4` - into `Modu.Pack.Package2.Class`. If `number` is a +positive integer then only `number` right elements will be displayed. If +`number` is a negative integer then `|number|` left elements will be deleted. If +`number` is real then left elements will be cut to fractional symbols; - `%tid` - for current `Thread` id; - `%tname` - for current `Thread` name; - `%msg` - for log message; @@ -378,10 +394,21 @@ name, `$trace` - optional exception stacktrace. For example, - `%level{WARN=W DEBUG=D ERROR=E TRACE=T INFO=I length=2}` - log importance level. By default logger will use level name in upper case but you can specify synonyms for all or part of them in curly brackets in format -`=`. Also you can specify a fixed length of log level -name. Default length is 0 - write level as is. For example +`=`. You can specify a fixed length of log level name. +Default length is 0 - write level as is. For example `'[%level{WARN=hmm ERROR=alarm length=5}]'` can be converted into `'[hmm ]'`, `'[alarm]'`, `'[INFO ]'`, `'[DEBUG]'`; +- `%color{TRACE=yellow DEBUG=green INFO=blue WARN=magenta ERROR=red}` - colorize +log string after that placeholder. You can specify color for any log level. +Level you not specified color will be use its default color (as in example +above). For example, `%color{ERROR=green}` means +`%color{TRACE=yellow DEBUG=green INFO=blue WARN=magenta ERROR=green}`. You can +use `yellow`, `green`, `blue`, `magenta`, `green` color names or color code +(more [information](https://misc.flogisoft.com/bash/tip_colors_and_formatting). +For example `%color{TRACE=35 DEBUG=30;48;5;82 INFO=green}`. You can use `%color` +placeholder several times; +- `%color{reset}` or `%creset` - reset log string colorizing after that +placeholder; - `%date{$yyyy-$yy-$MM-$MMM-$dd $hh:$mm:$ss:$mss $z}` - current date and time. String in curly brackets is used as subpattern. @@ -398,9 +425,25 @@ at the same log call line; `callframe().code.name` in log call block; Note that using `%framefile`, `%frameline` or `%framename` in the pattern will -slow your program because it requires several `callframe` calls on each +slow your logging because it requires several `callframe()` calls on each resultative log call; +### Async writing + +`LogP6` provides writer and handle implementation for asynchronous writing. + +You can use `LogP6::Handle::Async.new(IO::Handle :$delegate!, Scheduler :$scheduler = $*SCHEDULER)` +as handle which will schedule `WRITE` method call of `delegate` handle. + +If is it not enough to wrap a handle then you can wrap whole writer. Use +`LogP6::WriterConf::Async.new(LogP6::WriterConf :$delegate!, Scheduler :$scheduler = $*SCHEDULER), :$name, Bool :$need-callframe)` +as writer configuration of another configuration. Final writer will schedule +`write` method call of `delegate` created writer with copy of current +`logger context`. If you miss a `:name` parameter then `delegate`'s name will +be used. Pass boolean parameter `need-callframe` if you plan to use callframe +information in wrapped writer. Note that using callframe will slow your logging +because it requires several `callframe()` calls on each resultative log call. + ### Writer factory subroutines `LogP6` module has the following subs for manage writers configurations: @@ -430,8 +473,8 @@ array. Only `std` (for standard configuration) and `custom` types are supported. In case of standard configuration all field are optional excepts `name`. Handle can be: -- `file` type for output into file. You can specify `path` and `append` -arguments; +- `file` type for output into file. You can specify `path`,`append` (`True` +by default) and `out-buffer` arguments; - `std` type for output into `$*OUT` or `$*ERR`. You can specify `path` as `out` or `err`. - `custom` type. @@ -1030,17 +1073,15 @@ subroutines logic then make a special sub for retrieve logger like # ROADMAP -- Make IO::Handle for write log in databases; - Make IO::Handle rollover support - change log file after some period of time or after file size limit are reached; -- Add Writer for asynchronous writing; - Add a `Cro::Transform` for using `LogP6` in `cro` applications. # AUTHOR Mikhail Khorkov -Source can be located at: https://github.com/atroxaper/p6-LogP6. Comments and Pull Requests +Source can be located at: [github](https://github.com/atroxaper/p6-LogP6). Comments and Pull Requests are welcome. # COPYRIGHT AND LICENSE diff --git a/TODO.md b/TODO.md index 348e946..e3a3ef5 100644 --- a/TODO.md +++ b/TODO.md @@ -10,16 +10,16 @@ High priority: 1. ~~add `length=0` in `%level` for exact length. make it default~~ Medium priority: -1. use a better exceptions instead of 'die' -1. add params for %trait in pattern (short, long variant) -1. try make entities be really immutable (filters, writes, loggers) +1. ~~use a better exceptions instead of 'die'~~ +1. ~~add params for %trait in pattern (short, long variant)~~ 1. ~~add database writer~~ Low priority: 1. add 'turn off/on cliche' factory method 1. add trace-some methods in logger (like 'returns value', 'enter method with') -1. add backup/restore ndc and mdc +1. try make entities be really immutable (filters, writes, loggers) +1. ~~add backup/restore ndc and mdc~~ Archive: diff --git a/lib/LogP6.pm6 b/lib/LogP6.pm6 index 0a2a45d..f53c293 100644 --- a/lib/LogP6.pm6 +++ b/lib/LogP6.pm6 @@ -22,6 +22,8 @@ use LogP6::ThreadLocal; use LogP6::ConfigFile; use LogP6::LogGetter; +use LogP6::Exceptions; + our $trace is export(:configure) = Level::trace; our $debug is export(:configure) = Level::debug; our $info is export(:configure) = Level::info; @@ -31,9 +33,11 @@ our $error is export(:configure) = Level::error; my Lock $lock .= new; my atomicint $initialized = 0; +my LogP6::ConfigFile $config-file; + my @cliches; my $cliches-names; -my %cliches-to-loggers; +my %cliches-to-traits; my $loggers-pure = %(); my $loggers = $(); @@ -66,24 +70,40 @@ sub try-initialize() { sub init-from-file($config-path) is export(:configure) { $lock.protect({ + my $prev-initialized = ⚛$initialized; $initialized ⚛= 1; init-getter( get-wrap => sub ($t) { get-logger($t) }, get-pure => sub ($t) { get-logger-pure($t) }); - wipe-log-config(); - - return without $config-path; + without $config-path { + wipe-log-config(); + return; + } unless $config-path.trim.Bool { - die "log-p6 config-path is blank"; + logp6-error('log-p6 config-path is blank'); + wipe-log-config(); + return; } if !$config-path.IO.e { - die "log-p6 config '$config-path' is not exist"; + logp6-error("log-p6 config '$config-path' is not exist"); + wipe-log-config(); + return; } - my $config = parse-config($config-path); + my $config; + try { + $config-file //= LogP6::ConfigFile.new; + $config = $config-file.parse-config($config-path); + CATCH { default { + logp6-error($_); + wipe-log-config() if $prev-initialized == 0; + return; + }} + } + wipe-log-config(); set-default-pattern($_) with $config.default-pattern; set-default-auto-exceptions($_) with $config.default-auto-exceptions; set-default-handle($_) with $config.default-handle; @@ -94,6 +114,7 @@ sub init-from-file($config-path) is export(:configure) { writer($_) for $config.writers; filter($_) for $config.filters; cliche($_) for $config.cliches; + CATCH { default { logp6-error($_); .resume } } }); } @@ -111,12 +132,12 @@ sub create-default-cliche() { sub clean-all-settings() { @cliches = []; $cliches-names = SetHash.new; - %cliches-to-loggers = %(); + %cliches-to-traits = %(); $loggers-pure = %(); $loggers = %(); $default-pattern = '[%date{$hh:$mm:$ss}][%level] %msg'; - die "wrong default lib pattern <$($default-pattern)>" + die "wrong default pattern <$($default-pattern)>" unless Grammar.parse($default-pattern); $default-auto-exceptions = True; $default-handle = $*OUT; @@ -575,7 +596,6 @@ multi sub cliche(Str:D :$name!, :$remove! where *.so --> LogP6::Cliche) { my $old = @cliches.grep(*.name eq $name).first // LogP6::Cliche; @cliches = @cliches.grep(*.name ne $name).Array; $cliches-names{$name} = False; - %cliches-to-loggers = %(); update-loggers; $old; }); @@ -641,22 +661,7 @@ sub check-pattern(Str $pattern) { sub find-cliche-with(Str:D $name!, Str:D $type where * ~~ any('writer', 'filter') --> List:D ) { - @cliches.grep(*.has($name, $type)).list; -} - -multi sub update-loggers(Positional:D $cliches) { - for |$cliches -> $cliche { - for (%cliches-to-loggers{$cliche.name} // SetHash.new).keys -> $trait { - create-and-store-logger($trait); - } - } -} - -multi sub update-loggers() { - my @traits := atomic-fetch($loggers).keys.List; - for @traits -> $trait { - create-and-store-logger($trait); - } + @cliches.grep(*.has($name, $type)).List; } sub change-cliche($old-cliche, $new-cliche) { @@ -670,10 +675,10 @@ sub change-cliche($old-cliche, $new-cliche) { } sub create-logger($trait, $cliche) { - my $grooves = (0...^$cliche.writers.elems).list.map(-> $i { ( + my $grooves = (0...^$cliche.writers.elems).List.map(-> $i { ( get-writer($cliche.writers[$i]).make-writer(|writer-defaults($cliche)), get-filter($cliche.filters[$i]).make-filter(|filter-defaults($cliche)) - ) }).list; + ) }).List; return $grooves.elems > 0 ?? LogP6::LoggerPure.new(:$trait, :$grooves) !! LogP6::LoggerMute.new(:$trait); @@ -710,26 +715,23 @@ sub find-cliche-for-trait($trait) { } sub get-logger(Str:D $trait --> Logger:D) is export(:MANDATORY) { - my %logs = atomic-fetch($loggers); - return $_ with %logs{$trait}; + return $_ with atomic-fetch($loggers){$trait}; $lock.protect({ try-initialize(); - return $_ with $loggers{$trait}; - create-and-store-logger($trait); - $loggers{$trait} + return find-or-create-and-store-logger($trait); }); + CATCH { default { logp6-error($_); return LogP6::LoggerMute.new(:$trait) } } } sub get-logger-pure(Str:D $trait --> Logger:D) is export(:configure) { - my %logs = atomic-fetch($loggers-pure); - return $_ with %logs{$trait}; + return $_ with atomic-fetch($loggers-pure){$trait}; $lock.protect({ try-initialize(); - return $_ with atomic-fetch($loggers-pure){$trait}; - create-and-store-logger($trait); + return find-or-create-and-store-logger-pure($trait); }); + CATCH { default { logp6-error($_); return LogP6::LoggerMute.new(:$trait) } } } sub remove-logger(Str:D $trait --> Logger) is export(:configure) { @@ -748,20 +750,83 @@ sub remove-logger(Str:D $trait --> Logger) is export(:configure) { }); } -sub create-and-store-logger($trait) { +multi sub update-loggers(Positional:D $cliches) { + for |$cliches -> $cliche { + my %copy = %cliches-to-traits{$cliche.name} // SetHash.new; + %cliches-to-traits{$cliche.name} = SetHash.new; + for %copy.keys -> $trait { + logger-map-del($trait); + logger-pure-map-del($trait); + find-or-create-and-store-logger($trait); + } + } +} + +multi sub update-loggers() { + %cliches-to-traits = %(); + logger-map-clean(); + my @traits := logger-pure-map-clean(); + for @traits -> $trait { + find-or-create-and-store-logger($trait); + } +} + +sub find-or-create-and-store-logger-pure($trait) { + return $_ with atomic-fetch($loggers-pure){$trait}; my $cliche = find-cliche-for-trait($trait); + create-and-store-loggers($trait, $cliche, :pure); +} + +sub find-or-create-and-store-logger($trait) { + return $_ with atomic-fetch($loggers){$trait}; + my $cliche = find-cliche-for-trait($trait); + create-and-store-loggers($trait, $cliche, :!pure); +} + +sub create-and-store-loggers($trait, $cliche, :$pure) { my $logger-pure = create-logger($trait, $cliche); + my $logger = wrap-logger($logger-pure, $cliche); + logger-map-put($trait, $logger); + logger-pure-map-put($trait, $logger-pure); + (%cliches-to-traits{$cliche.name} //= SetHash.new){$trait} = True; - my %new-loggers = atomic-fetch($loggers).clone; + return $pure ?? $logger-pure !! $logger; +} + +sub logger-pure-map-put($trait, $logger-pure) { my %new-loggers-pure = atomic-fetch($loggers-pure).clone; - %new-loggers{$trait} = wrap-logger($logger-pure, $cliche); %new-loggers-pure{$trait} = $logger-pure; - (%cliches-to-loggers{$cliche.name} //= SetHash.new){$trait} = True; + atomic-assign($loggers-pure, %new-loggers-pure); +} +sub logger-map-put($trait, $logger) { + my %new-loggers = atomic-fetch($loggers).clone; + %new-loggers{$trait} = $logger; atomic-assign($loggers, %new-loggers); +} + +sub logger-pure-map-del($trait) { + my %new-loggers-pure = atomic-fetch($loggers-pure).clone; + %new-loggers-pure{$trait}:delete; atomic-assign($loggers-pure, %new-loggers-pure); +} + +sub logger-map-del($trait) { + my %new-loggers = atomic-fetch($loggers).clone; + %new-loggers{$trait}:delete; + atomic-assign($loggers, %new-loggers); +} + +sub logger-pure-map-clean() { + my $keys = atomic-fetch($loggers-pure).keys.List; + atomic-assign($loggers-pure, %()); + return $keys; +} - return $logger-pure; +sub logger-map-clean() { + my $keys = atomic-fetch($loggers).keys.List; + atomic-assign($loggers, %()); + return $keys; } END { diff --git a/lib/LogP6/Cliche.pm6 b/lib/LogP6/Cliche.pm6 index 17c9129..56328b0 100644 --- a/lib/LogP6/Cliche.pm6 +++ b/lib/LogP6/Cliche.pm6 @@ -30,9 +30,9 @@ class LogP6::Cliche { ) { my $new-writers = $!writers; my $new-filters = $!filters; - $new-writers = $new-writers.map(-> $w { $w eq $old ?? $new !! $w }).list + $new-writers = $new-writers.map(-> $w { $w eq $old ?? $new !! $w }).List if $type eq 'writer'; - $new-filters = $new-filters.map(-> $f { $f eq $old ?? $new !! $f }).list + $new-filters = $new-filters.map(-> $f { $f eq $old ?? $new !! $f }).List if $type eq 'filter'; self.clone(writers => $new-writers, filters => $new-filters); } diff --git a/lib/LogP6/ConfigFile.pm6 b/lib/LogP6/ConfigFile.pm6 index 502cf70..c8e7960 100644 --- a/lib/LogP6/ConfigFile.pm6 +++ b/lib/LogP6/ConfigFile.pm6 @@ -1,5 +1,3 @@ -unit module LogP6::ConfigFile; - use JSON::Fast; use LogP6::WriterConf::Std; @@ -32,241 +30,276 @@ sub default-config-path() is export { } } -sub parse-config(IO() $file-path) is export { - CATCH { - default { - die "Cannot read and create config from file $file-path. Cause " - ~ $_.^name ~ ': ' ~ $_.Str +class LogP6::ConfigFile { + has %!current-cache = %(); + has %!previous-cache = %(); + + method parse-config(LogP6::ConfigFile:D: IO() $file-path) is export { + CATCH { + default { + die "Cannot read and create config from file $file-path. Cause " + ~ $_.^name ~ ': ' ~ $_.gist, "\n"; + } } - } - return LogP6::Config.new unless $file-path.e; - my $file-content = slurp($file-path).trim; - die "config file $file-path is empty" if $file-content.chars == 0; - my \conf = from-json($file-content); + return LogP6::Config.new unless $file-path.e; + my $file-content = slurp($file-path).trim; + die "config file $file-path is empty" if $file-content.chars == 0; + my \conf = from-json($file-content); - return LogP6::Config.new( - writers => list(conf, &writer), - filters => list(conf, &filter), - cliches => list(conf, &cliche), - default-pattern => string(conf), - default-auto-exceptions => bool(conf), - default-handle => handle(conf), - default-x-pattern => string(conf), - default-level => level(conf), - default-first-level-check => bool(conf), - default-wrapper => wrapper(conf) - ); -} + my $config = LogP6::Config.new( + writers => self!list(conf, 'writer'), + filters => self!list(conf, 'filter'), + cliches => self!list(conf, 'cliche'), + default-pattern => self!string-e(conf), + default-auto-exceptions => self!bool(conf), + default-handle => self!handle(conf), + default-x-pattern => self!string-e(conf), + default-level => self!level(conf), + default-first-level-check => self!bool(conf), + default-wrapper => self!wrapper(conf) + ); -sub list(\json, &each) { - return () without json; - return json.map({each($_)}).list; -} + %!previous-cache = %!current-cache; + %!current-cache = %(); -sub writer(\json) { - given json { - when 'std' { - return LogP6::WriterConf::Std.new( - name => json // Str, - pattern => json // Str, - handle => handle(json), - auto-exceptions => bool(json) - ); - } - when 'custom' { - return custom(json); - } - default { - die "Wrong writer type $_"; - } + return $config; } - json.Str; -} -sub filter(\json) { - given json { - when 'std' { - return LogP6::FilterConf::Std.new( - name => json // Str, - level => level(json), - first-level-check => json // Bool, - before-check => list(json, &custom), - after-check => list(json, &custom) - ); - } - when 'custom' { - return custom(json); + method !list(\json, $each) { + return () without json; + return json.map({self!"$each"($_)}).eager.List; + } + + method !writer(\json) { + given json { + when 'std' { + return LogP6::WriterConf::Std.new( + name => json // Str, + pattern => self!string-e(json), + handle => self!handle(json), + auto-exceptions => self!bool(json) + ); + } + when 'custom' { + return self!custom(json); + } + default { + die "Wrong writer type $_"; + } } - default { - die "Wrong filter type $_"; + } + + method !filter(\json) { + given json { + when 'std' { + return LogP6::FilterConf::Std.new( + name => json // Str, + level => self!level(json), + first-level-check => json // Bool, + before-check => self!list(json, 'custom'), + after-check => self!list(json, 'custom') + ); + } + when 'custom' { + return self!custom(json); + } + default { + die "Wrong filter type $_"; + } } } - json.Str; -} -sub bool(\json) { - return Bool without json; - return json if json ~~ Bool; - return json eq 'true'; -} + method !bool(\json) { + return Bool without json; + return json if json ~~ Bool; + return json eq 'true'; + } -sub string(\json) { - return json.Str with json; - return Str; -} + method !string(\json) { + return json.Str with json; + return Str; + } -sub cliche(\json) { - my $name = json; - die 'Missing cliche\'s name' without $name; - return LogP6::Cliche.new( - name => $name, - matcher => matcher(json), - default-pattern => string(json), - default-auto-exceptions => bool(json), - default-handle => handle(json), - default-x-pattern => string(json), - default-level => level(json), - default-first-level-check => bool(json), - grooves => list(json, &string), - wrapper => wrapper(json) - ); -} + method !string-e(\json) { + return json.Str.trans(['\e', '\a', '\n'] => ["\e", "\a", "\n"]) with json; + return Str; + } -sub handle(\json) { - return IO::Handle without json; - given json { - when 'std' { - my $path = json; - given $path { - when 'out' { - return $*OUT; - } - when 'err' { - return $*ERR; - } - default { - die "Wrong std handle path $path"; + method !cliche(\json) { + my $name = json; + die 'Missing cliche\'s name' without $name; + return LogP6::Cliche.new( + name => $name, + matcher => self!matcher(json), + default-pattern => self!string-e(json), + default-auto-exceptions => self!bool(json), + default-handle => self!handle(json), + default-x-pattern => self!string-e(json), + default-level => self!level(json), + default-first-level-check => self!bool(json), + grooves => self!list(json, 'string'), + wrapper => self!wrapper(json) + ); + } + + method !handle(\json) { + return IO::Handle without json; + given json { + when 'std' { + my $path = json; + my $result; + given $path { + when 'out' { + $result = $*OUT; + } + when 'err' { + $result = $*ERR; + } + default { + die "Wrong std handle path $path"; + } } + $result.out-buffer = $_ with json; + return $result; + } + when 'file' { + my $path = json; + my $out-buffer = json; + die 'Missing file handle path' without $path; + return self!produce(:use-cache, json, sub { + my $mode = (json // True) ?? :a !! :w; + my $handle = $path.IO.open(|$mode, :out-buffer($out-buffer // Nil)); + return $handle; + }); + } + when 'custom' { + return self!custom(json, :use-cache); + } + default { + die "Wrong handle type $_"; + ; } - } - when 'file' { - my $path = json; - die 'Missing file handle path' without $path; - my $append = json; - my $not-append = ($append eqv False); - return $path.IO.open(:create, append => !$not-append); - } - when 'custom' { - return custom(json); - } - default { - die "Wrong handle type $_";; } } -} -sub any(\json) { - return list(json, &any) if json ~~ Positional; - if json ~~ Associative { - return custom(json) if json ~~ 'custom'; - return associative(json); + method !any(\json) { + return self!list(json, 'any') if json ~~ Positional; + if json ~~ Associative { + return self!custom(json) if json ~~ 'custom'; + return self!associative(json); + } + return json; } - return json; -} -sub associative(\json) { - return %() without json; - return Any unless json ~~ Associative; - my %result = %(); - return json.kv.map(-> $k, $v { $k => any($v) }).Hash; -} + method !associative(\json) { + return %() without json; + return Any unless json ~~ Associative; + my %result = %(); + return json.kv.map(-> $k, $v { $k => self!any($v) }).Hash; + } -sub custom(\json) { - my \my-require = json; - die 'Missing \'require\' field in custom definition' without my-require; + method !custom(\json, :$use-cache) { + my \my-require = json; + die 'Missing \'require\' field in custom definition' without my-require; - my \fqn-method = json; - my \fqn-class = json; - die "Missing both 'fqn-method' and 'fqn-class' fields in custom definition" - if !fqn-method.defined && !fqn-class.defined; - die "Defined both 'fqn-method' and 'fqn-class' fields in custom definition" - if fqn-method.defined && fqn-class.defined; + my \fqn-method = json; + my \fqn-class = json; + die "Missing both 'fqn-method' and 'fqn-class' fields in custom definition" + if !fqn-method.defined && !fqn-class.defined; + die "Defined both 'fqn-method' and 'fqn-class' fields in custom definition" + if fqn-method.defined && fqn-class.defined; - my \args = associative(json); - die "'args' field are not Associative in cusom definition" - unless args ~~ Associative; + my \args = self!associative(json); + die "'args' field are not Associative in cusom definition" + unless args ~~ Associative; - require ::(my-require); - with fqn-method { - my &method = ::(fqn-method); - return method(|args); - } - with fqn-class { - my $class-name = ::(fqn-class); - return $class-name.new(|args); + return self!produce(:$use-cache, json, sub { + require ::(my-require); + with fqn-method { + my &method = ::(fqn-method); + return method(|args); + } + with fqn-class { + my $class-name = ::(fqn-class); + return $class-name.new(|args); + } + }); } -} -sub level(\json) { - return LogP6::Level without json; - given json { - when 'trace' { return trace; } - when 'debug' { return debug; } - when 'info' { return info; } - when 'warn' { return warn; } - when 'error' { return error; } - default { die 'wrong level value ' ~ json; } + method !level(\json) { + return LogP6::Level without json; + given json { + when 'trace' { return LogP6::Level::trace; } + when 'debug' { return LogP6::Level::debug; } + when 'info' { return LogP6::Level::info; } + when 'warn' { return LogP6::Level::warn; } + when 'error' { return LogP6::Level::error; } + default { die 'wrong level value ' ~ json; } + } } -} -sub matcher(\json) { - die 'Missing cliche\'s matcher' without json; - my $matcher = json.Str; - given $matcher { - when /^ \/ .+ \/ $/ { - my $substr = $matcher.substr(1, *-1); - return / <$substr> /; - } - default { - return $matcher; + method !matcher(\json) { + die 'Missing cliche\'s matcher' without json; + my $matcher = json.Str; + given $matcher { + when /^ \/ .+ \/ $/ { + my $substr = $matcher.substr(1, *-1); + return / <$substr> /; + } + default { + return $matcher; + } } } -} -sub wrapper(\json) { - return LogP6::Wrapper without json; - given json { - when 'time' { - die "Missing 'seconds' field in time wrapper-factory" - without json; - return custom(%( - :type, - :require, - :fqn-class, - args => %( - seconds => json, - config-path => json - ) - )); - } - when 'each' { - return custom(%( - :type, - :require, - :fqn-class, - args => %( - config-path => json - ) - )); - } - when 'transparent' { - return LogP6::Wrapper::Transparent::Wrapper.new; - } - when 'custom' { - return custom(json); + method !wrapper(\json) { + return LogP6::Wrapper without json; + given json { + when 'time' { + die "Missing 'seconds' field in time wrapper-factory" + without json; + return self!custom(%( + :type, + :require, + :fqn-class, + args => %( + seconds => json, + config-path => json + ) + )); + } + when 'each' { + return self!custom(%( + :type, + :require, + :fqn-class, + args => %( + config-path => json + ) + )); + } + when 'transparent' { + return LogP6::Wrapper::Transparent::Wrapper.new; + } + when 'custom' { + return self!custom(json); + } + defined { + die 'wrong wrapper type value ' ~ json; + } } - defined { - die 'wrong wrapper type value ' ~ json; + } + + method !produce(\json, &factory, :$use-cache) { + return factory() unless $use-cache; + my $key = to-json(json, :sorted-keys); + with %!previous-cache{$key} { + %!current-cache{$key} = $_; + return $_; } + return $_ with %!current-cache{$key}; + my $result = factory(); + %!current-cache{$key} = $result; + return $result; } } \ No newline at end of file diff --git a/lib/LogP6/Context.pm6 b/lib/LogP6/Context.pm6 index 6aa6a1a..86f75cd 100644 --- a/lib/LogP6/Context.pm6 +++ b/lib/LogP6/Context.pm6 @@ -26,6 +26,26 @@ submethod BUILD() { $!tname = $!thread.name; } +submethod TWEAK( + :$msg, :$date, :$level, :$x, :$trait, :@ndc, :%mdc, :$callframe +) { + $!msg = $msg; + $!date = $date; + $!level = $level; + $!x = $x; + $!trait = $trait; + @!ndc := @ndc; + %!mdc = %mdc; + $!callframe = $callframe; +} + +method copy() { + return LogP6::Context.new( + :$!msg, :$!date, :$!level, :$!x, :$!trait, + :ndc(@!ndc.clone), :mdc(%!mdc.clone), :$!callframe + ); +} + #|[Gets Context object for current Thread. # Throw exception in case Thread does not have a Context for now. # It is not recommend to use the method as part of LogP6 API. @@ -127,6 +147,17 @@ method mdc-clean() { %!mdc = %(); } +#| Get copy of NDC and MDC +method dc-get() { + %('ndc' => @!ndc.clone, 'mdc' => %!mdc.clone); +} + +#| Restore values of NDC and MDC from its copy +method dc-restore($dc) { + @!ndc := $dc // []; + %!mdc = $dc // %(); +} + #|[Gets current DataTime.now value. The value will be cache until the date will #| be set to undefined value by .date-set(), .date-clean() or clean() methods. #| Normally the date value are reset before each logging without addition user's diff --git a/lib/LogP6/Exceptions.pm6 b/lib/LogP6/Exceptions.pm6 index 18dd9d1..5dadd2e 100644 --- a/lib/LogP6/Exceptions.pm6 +++ b/lib/LogP6/Exceptions.pm6 @@ -4,3 +4,11 @@ class X::LogP6::PatternIsNotValid is Exception { "Wrong writer pattern format: <$!pattern>"; } } + +multi sub logp6-error(Exception:D $x) is export { + $*ERR.print('LogP6 error: ', $x.^name, ': ', $x.message, "\n", $x.backtrace); +} + +multi sub logp6-error(Str:D $x) is export { + $*ERR.say('LogP6 error: ', $x); +} \ No newline at end of file diff --git a/lib/LogP6/Filter/Std.pm6 b/lib/LogP6/Filter/Std.pm6 index cbc95d7..6f4ccf2 100644 --- a/lib/LogP6/Filter/Std.pm6 +++ b/lib/LogP6/Filter/Std.pm6 @@ -7,23 +7,6 @@ class LogP6::Filter::Std does LogP6::Filter { has List:D $.before-check is required; has List:D $.after-check is required; - only method new(LogP6::FilterConf:D $conf, *%defaults) { - my $first-level-check = $conf.first-level-check // - %defaults; - my $level = $conf.level // %defaults; - my &level-check := chose-level-check($level); - my $before = ($conf.before-check // ()).Array; - $before = $first-level-check - ?? [&level-check].push(|$before) - !! $before.push(&level-check); - $before = $before.list; - self.bless( - reactive-level => $first-level-check ?? $level !! LogP6::Level::trace, - before-check => $before, - after-check => $conf.after-check // (), - ); - } - method reactive-level() { $!reactive-level; } @@ -40,20 +23,4 @@ class LogP6::Filter::Std does LogP6::Filter { return unless $check($context); } } - - sub chose-level-check($need-level) { - given $need-level { - when LogP6::Level::trace { return &trace-level-check } - when LogP6::Level::debug { return &debug-level-check } - when LogP6::Level::info { return &info-level-check } - when LogP6::Level::warn { return &warn-level-check } - when LogP6::Level::error { return &error-level-check } - } - } - - sub trace-level-check($context) { LogP6::Level::trace <= $context.level } - sub debug-level-check($context) { LogP6::Level::debug <= $context.level } - sub info-level-check($context) { LogP6::Level::info <= $context.level } - sub warn-level-check($context) { LogP6::Level::warn <= $context.level } - sub error-level-check($context) { LogP6::Level::error <= $context.level } } diff --git a/lib/LogP6/FilterConf/Std.pm6 b/lib/LogP6/FilterConf/Std.pm6 index 8cf3f2f..b8d9abe 100644 --- a/lib/LogP6/FilterConf/Std.pm6 +++ b/lib/LogP6/FilterConf/Std.pm6 @@ -17,6 +17,37 @@ class LogP6::FilterConf::Std does LogP6::FilterConf { } method make-filter(*%defaults --> LogP6::Filter:D) { - LogP6::Filter::Std.new(self, |%defaults); + my $first-level-check = $!first-level-check // + %defaults; + my $level = $!level // %defaults; + my $reactive-level = $first-level-check ?? $level !! LogP6::Level::trace; + my &level-check := chose-level-check($level); + my $before = ($!before-check // ()).Array; + $before = $first-level-check + ?? [&level-check].push(|$before) + !! $before.push(&level-check); + $before = $before.List; + + LogP6::Filter::Std.new( + :$reactive-level, + before-check => $before, + after-check => $!after-check // (), + ); + } + + sub chose-level-check($need-level) { + given $need-level { + when LogP6::Level::trace { return &trace-level-check } + when LogP6::Level::debug { return &debug-level-check } + when LogP6::Level::info { return &info-level-check } + when LogP6::Level::warn { return &warn-level-check } + when LogP6::Level::error { return &error-level-check } + } } + + sub trace-level-check($context) { LogP6::Level::trace <= $context.level } + sub debug-level-check($context) { LogP6::Level::debug <= $context.level } + sub info-level-check($context) { LogP6::Level::info <= $context.level } + sub warn-level-check($context) { LogP6::Level::warn <= $context.level } + sub error-level-check($context) { LogP6::Level::error <= $context.level } } diff --git a/lib/LogP6/Handle/Async.pm6 b/lib/LogP6/Handle/Async.pm6 new file mode 100644 index 0000000..558d9b0 --- /dev/null +++ b/lib/LogP6/Handle/Async.pm6 @@ -0,0 +1,24 @@ +#|[Async IO::Handle which delegates all WRITE calls to another handle in +#| another threads.] +class LogP6::Handle::Async is IO::Handle { + has IO::Handle $.delegate is required; + has Scheduler $.scheduler; + + submethod TWEAK() { + self.encoding: $!delegate.encoding; + $!scheduler = $*SCHEDULER without $!scheduler; + } + + method WRITE(IO::Handle:D: Blob:D \data --> Bool:D) { + $!scheduler.cue({ $!delegate.WRITE(data) }); + True; + } + + method close() { + $!delegate.close; + } + + method READ(|) { #`[do nothing] } + + method EOF { #`[do nothing] } +} diff --git a/lib/LogP6/Handle/AsyncSingle.pm6 b/lib/LogP6/Handle/AsyncSingle.pm6 new file mode 100644 index 0000000..7415a88 --- /dev/null +++ b/lib/LogP6/Handle/AsyncSingle.pm6 @@ -0,0 +1,38 @@ +#|[Async IO::Handle which delegate all WRITE calls to another handle in +#| one separate thread.] +class LogP6::Handle::AsyncSingle is IO::Handle { + has IO::Handle $.delegate is required; + has Scheduler $.scheduler; + has Channel $!queue; + has Promise $!executor; + + submethod TWEAK() { + self.encoding: $!delegate.encoding; + $!scheduler = $*SCHEDULER without $!scheduler; + $!queue .= new; + $!executor .= new; + $!scheduler.cue({ + react { + whenever $!queue -> \blob { + $!delegate.WRITE(blob); + } + } + $!executor.keep(True); + }); + } + + method WRITE(IO::Handle:D: Blob:D \data --> Bool:D) { + $!queue.send(data) unless $!queue.closed; + True; + } + + method close() { + $!queue.close; + await $!executor; + $!delegate.close; + } + + method READ(|) { #`[do nothing] } + + method EOF { #`[do nothing] } +} \ No newline at end of file diff --git a/lib/LogP6/LogGetter.pm6 b/lib/LogP6/LogGetter.pm6 index 0d1747a..a1094bd 100644 --- a/lib/LogP6/LogGetter.pm6 +++ b/lib/LogP6/LogGetter.pm6 @@ -1,8 +1,6 @@ -#|[Module with subs for getting wrapped and pure logger. -#| The modules are created for avoid circular dependencies +#|[Subs for getting wrapped and pure logger. +#| The subs are created for avoid circular dependencies #| (many modules depends on LogP6 module).] -unit module LogP6::LogGetter; - my &my-get-wrap; my &my-get-pure; diff --git a/lib/LogP6/Logger.pm6 b/lib/LogP6/Logger.pm6 index e7b9ea1..9c79497 100644 --- a/lib/LogP6/Logger.pm6 +++ b/lib/LogP6/Logger.pm6 @@ -18,6 +18,10 @@ role LogP6::Logger { method mdc-remove($key) { ... } #| Cleans MDC method mdc-clean() { ... } + #| Get copy of NDC and MDC + method dc-copy() { ... } + #| Restore values of NDC and MDC from its copy + method dc-restore($dc) { ... } #|[Writes log with trace importance level. #| @args - data for logging. If the array has more then one element then the #| first element is used as format for sprintf sub and the rest element as diff --git a/lib/LogP6/LoggerPure.pm6 b/lib/LogP6/LoggerPure.pm6 index 944d76d..1c7240b 100644 --- a/lib/LogP6/LoggerPure.pm6 +++ b/lib/LogP6/LoggerPure.pm6 @@ -1,6 +1,7 @@ use LogP6::Level; use LogP6::Logger; use LogP6::FilterConf::Std; +use LogP6::Exceptions; class LogP6::LoggerPure does LogP6::Logger { has Str:D $.trait is required; @@ -14,51 +15,78 @@ class LogP6::LoggerPure does LogP6::Logger { @$!grooves.map(-> $g {$g[1].reactive-level // LogP6::Level::trace}).min; } + method trait() { + $!trait; + } + method ndc-push($obj) { + CATCH { default { logp6-error($_) } } get-context.ndc-push: $obj; } method ndc-pop() { - get-context.ndc-pop; + try { + CATCH { default { logp6-error($_) } } + return get-context.ndc-pop; + } } method ndc-clean() { + CATCH { default { logp6-error($_) } } get-context.ndc-clean; } method mdc-put($key, $obj) { + CATCH { default { logp6-error($_) } } get-context.mdc-put: $key, $obj; } method mdc-remove($key) { + CATCH { default { logp6-error($_) } } get-context.mdc-remove: $key; } method mdc-clean() { + CATCH { default { logp6-error($_) } } get-context.mdc-clean; } + method dc-copy() { + CATCH { default { logp6-error($_) } } + get-context.dc-get; + } + + method dc-restore($dc) { + CATCH { default { logp6-error($_) } } + get-context.dc-restore($dc); + } + method trace(*@args, :$x) { + CATCH { default { logp6-error($_) } } return if $!reactive-level > LogP6::Level::trace; self!log(LogP6::Level::trace, @args, :$x); } method debug(*@args, :$x) { + CATCH { default { logp6-error($_) } } return if $!reactive-level > LogP6::Level::debug; self!log(LogP6::Level::debug, @args, :$x); } method info(*@args, :$x) { + CATCH { default { logp6-error($_) } } return if $!reactive-level > LogP6::Level::info; self!log(LogP6::Level::info, @args, :$x); } method warn(*@args, :$x) { + CATCH { default { logp6-error($_) } } return if $!reactive-level > LogP6::Level::warn; self!log(LogP6::Level::warn, @args, :$x); } method error(*@args, :$x) { + CATCH { default { logp6-error($_) } } return if $!reactive-level > LogP6::Level::error; self!log(LogP6::Level::error, @args, :$x); } @@ -87,12 +115,15 @@ class LogP6::LoggerPure does LogP6::Logger { class LogP6::LoggerMute does LogP6::Logger { has Str:D $.trait is required; + method trait() { $!trait } method ndc-push($obj) {} method ndc-pop() {} method ndc-clean() {} method mdc-put($key, $obj) {} method mdc-remove($key) {} method mdc-clean() {} + method dc-copy() { Nil } + method dc-restore($dc) {} method trace(*@args, :$x) { get-context().clean } method debug(*@args, :$x) { get-context().clean } method info(*@args, :$x) { get-context().clean } diff --git a/lib/LogP6/Wrapper/SyncAbstract.pm6 b/lib/LogP6/Wrapper/SyncAbstract.pm6 index a97c776..6c10561 100644 --- a/lib/LogP6/Wrapper/SyncAbstract.pm6 +++ b/lib/LogP6/Wrapper/SyncAbstract.pm6 @@ -40,11 +40,13 @@ class LogP6::Wrapper::SyncAbstract does LogP6::Logger { method mdc-put($key, $obj) { $!aggr.mdc-put($key, $obj) } method mdc-remove($key) { $!aggr.mdc-remove($key) } method mdc-clean() { $!aggr.mdc-clean() } - method trace(*@args, :$x) { self.sync(get-context); $!aggr.trace(|@args, :$x)} - method debug(*@args, :$x) { self.sync(get-context); $!aggr.debug(|@args, :$x)} - method info(*@args, :$x) { self.sync(get-context); $!aggr.info(|@args, :$x)} - method warn(*@args, :$x) { self.sync(get-context); $!aggr.warn(|@args, :$x)} - method error(*@args, :$x) { self.sync(get-context); $!aggr.error(|@args, :$x)} + method dc-copy() { $!aggr.dc-copy } + method dc-restore($dc) { $!aggr.dc-restore($dc) } + method trace(*@args, :$x) { self.sync(get-context); $!aggr.trace(|@args, :$x)} + method debug(*@args, :$x) { self.sync(get-context); $!aggr.debug(|@args, :$x)} + method info(*@args, :$x) { self.sync(get-context); $!aggr.info(|@args, :$x)} + method warn(*@args, :$x) { self.sync(get-context); $!aggr.warn(|@args, :$x)} + method error(*@args, :$x) { self.sync(get-context); $!aggr.error(|@args, :$x)} } #|[Wrapper logic for synchronize a configuration and a logger. diff --git a/lib/LogP6/Wrapper/SyncTime.pm6 b/lib/LogP6/Wrapper/SyncTime.pm6 index 89f048e..c3ad0e3 100644 --- a/lib/LogP6/Wrapper/SyncTime.pm6 +++ b/lib/LogP6/Wrapper/SyncTime.pm6 @@ -1,6 +1,7 @@ use LogP6::Wrapper; use LogP6::Wrapper::SyncAbstract; use LogP6::LogGetter; +use LogP6::Exceptions; #| Wrapper for synchronize a logger each X seconds itself. class LogP6::Wrapper::SyncTime is LogP6::Wrapper::SyncAbstract { @@ -20,6 +21,7 @@ class LogP6::Wrapper::SyncTime is LogP6::Wrapper::SyncAbstract { self.update-aggr; self.put-sync-obj($now); } + CATCH { default { logp6-error($_) } } } } diff --git a/lib/LogP6/Writer/Async.pm6 b/lib/LogP6/Writer/Async.pm6 new file mode 100644 index 0000000..9c1a2e8 --- /dev/null +++ b/lib/LogP6/Writer/Async.pm6 @@ -0,0 +1,16 @@ +use LogP6::Writer; + +#| Async writer which delegates writing to another writer in another threads +class LogP6::Writer::Async does LogP6::Writer { + has LogP6::Writer $.delegate is required; + has Scheduler $.scheduler is required; + has Bool $.need-callframe is required; + + method write($context) { + # initialize date and callframe before copy context + $context.date(); + $context.callframe() if $!need-callframe; + my $copy = $context.copy; + $!scheduler.cue({ $!delegate.write($copy) }); + } +} \ No newline at end of file diff --git a/lib/LogP6/Writer/Std.pm6 b/lib/LogP6/Writer/Std.pm6 index d1f3cc0..8bbcfdd 100644 --- a/lib/LogP6/Writer/Std.pm6 +++ b/lib/LogP6/Writer/Std.pm6 @@ -8,17 +8,6 @@ class LogP6::Writer::Std does LogP6::Writer { has @!pieces; - only method new(LogP6::WriterConf:D $conf, *%defaults) { - my $auto-exeptions = $conf.auto-exceptions // - %defaults; - my $pattern = $conf.pattern // %defaults; - $pattern ~= %defaults if $auto-exeptions; - self.bless( - pattern => $pattern, - handle => $conf.handle // %defaults - ); - } - submethod TWEAK() { @!pieces := Grammar.parse($!pattern, actions => Actions).made; } diff --git a/lib/LogP6/WriterConf/Async.pm6 b/lib/LogP6/WriterConf/Async.pm6 new file mode 100644 index 0000000..8e00fcd --- /dev/null +++ b/lib/LogP6/WriterConf/Async.pm6 @@ -0,0 +1,35 @@ +use LogP6::WriterConf; +use LogP6::Writer::Async; + +#| Conf for Async writer which delegates writing to another writer in another +#| threads. +class LogP6::WriterConf::Async does LogP6::WriterConf { + has LogP6::WriterConf $.delegate is required; + has Str $.name; + has Scheduler $.scheduler; + has Bool $.need-callframe; + + submethod TWEAK() { + $!name = $!delegate.name without $!name; + } + + method clone-with-name($name --> LogP6::WriterConf:D) { + return self.clone(:$name); + } + + method make-writer(*%defaults --> LogP6::Writer:D) { + LogP6::Writer::Async.new( + :delegate($!delegate.make-writer(|%defaults)), + :scheduler($!scheduler // $*SCHEDULER), + :$!need-callframe + ); + } + + method name(--> Str) { + $!name; + } + + method close(--> Nil) { + $!delegate.close; + } +} diff --git a/lib/LogP6/WriterConf/Pattern.pm6 b/lib/LogP6/WriterConf/Pattern.pm6 index a149420..835a831 100644 --- a/lib/LogP6/WriterConf/Pattern.pm6 +++ b/lib/LogP6/WriterConf/Pattern.pm6 @@ -1,5 +1,3 @@ -unit module LogP6::WriterConf::Pattern; - use LogP6::Level; role PatternPart { @@ -10,6 +8,49 @@ class Trait does PatternPart { method show($context) { $context.trait } } +role TraitParam does PatternPart { + # not need eviction. if we use trait ones then we use it all program lifetime + has %!cache = %(); + + method calculate($trait) { ... } + + method show($context) { + my $trait = $context.trait; + %!cache{$trait} //= self.calculate($trait); + } +} + +class TraitShort does TraitParam { + has $.separator; + has $.minus; + has $.length; + has $.abreviature; + + method calculate($trait) { + my $parts = $trait.split('::').List; + my $elems = $parts.elems; + if $!length >= $elems || $!length == 0 { + return $parts.join($!separator); + } + my $middle = $!minus ?? $!length !! $elems - $!length; + return $parts.kv.map(-> $i, $p { + if $i < $middle { + $!abreviature ?? substr($p, 0, $!abreviature) !! Any; + } else { + $p; + } + }).grep(*.defined).join($!separator); + } +} + +class TraitSprintf does TraitParam { + has $.placeholder; + + method calculate($trait) { + sprintf($!placeholder, $trait); + } +} + class Tid does PatternPart { method show($context) { $context.tid } } @@ -125,13 +166,23 @@ class FrameName does PatternPart { } my $lnames = []; -$lnames[trace.Int] = 'TRACE'; -$lnames[debug.Int] = 'DEBUG'; -$lnames[info.Int] = 'INFO'; -$lnames[warn.Int] = 'WARN'; -$lnames[error.Int] = 'ERROR'; +$lnames[LogP6::Level::trace.Int] = 'TRACE'; +$lnames[LogP6::Level::debug.Int] = 'DEBUG'; +$lnames[LogP6::Level::info.Int] = 'INFO'; +$lnames[LogP6::Level::warn.Int] = 'WARN'; +$lnames[LogP6::Level::error.Int] = 'ERROR'; $lnames .= List; +my $color = []; +$color[LogP6::Level::trace.Int] = "33"; # yellow +$color[LogP6::Level::debug.Int] = "32"; # green; +$color[LogP6::Level::info.Int] = "34"; # blue; +$color[LogP6::Level::warn.Int] = "35"; # magenta; +$color[LogP6::Level::error.Int] = "31"; # red; +$color .= List; + +my $code = %(:33yellow, :32green, :34blue, :35magenta, :31red); + class LevelName does PatternPart { has $.levels; @@ -139,7 +190,7 @@ class LevelName does PatternPart { my $levels = $lnames.clone.Array; my $length = $conf // 0; for 1..5 -> $i { - $levels[$i] = $conf{$i.Str} // $levels[$i]; + $levels[$i] = $conf{$i} // $levels[$i]; $levels[$i] = sprintf('%-*.*s', $length, $length, $levels[$i]) if $length > 0; } @@ -152,12 +203,44 @@ class LevelName does PatternPart { } } +class Color { ... } +class ColorReset { ... } + +role ColorFactory { + method create($conf) { + return ColorReset if $conf{'reset'}; + my $colors = $color.clone.Array; + for 1..5 -> $i { + $colors[$i] = $conf{$i} // $colors[$i]; + $colors[$i] = "\e[" ~ $colors[$i] ~ 'm'; + } + Color.new(colors => $colors.List); + } +} + +class ColorReset does ColorFactory does PatternPart { + method show($context) { "\e[0m" } +} + +class Color does ColorFactory does PatternPart { + has $.colors; + + method show($context) { + $!colors[$context.level]; + } +} + grammar Grammar is export { token TOP { * } proto token item { * } - # %trait - logger name (trait) - token item:sym { '%trait' } + # %trait{short=[delimiter]number printf=%6s} - logger name (trait) + token item:sym { '%trait'? } + token trait-params { \{ \} } + proto rule trait-param { * } + rule trait-param:sym + { 'short' '=' '[' $=<-[\]]>+ ']' } + rule trait-param:sym { 'sprintf' '=' } # %tid - thread id token item:sym { '%tid' } # %tname - thread name @@ -208,14 +291,60 @@ grammar Grammar is export { token item:sym { '%frameline' } # %framename - frame code name token item:sym { '%framename' } + # %color{TRACE=yellow DEBUG=green INFO=blue WARN=magenta ERROR=red} + # %color{reset} %creset + token item:sym { '%color'? } + token item:sym { '%creset' } + token color-params { \{ \} } + proto token color-param { * } + token color-param:sym { + } + token color-param:sym { 'reset' } + proto rule color-level-param { * } + rule color-level-param:sym { 'TRACE' '=' } + rule color-level-param:sym { 'DEBUG' '=' } + rule color-level-param:sym { 'INFO' '=' } + rule color-level-param:sym { 'WARN' '=' } + rule color-level-param:sym { 'ERROR' '=' } + proto token color { * } + token color:sym + { $=('black' | 'white' | 'yellow' | 'green' | 'blue' | 'magenta' | 'red') } + token color:sym { (';')* } token word { $=<-[\s}]>+ } + token minus { '-' } token num { $=\d+ } + token fract { '.' } + token real-num { ??} } class Actions is export { - method TOP($/) { make $>>.made.List } - method item:sym($/) { make Trait } + method TOP($/) { + my $items = $>>.made.List; + my $first = $items.reverse.first(* ~~ ColorFactory); + with $first { + if $first ~~ ColorReset { + make $items; + } else { + make (|$items, ColorReset).List; + } + } else { + make $items; + } + } + method item:sym($/) { + with $ { + make $.made + } else { + make Trait + } + } + method trait-params($/) { make $.made } + method trait-param:sym($/) { + make TraitShort.new(:separator($.Str), |$.made); + } + method trait-param:sym($/) { + make TraitSprintf.new(:placeholder($.Str)) + } method item:sym($/) { make Tid } method item:sym($/) { make Tname } method item:sym($/) { make Msg } @@ -267,13 +396,46 @@ class Actions is export { } } method level-params($/) { make $>>.made.hash } - method level-param:sym($/) { make trace.Int.Str => $.Str } - method level-param:sym($/) { make debug.Int.Str => $.Str } - method level-param:sym($/) { make info.Int.Str => $.Str } - method level-param:sym($/) { make warn.Int.Str => $.Str } - method level-param:sym($/) { make error.Int.Str => $.Str } - method level-param:sym($/) { make 'length' => $.Str } + method level-param:sym($/) { make Level::trace.Int => $.Str } + method level-param:sym($/) { make Level::debug.Int => $.Str } + method level-param:sym($/) { make Level::info.Int => $.Str } + method level-param:sym($/) { make Level::warn.Int => $.Str } + method level-param:sym($/) { make Level::error.Int => $.Str } + method level-param:sym($/) { make 'length' => $.made.Str } method item:sym($/) { make FrameFile } method item:sym($/) { make FrameLine } method item:sym($/) { make FrameName } + method item:sym($/) { + with $ { + make ColorFactory.create($.made); + } else { + make ColorFactory.create(%()); + } + } + method item:sym($/) { make ColorReset } + method color-params($/) { make $.made } + method color-param:sym($/) { make $>>.made.hash } + method color-param:sym($/) { make 'reset' => True } + method color-level-param:sym($/) + { make Level::trace.Int => $.made } + method color-level-param:sym($/) + { make Level::debug.Int => $.made } + method color-level-param:sym($/) + { make Level::info.Int => $.made } + method color-level-param:sym($/) + { make Level::warn.Int => $.made } + method color-level-param:sym($/) + { make Level::error.Int => $.made } + method color:sym($/) { make $code{$.Str} } + method color:sym($/) { make $/.Str } + method minus($/) { make True } + method num($/) { make $.Int } + method fract($/) { make $.made } + method real-num($/) { + make %( + :length($.made), + :abreviature($.made // 0), + :minus($.made // False) + ); + } } \ No newline at end of file diff --git a/lib/LogP6/WriterConf/Std.pm6 b/lib/LogP6/WriterConf/Std.pm6 index ae9b6d8..66fbd0d 100644 --- a/lib/LogP6/WriterConf/Std.pm6 +++ b/lib/LogP6/WriterConf/Std.pm6 @@ -24,7 +24,12 @@ class LogP6::WriterConf::Std does LogP6::WriterConf { } method make-writer(*%defaults --> LogP6::Writer:D) { - LogP6::Writer::Std.new(self, |%defaults); + my $auto-ex = $!auto-exceptions // %defaults; + my $pattern = $!pattern // %defaults; + $pattern ~= %defaults if $auto-ex; + my $handle = $!handle // %defaults; + + LogP6::Writer::Std.new(:$pattern, :$handle); } method close(--> Nil) { diff --git a/t/00-bad-config-file.t b/t/00-bad-config-file.t new file mode 100644 index 0000000..764b882 --- /dev/null +++ b/t/00-bad-config-file.t @@ -0,0 +1,23 @@ +use Test; + +use lib 'lib'; +use LogP6 :configure; +use LogP6::ConfigFile; +use lib './t/resource/Helpers'; +use IOString; + +plan 3; + +$*ERR = IOString.new; + +lives-ok { + init-from-file('./t/resource/00-config-file/log-p6-wrong-syntax.json') }, + 'cannot init from wrong syntax file'; +lives-ok { + init-from-file('./t/resource/00-config-file/log-p6-corrupt.json') }, + 'cannot init from corrupt file'; +lives-ok { + init-from-file('./t/resource/00-config-file/log-p6-wrong-config.json') }, + 'cannot init from corrupt file'; + +done-testing; diff --git a/t/00-config-file.t b/t/00-config-file.t index 81ad4a3..60b26f7 100644 --- a/t/00-config-file.t +++ b/t/00-config-file.t @@ -3,13 +3,18 @@ use Test; use lib 'lib'; use LogP6 :configure; use LogP6::ConfigFile; +use lib './t/resource/Helpers'; +use IOString; plan 8; +$*ERR = IOString.new; +my LogP6::ConfigFile $config .= new; + subtest { plan 1; - dies-ok { init-from-file('./t/resource/00-config-file/log-p6-empty.json') }, + lives-ok { init-from-file('./t/resource/00-config-file/log-p6-empty.json') }, 'cannot init from empty file'; }, 'empty file'; @@ -17,30 +22,34 @@ subtest { subtest { plan 2; - dies-ok { init-from-file('./t/resource/00-config-file/not-exist.json') }, + lives-ok { init-from-file('./t/resource/00-config-file/not-exist.json') }, 'cannot init from miss file'; lives-ok { init-from-file(Any) }, 'can init empty argument'; }, 'miss file'; subtest { - plan 18; + plan 23; + + CATCH { default {say .gist }} my ($w, $cn); - $cn = parse-config('./t/resource/00-config-file/log-p6-1.json'); + $cn = $config.parse-config('./t/resource/00-config-file/log-p6-1.json'); - is $cn.writers.elems, 5, 'parsed 5 writers'; + is $cn.writers.elems, 6, 'parsed 5 writers'; $w = $cn.writers[0]; is $w.name, 'w1', 'w1 name'; is $w.pattern, '%msg', 'w1 pattern'; is $w.handle.Str, './t/resource/00-config-file/handle1.after', 'w1 handle'; + is $w.handle.out-buffer, 0, 'w1 out-buffer false(0)'; ok $w.auto-exceptions, 'w1 auto-exceptions'; $w = $cn.writers[1]; is $w.name, 'w2', 'w2 name'; is $w.pattern, '%level | %msg', 'w2 pattern'; is $w.handle, $*OUT, 'w2 handle'; + is $w.handle.out-buffer, 100, 'w2 out-buffer false(0)'; is $cn.writers[2].handle, $*ERR, 'w3 handle'; nok $w.auto-exceptions, 'w2 auto-exceptions'; @@ -51,11 +60,20 @@ subtest { nok $w.auto-exceptions, 'w4 auto-exceptions'; $w = $cn.writers[4]; - is $w.name, 'w5', 'w4 name'; + is $w.name, 'w5', 'w5 name'; nok $w.pattern, 'w5 pattern'; nok $w.handle, 'w5 handle'; nok $w.auto-exceptions, 'w5 auto-exceptions'; + $w = $cn.writers[5]; + is $w.handle.out-buffer, 1000, 'w6 handle out-buffer 1000'; + + my $w0h = $cn.writers[0].handle.WHICH; + my $w3h = $cn.writers[3].handle.WHICH; + $cn = $config.parse-config('./t/resource/00-config-file/log-p6-1.json'); + is $w0h, $cn.writers[0].handle.WHICH, 'get file handle from cache'; + is $w3h, $cn.writers[3].handle.WHICH, 'get custom handle from cache'; + }, 'writers'; subtest { @@ -66,7 +84,7 @@ subtest { use Custom; my ($f, $cn); - $cn = parse-config('./t/resource/00-config-file/log-p6-1.json'); + $cn = $config.parse-config('./t/resource/00-config-file/log-p6-1.json'); is $cn.filters.elems, 4, 'parsed 4 writers'; @@ -106,7 +124,7 @@ subtest { use LogP6::Wrapper::Transparent; my ($c, $cn); - $cn = parse-config('./t/resource/00-config-file/log-p6-1.json'); + $cn = $config.parse-config('./t/resource/00-config-file/log-p6-1.json'); is $cn.cliches.elems, 2, 'parsed 2 cliches'; @@ -143,11 +161,11 @@ subtest { use LogP6::Wrapper::SyncTime; - my $cn = parse-config('./t/resource/00-config-file/log-p6-1.json'); + my $cn = $config.parse-config('./t/resource/00-config-file/log-p6-1.json'); is $cn.default-pattern, '%msg', 'default-pattern'; is $cn.default-auto-exceptions, False, 'default-auto-exceptions'; is $cn.default-handle, $*ERR, 'default-handle'; - is $cn.default-x-pattern, '%x', 'default-x-pattern'; + is $cn.default-x-pattern, "%x\n%x\a%x\e%x", 'default-x-pattern'; is $cn.default-level, $trace, 'default-level'; is $cn.default-first-level-check, True, 'default-first-level-check'; ok $cn.default-wrapper, 'default-wrapper ok'; diff --git a/t/00-context.t b/t/00-context.t index 8556815..490f0c3 100644 --- a/t/00-context.t +++ b/t/00-context.t @@ -4,7 +4,7 @@ use lib 'lib'; use LogP6::Logger; use LogP6::Level; -plan 44; +plan 54; my LogP6::Context $context = get-context(); @@ -22,14 +22,14 @@ is $context.msg, 'setted', 'setted msg'; nok $context.level.defined, 'default level'; $context.level-set('boom'); is $context.level, 'boom', 'can set any level'; -$context.level-set(info); -is $context.level, info, 'set properly level'; +$context.level-set(LogP6::Level::info); +is $context.level, LogP6::Level::info, 'set properly level'; nok $context.x.defined, 'default x'; $context.x-set(X::AdHoc.new); is $context.x, X::AdHoc.new, 'setted x'; -$context.reset('resetted', trace, X::AdHoc.new(:payload

)); +$context.reset('resetted', LogP6::Level::trace, X::AdHoc.new(:payload

)); is $context.msg, 'resetted', 'resetted msg'; -is $context.level, trace, 'resetted level'; +is $context.level, LogP6::Level::trace, 'resetted level'; is $context.x.payload, 'p', 'resetted x'; # trait @@ -97,4 +97,38 @@ nok $context.msg, 'clean all with msg'; nok $context.x, 'clean all with x'; nok $context.level, 'clean all with level'; +# clone +$context.msg-set('msg'); +my $date = $context.date(); +$context.level-set(LogP6::Level::info); +$context.x-set(X::AdHoc.new); +$context.trait-set('trait'); +$context.ndc-push('ndc'); +$context.mdc-put('key', 'value'); +sub sub-info() { $context.callframe } +sub info() { sub-info } +sub foo() { info() } +foo(); +my $copy = $context.copy; +$context.msg-set('gsm'); +is $copy.msg, 'msg', 'copy msg'; +$context.date-clean; +is $copy.date, $date, 'copy date'; +$context.level-set(LogP6::Level::warn); +is $copy.level, LogP6::Level::info, 'copy level'; +$context.x-set(Any); +is $copy.x, X::AdHoc.new, 'copy x'; +$context.trait-set('tiart'); +is $copy.trait, 'trait', 'copy trait'; +is $copy.tid, $context.tid, 'copy tid'; +is $copy.tname, $context.tname, 'copy tname'; +$context.ndc-push('cdn'); +is-deeply $copy.ndc, ['ndc'], 'copy ndc'; +$context.mdc-put('value', 'key'); +is-deeply $copy.mdc, %(:key), 'copy mdc'; +my $callframe = $context.callframe; +$context.clean; +is $copy.callframe, $callframe, 'copy callframe'; + + done-testing; diff --git a/t/00-factories.t b/t/00-factories.t index f0a9315..b2f7c70 100644 --- a/t/00-factories.t +++ b/t/00-factories.t @@ -2,9 +2,13 @@ use Test; use lib 'lib'; use LogP6 :configure; +use lib './t/resource/Helpers'; +use IOString; plan 5; +$*ERR = IOString.new; + sub some-sub1($context) { True; } @@ -122,7 +126,6 @@ my $pattern1 = '[%date{$hh:$mm:$ss}][%level{length=5}] %msg'; my $pattern2 = '%level| %msg'; subtest { - plan 45; my $w-wo-name = writer(:pattern($pattern1), :auto-exceptions, @@ -201,7 +204,6 @@ subtest { }, 'writer'; subtest { - plan 54; use LogP6::Wrapper::Transparent; @@ -298,11 +300,11 @@ subtest { }, 'cliche'; subtest { - plan 18; use lib './t/resource/Helpers'; use LogP6::LoggerPure; + use LogP6::LoggerPure; use LogP6::Wrapper::SyncTime; use IOString; @@ -318,7 +320,7 @@ subtest { my $log = get-logger('log'); ok $log, 'log cliche'; isnt $log, $any-log, 'default and log are not the same logs'; - isnt get-logger('any2'), $any-log, 'default and another are not the same'; + isnt get-logger('any2'), $any-log, 'different trait - same logger'; is get-logger('any'), $any-log, 'same trait - same logger'; is get-logger('log'), $log, 'same trait - same logger again'; @@ -368,8 +370,7 @@ subtest { remove-logger('not-log'); remove-logger('any'); remove-logger('any2'); - cliche(:name(''), :matcher('hahaha'), grooves => ('', ''), :replace); - dies-ok { get-logger('not-log') }, 'default cliche is changed'; + does-ok get-logger('not-log'), LogP6::LoggerMute, 'default cliche is changed'; }, 'logger'; diff --git a/t/00-grammar.t b/t/00-grammar.t index 295e0f4..2b6f2e4 100644 --- a/t/00-grammar.t +++ b/t/00-grammar.t @@ -7,7 +7,7 @@ use LogP6::WriterConf::Pattern; use LogP6::Context; use IOString; -plan 14; +plan 46; my LogP6::Context $context .= new; $context.level-set($info); @@ -77,4 +77,50 @@ sub info($frame) { sub foo() { info(callframe) } foo; +# %trait +$context.trait-set('LogP6::Writer::Async::Std'); +is parse-process('%trait{short=[.]5.4}'), 'LogP6.Writer.Async.Std', '[.]5.4'; +is parse-process('%trait{short=[.]5}' ), 'LogP6.Writer.Async.Std', '[.]5'; +is parse-process('%trait{short=[.]4.2}'), 'LogP6.Writer.Async.Std', '[.]4.4'; +is parse-process('%trait{short=[.]4}' ), 'LogP6.Writer.Async.Std', '[.]4'; +is parse-process('%trait{short=[.]3.2}'), 'Lo.Writer.Async.Std', '[.]3.2'; +is parse-process('%trait{short=[.]3}' ), 'Writer.Async.Std', '[.]3'; +is parse-process('%trait{short=[.]2.3}'), 'Log.Wri.Async.Std', '[.]2.3'; +is parse-process('%trait{short=[.]2}' ), 'Async.Std', '[.]2'; +is parse-process('%trait{short=[.]1.1}'), 'L.W.A.Std', '[.]1.1'; +is parse-process('%trait{short=[.]1}' ), 'Std', '[.]1'; +is parse-process('%trait{short=[.]0.4}'), 'LogP6.Writer.Async.Std', '[.]0.4'; +is parse-process('%trait{short=[.]0}' ), 'LogP6.Writer.Async.Std', '[.]0'; +is parse-process('%trait{short=[.]-0.2}'),'LogP6.Writer.Async.Std', '[.]-0.2'; +is parse-process('%trait{short=[.]-0}' ), 'LogP6.Writer.Async.Std', '[.]-0'; +is parse-process('%trait{short=[.]-1.4}'),'LogP.Writer.Async.Std', '[.]-1.4'; +is parse-process('%trait{short=[.]-1}' ), 'Writer.Async.Std', '[.]-1'; +is parse-process('%trait{short=[.]-2.0}'),'Async.Std', '[.]-2.0'; +is parse-process('%trait{short=[.]-2}' ), 'Async.Std', '[.]-2'; +is parse-process('%trait{short=[.]-3.1}'),'L.W.A.Std', '[.]-3.1'; +is parse-process('%trait{short=[.]-3}' ), 'Std', '[.]-3'; +is parse-process('%trait{short=[.]-4.0}'),'LogP6.Writer.Async.Std', '[.]-4.0'; +is parse-process('%trait{short=[.]-4}' ), 'LogP6.Writer.Async.Std', '[.]-4'; +is parse-process('%trait{short=[.]-5.2}'), 'LogP6.Writer.Async.Std', '[.]-5.2'; +is parse-process('%trait{short=[.]-5}' ), 'LogP6.Writer.Async.Std', '[.]-5'; +$context.trait-set('LogP6::Writer::Async'); +is parse-process('%trait{sprintf=%.2s}'), 'Lo', '%.2s'; +is parse-process('%trait{sprintf=%21s}'), ' LogP6::Writer::Async', '%21s'; +is parse-process('%trait{sprintf=%-21s}'), 'LogP6::Writer::Async ', '%-21s'; + +# %color +$context.level-set($warn); +$context.msg-set('msg'); +is parse-process('%msg [%color%level%color{reset}]'), + "msg [\e[35mWARN\e[0m]", 'color empty with reset'; +is parse-process('%msg [%color{WARN=34}%level%color{reset}]'), + "msg [\e[34mWARN\e[0m]", 'color WARN with reset'; +$context.level-set($error); +is parse-process('%msg [%color{WARN=34}%level%color{reset}]'), + "msg [\e[31mERROR\e[0m]", 'color WARN error with reset'; +is parse-process('%msg [%color%level]'), + "msg [\e[31mERROR]\e[0m", 'color empty without reset'; +is parse-process('%msg [%color%level%creset]'), + "msg [\e[31mERROR\e[0m]", 'color empty with creset'; + done-testing; diff --git a/t/00-handle-async-single.t b/t/00-handle-async-single.t new file mode 100644 index 0000000..2e7a037 --- /dev/null +++ b/t/00-handle-async-single.t @@ -0,0 +1,22 @@ +use Test; + +use lib 'lib'; +use LogP6::Handle::AsyncSingle; +use lib './t/resource/Helpers'; +use IOString; + +plan 2; + +my IOString $delegate .= new; +my LogP6::Handle::AsyncSingle $async .= new: :$delegate; + +$async.say('boom'); +$async.say('moob'); +sleep(1); + +is $delegate.clean, "boom\nmoob\n", 'delegate writes'; + +$async.close; +ok $delegate.closed, 'delegate closed'; + +done-testing; diff --git a/t/00-handle-async.t b/t/00-handle-async.t new file mode 100644 index 0000000..22ed507 --- /dev/null +++ b/t/00-handle-async.t @@ -0,0 +1,22 @@ +use Test; + +use lib 'lib'; +use LogP6::Handle::Async; +use lib './t/resource/Helpers'; +use IOString; + +plan 2; + +my IOString $delegate .= new; +my LogP6::Handle::Async $async .= new: :$delegate; + +$async.say('boom'); +$async.say('moob'); +sleep(1); + +is $delegate.clean, "boom\nmoob\n", 'delegate writes'; + +$async.close; +ok $delegate.closed, 'delegate closed'; + +done-testing; diff --git a/t/00-logger-wrapper-sync-abstract.t b/t/00-logger-wrapper-sync-abstract.t index bdff653..cfc6cae 100644 --- a/t/00-logger-wrapper-sync-abstract.t +++ b/t/00-logger-wrapper-sync-abstract.t @@ -41,6 +41,14 @@ class MockLogger does LogP6::Logger { @!calls.push('mdc-clean'); } + method dc-copy() { + @!calls.push('dc-copy'); + } + + method dc-restore($dc) { + @!calls.push('dc-restore' ~ $dc); + } + method trace(*@args, :$x) { @!calls.push('trace' ~ @args.join('') ~ ((defined $x) ?? $x.message !! '')); } diff --git a/t/00-logger-wrapper-sync-time.t b/t/00-logger-wrapper-sync-time.t index af247f1..797b3cc 100644 --- a/t/00-logger-wrapper-sync-time.t +++ b/t/00-logger-wrapper-sync-time.t @@ -13,7 +13,7 @@ writer(:name, :handle($h), :pattern<%msg>); filter(:name, :level($info)); set-default-wrapper( - LogP6::Wrapper::SyncTime::Wrapper.new(:2seconds)); + LogP6::Wrapper::SyncTime::Wrapper.new(:1seconds)); subtest { plan 3; @@ -27,13 +27,13 @@ subtest { $cliche = cliche(:name($cliche.name), :matcher($cliche.matcher), :replace); $logger.info('log this yet'); - sleep(3); + sleep(1.2); $logger.info('ignore this'); is $h.clean, "log this yet\n", 'mute logger is turned off'; cliche(:name($cliche.name), :matcher($cliche.matcher), grooves => , :replace); - sleep(3); + sleep(1.2); $logger.info('log this'); is $h.clean.trim, 'log this', 'mute logger worked again'; }, 'mute sync'; @@ -50,12 +50,12 @@ subtest { filter(:name, :level($error), :replace); $logger.info('log this yet'); - sleep(3); + sleep(1); $logger.info('ignore this'); is $h.clean, "log this yet\n", 'general logger is changed level'; filter(:name, :level($info), :replace); - sleep(3); + sleep(1); $logger.info('log this'); is $h.clean.trim, 'log this', 'general logger worked again'; }, 'general sync'; diff --git a/t/00-logger.t b/t/00-logger.t index 9497da2..c939853 100644 --- a/t/00-logger.t +++ b/t/00-logger.t @@ -7,7 +7,7 @@ use LogP6::LoggerPure; use IOString; use LogP6 :configure; -plan 7; +plan 8; my ($h1, $h2, $h3) = (IOString.new xx 3).list; sub clean-io() { $_.clean for ($h1, $h2, $h3) } @@ -203,4 +203,31 @@ subtest { isnt $l1, $l2, 'frames are different'; }, 'different frame'; +subtest { + plan 5; + + my LogP6::Logger $log = get-logger(''); + + $log.ndc-clean; + $log.mdc-clean; + $log.mdc-put('k', 'v'); + $log.mdc-put('v', 'k'); + my $dc = $log.dc-copy; + $log.mdc-clean; + nok $log.mdc-remove('k'), 'mdc is empty'; + $log.dc-restore($dc); + is $log.mdc-remove('k'), 'v', 'mdc restore k'; + is $log.mdc-remove('v'), 'k', 'mdc restore v'; + + $log.mdc-clean; + $log.ndc-push: 'k'; + $log.ndc-push: 'v'; + $dc = $log.dc-copy; + $log.ndc-clean; + $log.dc-restore($dc); + is $log.ndc-pop, 'v', 'ndc restore v'; + is $log.ndc-pop, 'k', 'ndc restore k'; + +}, 'dc-copy'; + done-testing; diff --git a/t/00-writer-async.t b/t/00-writer-async.t new file mode 100644 index 0000000..3d9374a --- /dev/null +++ b/t/00-writer-async.t @@ -0,0 +1,28 @@ +use Test; + +use lib 'lib'; +use lib './t/resource/Helpers'; +use LogP6 :configure; +use IOString; +use LogP6::WriterConf::Async; + +plan 2; + +my IOString $handle .= new; +my $delegate = writer(:pattern('%msg'), :$handle); +my $async = LogP6::WriterConf::Async.new(:name, :$delegate); +writer($async); +cliche(:name, :matcher, + grooves => ('async-writer', level($info))); + +my $log = get-logger('async'); + +$log.info('boom'); + +sleep(1); + +is $handle.clean(), "boom\n", 'delegate writes'; +$async.close; +ok $handle.closed, 'handle closed'; + +done-testing; diff --git a/t/resource/00-config-file/log-p6-1.json b/t/resource/00-config-file/log-p6-1.json index 7656ac2..2993cb6 100644 --- a/t/resource/00-config-file/log-p6-1.json +++ b/t/resource/00-config-file/log-p6-1.json @@ -5,7 +5,7 @@ "type": "std", "path": "err" }, - "default-x-pattern": "%x", + "default-x-pattern": "%x\\n%x\\a%x\\e%x", "default-level": "trace", "default-first-level-check": true, "default-wrapper": { @@ -21,6 +21,7 @@ "handle": { "type": "file", "path": "./t/resource/00-config-file/handle1.after", + "out-buffer": false, "append": false }, "auto-exceptions": true @@ -31,7 +32,8 @@ "pattern": "%level | %msg", "handle": { "type": "std", - "path": "out" + "path": "out", + "out-buffer": 100 }, "auto-exceptions": false }, @@ -62,6 +64,16 @@ "args": { "name": "w5" } + }, + { + "type": "std", + "name": "w6", + "handle": { + "type": "file", + "path": "./t/resource/00-config-file/handle1.after", + "out-buffer": 1000 + }, + "auto-exceptions": true } ], "filters": [ diff --git a/t/resource/00-config-file/log-p6-corrupt.json b/t/resource/00-config-file/log-p6-corrupt.json new file mode 100644 index 0000000..fd89171 --- /dev/null +++ b/t/resource/00-config-file/log-p6-corrupt.json @@ -0,0 +1,3 @@ +{ + "write +} diff --git a/t/resource/00-config-file/log-p6-wrong-config.json b/t/resource/00-config-file/log-p6-wrong-config.json new file mode 100644 index 0000000..d074093 --- /dev/null +++ b/t/resource/00-config-file/log-p6-wrong-config.json @@ -0,0 +1,12 @@ +{ + "writers": [ + { + "type": "std", + "name": "w1" + }, + { + "type": "std", + "name": "w1" + } + ] +} diff --git a/t/resource/00-config-file/log-p6-wrong-syntax.json b/t/resource/00-config-file/log-p6-wrong-syntax.json new file mode 100644 index 0000000..38d6aac --- /dev/null +++ b/t/resource/00-config-file/log-p6-wrong-syntax.json @@ -0,0 +1,15 @@ +{ + "writers": [ + { + "type": "STD", + "name": "w1", + "pattern": "%msg", + "handle": { + "type": "file", + "path": "./t/resource/00-config-file/handle1.after", + "append": false + }, + "auto-exceptions": true + } + ] +} diff --git a/t/resource/Helpers/IOString.pm6 b/t/resource/Helpers/IOString.pm6 index 06b91b3..58cd88f 100644 --- a/t/resource/Helpers/IOString.pm6 +++ b/t/resource/Helpers/IOString.pm6 @@ -2,6 +2,7 @@ unit class IOString is IO::Handle; has Str $!writed; +has Bool $!closed; submethod TWEAK { self.encoding: 'utf8'; @@ -37,4 +38,12 @@ multi method Str(IOString:D:) { multi method gist(IOString:D:) { $!writed; +} + +method close() { + $!closed = True; +} + +method closed() { + $!closed; } \ No newline at end of file