├── .github └── workflows │ ├── linux.yml │ └── macos.yml ├── .gitignore ├── CHANGES ├── META6.json ├── README.md ├── eg ├── color.raku ├── imports.raku ├── imports2.raku ├── json.raku ├── main.raku ├── simple.raku └── use-args.raku ├── lib └── Log │ ├── Async.rakumod │ └── Async │ ├── CommandLine.rakumod │ └── Context.rakumod ├── sparrow.yaml └── t ├── 01-basic.rakutest ├── 02-levels.rakutest ├── 03-log.rakutest ├── 04-filter.rakutest ├── 05-concurrent.rakutest ├── 06-sleep.rakutest ├── 07-done.rakutest ├── 08-use.rakutest ├── 10-formatter.rakutest ├── 11-log-async-context.rakutest ├── 12-context.rakutest ├── 13-remove-tap.rakutest ├── 14-frame.rakutest ├── 15-untapped.rakutest ├── 16-imports.rakutest ├── 16-imports2.rakutest ├── 99-meta.rakutest ├── TestModule.rakumod └── lib ├── one.rakumod └── two.rakumod /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | raku: 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | raku-version: 18 | - '2024.05' 19 | - 'latest' 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: Raku/setup-raku@v1 24 | with: 25 | raku-version: ${{ matrix.raku-version }} 26 | - name: Install Dependencies 27 | run: zef install --deps-only . 28 | - name: Run Tests 29 | run: zef test -v . 30 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | raku: 13 | strategy: 14 | matrix: 15 | os: 16 | - macos-latest 17 | raku-version: 18 | - 'latest' 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: Raku/setup-raku@v1 23 | with: 24 | raku-version: ${{ matrix.raku-version }} 25 | - name: Install Dependencies 26 | run: zef install --deps-only . 27 | - name: Run Tests 28 | run: zef test -v . 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .precomp 2 | *~ 3 | \#*\# 4 | .\#* 5 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 0.0.13 2025-05-26 2 | - text fixes 3 | 4 | 0.0.12 2025-05-20 5 | - add use-args to allow command line options with usage 6 | 7 | 0.0.11 2025-05-09 8 | - add import options 9 | 10 | 0.0.10 2024-01-11 11 | - refactor test and fix CI 12 | 13 | 0.0.9 2024-01-10 14 | - update to be raku not p6 15 | - add ci pipeline 16 | 17 | 0.0.8 2022-03-14 18 | - Fix race condition in tests (niner) 19 | - first Changes file 20 | - replace Perl 6 with Raku 21 | 22 | -------------------------------------------------------------------------------- /META6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Log::Async", 3 | "source-url" : "git://github.com/bduggan/raku-log-async.git", 4 | "raku" : "6.*", 5 | "build-depends" : [ ], 6 | "provides" : { 7 | "Log::Async" : "lib/Log/Async.rakumod", 8 | "Log::Async::CommandLine" : "lib/Log/Async/CommandLine.rakumod", 9 | "Log::Async::Context" : "lib/Log/Async/Context.rakumod" 10 | }, 11 | "tags" : [ 12 | "log", "logging", "logger", 13 | "async", "asynchronous" 14 | ], 15 | "depends" : [ 16 | "Terminal::ANSI" 17 | ], 18 | "description" : "Asynchronous logging with supplies and taps", 19 | "test-depends" : [ ], 20 | "version" : "0.0.13", 21 | "license" : "Artistic-1.0-Perl", 22 | "auth" : "zef:bduggan", 23 | "authors" : [ 24 | "Brian Duggan" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Log::Async 2 | ========== 3 | Thread-safe asynchronous logging using supplies. 4 | 5 | [![SparrowCI](https://ci.sparrowhub.io/project/gh-bduggan-raku-log-async/badge)](https://ci.sparrowhub.io) 6 | [![Actions Status](https://github.com/bduggan/raku-log-async/actions/workflows/linux.yml/badge.svg)](https://github.com/bduggan/raku-log-async/actions/workflows/linux.yml) 7 | [![Actions Status](https://github.com/bduggan/raku-log-async/actions/workflows/macos.yml/badge.svg)](https://github.com/bduggan/raku-log-async/actions/workflows/macos.yml) 8 | 9 | 10 | Synopsis 11 | ======== 12 | 13 | This module can be used in a few different ways: 14 | 15 | ```raku 16 | use Log::Async ; # send all logs to stderr 17 | use Log::Async ; # in color 18 | use Log::Async ; # only send warning, error and fatal to stderr 19 | use Log::Async ; # parse -v, -d or --log=level from command line options 20 | ``` 21 | 22 | Or don't import anything and set the destinations and levels to log yourself: 23 | 24 | ```raku 25 | use Log::Async; 26 | logger.send-to($*OUT); 27 | 28 | trace 'how'; 29 | debug 'now'; 30 | info 'brown'; 31 | warning 'cow'; 32 | error 'wow'; 33 | fatal 'ow'; 34 | 35 | (start debug 'one') 36 | .then({ debug 'two' }); 37 | (start debug 'buckle') 38 | .then({ debug 'my shoe' }); 39 | sleep 1; 40 | 41 | my $when = now + 1; 42 | 43 | for ^100 { 44 | Promise.at($when) 45 | .then({ debug "come together"}) 46 | .then({ debug "right now"}) 47 | .then({ debug "over me"}); 48 | } 49 | 50 | logger.send-to("/var/log/hal.errors", :level(ERROR)); 51 | error "I'm sorry Dave, I'm afraid I can't do that"; 52 | ``` 53 | 54 | Description 55 | =========== 56 | 57 | `Log::Async` provides asynchronous logging using 58 | the supply and tap semantics of Raku. Log messages 59 | are emitted asynchronously to a supply. Taps are 60 | only executed by one thread at a time. 61 | 62 | By default a single tap is created which prints the timestamp, 63 | level and message to stdout. 64 | 65 | Exports 66 | ======= 67 | 68 | **trace, debug, info, warning, error, fatal**: each of these 69 | asynchronously emits a message at that level. 70 | 71 | **enum Loglevels**: TRACE DEBUG INFO WARNING ERROR FATAL 72 | 73 | **class Log::Async**: Does the real work. 74 | 75 | **sub logger**: return or create a logger singleton. 76 | 77 | **sub set-logger**: set a new logger singleton. 78 | 79 | Log::Async Methods 80 | ========== 81 | 82 | ### add-tap(Code $code,:$level,:$msg) 83 | ```raku 84 | my $tap = logger.add-tap({ say $^m ~ '!!!!!' }, :level(FATAL)); 85 | logger.add-tap({ $*ERR.say $^m }, :level(DEBUG | ERROR)); 86 | logger.add-tap({ say "# $^m"}, :level(* < ERROR) ); 87 | logger.add-tap({ say "meow: " ~ $^m }, :msg(rx/cat/)); 88 | logger.add-tap(-> $m { say "thread { $m.id } says $m" }); 89 | logger.add-tap(-> $m { say "$m {$m.file} {$m.line} $m: $m" }); 90 | logger.add-tap(-> $m { say "{ $m.utc } ($m) $m", 91 | :level(INFO..WARNING) }); 92 | ``` 93 | 94 | Add a tap, optionally filtering by the level or by the message. 95 | `$code` receives a hash with the keys `msg` (a string), `level` (a 96 | Loglevel), `when` (a DateTime), `THREAD` (the caller's $\*THREAD), 97 | `frame` (the current callframe), and possibly `ctx` (the context, see below). 98 | 99 | `$level` and `$msg` are filters: they will be smartmatched against 100 | the level and msg keys respectively. 101 | 102 | `add-tap` returns a tap, which can be sent to `remove-tap` to turn 103 | it off. 104 | 105 | ### remove-tap($tap) 106 | 107 | ```raku 108 | logger.remove-tap($tap) 109 | ``` 110 | Closes and removes a tap. 111 | 112 | ### send-to(Str $filename, Code :$formatter, |args) 113 | ```raku 114 | send-to(IO::Handle $handle) 115 | send-to(IO::Path $path) 116 | logger.send-to('/tmp/out.log'); 117 | logger.send-to('/tmp/out.log', :level( * >= ERROR)); 118 | logger.send-to('/tmp/out.log', formatter => -> $m, :$fh { $fh.say: "{$m.lc}: $m" }); 119 | logger.send-to($*OUT, 120 | formatter => -> $m, :$fh { 121 | $fh.say: "{ $m.file } { $m.line } { $m.code.name }: $m" 122 | }); 123 | ``` 124 | Add a tap that prints timestamp, level and message to a file or filehandle. 125 | `formatter` is a Code argument which takes `$m` (see above), as well as 126 | the named argument `:$fh` -- an open filehandle for the destination. 127 | 128 | Additional args (filters) are sent to add-tap. 129 | 130 | ### close-taps 131 | ```raku 132 | logger.close-taps 133 | ``` 134 | Close all the taps. 135 | 136 | ### done 137 | ```raku 138 | logger.done 139 | ``` 140 | Tell the supplier it is done, then wait for the supply to be done. 141 | This is automatically called in the END phase. 142 | 143 | ### untapped-ok 144 | ```raku 145 | logger.untapped-ok = True 146 | ``` 147 | This will suppress warnings about sending a log message before any 148 | taps are added. 149 | 150 | Context 151 | ======= 152 | To display stack trace information, logging can be initialized with `add-context`. 153 | This sends a stack trace with every log request (so may be expensive). Once `add-context` 154 | has been called, a `ctx` element will be passed which is a `Log::Async::Context` 155 | object. This has a `stack` method which returns an array of backtrace frames. 156 | 157 | ```raku 158 | logger.add-context; 159 | logger.send-to('/var/log/debug.out', 160 | formatter => -> $m, :$fh { 161 | $fh.say: "file { $m.file}, line { $m.line }, message { $m }" 162 | } 163 | ); 164 | logger.send-to('/var/log/trace.out', 165 | formatter => -> $m, :$fh { 166 | $fh.say: $m; 167 | $fh.say: "file { .file}, line { .line }" for $m.stack; 168 | } 169 | ); 170 | ``` 171 | 172 | A custom context object can be used as an argument to add-context. This 173 | object should have a `generate` method. `generate` will be called to 174 | generate context whenever a log message is sent. 175 | 176 | For instance: 177 | ```raku 178 | my $context = Log::Async::Context.new but role { 179 | method generate { ... } 180 | method custom-method { ... } 181 | }; 182 | logger.add-context($context); 183 | 184 | # later 185 | logger.add-tap(-> $m { say $m.ctx.custom-method } ) 186 | 187 | ``` 188 | 189 | More Examples 190 | ======== 191 | 192 | 193 | ### Send debug messages to stdout. 194 | ```raku 195 | logger.send-to($*OUT,:level(DEBUG)); 196 | ``` 197 | 198 | ### Send warnings, errors, and fatals to a log file. 199 | 200 | ```raku 201 | logger.send-to('/var/log/error.log',:level(* >= WARNING)); 202 | ``` 203 | 204 | ### Add a tap that prints the file, line number, message, and utc timestamp. 205 | 206 | ```raku 207 | logger.send-to($*OUT, 208 | formatter => -> $m, :$fh { 209 | $fh.say: "{ $m.utc } ({ $m.file } +{ $m.line }) $m $m" 210 | }); 211 | trace 'hi'; 212 | 213 | # output: 214 | 2017-02-20T14:00:00.961447Z (eg/out.raku +10) TRACE hi 215 | ``` 216 | 217 | 218 | Caveats 219 | ======= 220 | Because messages are emitted asynchronously, the order in which 221 | they are emitted depends on the scheduler. Taps are executed 222 | in the same order in which they are emitted. Therefore timestamps 223 | in the log might not be in chronological order. 224 | 225 | Author 226 | ====== 227 | Brian Duggan 228 | 229 | Contributors 230 | ============ 231 | Bahtiar Gadimov 232 | 233 | Curt Tilmes 234 | 235 | Marcel Timmerman 236 | 237 | Juan Julián Merelo Guervós 238 | 239 | Slobodan Mišković 240 | -------------------------------------------------------------------------------- /eg/color.raku: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | use Log::Async; 4 | use Terminal::ANSI::OO 't'; 5 | 6 | my %colors = 7 | trace => '#7bb274', # Sage green 8 | debug => '#6b8ba4', # Dusty blue 9 | info => '#cba560', # Goldenrod 10 | warning => '#ab6ca2', # Wisteria 11 | error => '#d9544d', # Coral red 12 | ; 13 | 14 | 15 | sub color-formatter ( $m, :$fh ) { 16 | $fh.say: t.color( %colors{$m.lc} // '#ff0000' ) 17 | ~ ' ' ~ $m 18 | ~ ' ' ~ ('[' ~ $m.lc ~ ']').fmt('%-10s') 19 | ~ $*THREAD.id.fmt('%3s') 20 | ~ ' ' ~ $m ~ t.text-reset; 21 | } 22 | 23 | logger.send-to: $*ERR, formatter => &color-formatter; 24 | 25 | trace 'innie'; 26 | debug 'minnie'; 27 | warning 'moe'; 28 | info 'oe'; 29 | fatal 'e'; 30 | -------------------------------------------------------------------------------- /eg/imports.raku: -------------------------------------------------------------------------------- 1 | use Log::Async ; 2 | 3 | info 'hi'; 4 | debug 'debugging'; 5 | trace 'tracing'; 6 | warning 'this is a warning'; 7 | fatal 'this is a fatal error'; 8 | -------------------------------------------------------------------------------- /eg/imports2.raku: -------------------------------------------------------------------------------- 1 | use Log::Async ; 2 | 3 | trace 'tracing'; 4 | debug 'debugging'; 5 | info 'this is some information'; 6 | warning 'this is a warning'; 7 | error 'oh no!'; 8 | fatal 'abort'; 9 | 10 | -------------------------------------------------------------------------------- /eg/json.raku: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | =begin pod 4 | Run this from the upper directory using: 5 | 6 | raku -Ilib eg/json.raku | jq "." 7 | 8 | You don't need to have C installed; if you do, you'll get nicely formatted JSON output 9 | =end pod 10 | 11 | use Log::Async; 12 | use JSON::Fast; 13 | 14 | sub json-formatter ( $m, :$fh ) { 15 | $fh.say: to-json 16 | $m:kv.Hash 17 | } 18 | 19 | logger.send-to($*OUT, formatter => &json-formatter ); 20 | 21 | trace 'innie'; 22 | debug 'minnie'; 23 | warning 'moe'; 24 | info 'oe'; 25 | fatal 'e'; 26 | -------------------------------------------------------------------------------- /eg/main.raku: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | use Log::Async ; 4 | 5 | sub MAIN(Str $arg, 6 | :$foo #= this is foo 7 | ) { 8 | info 'hi'; 9 | debug 'there'; 10 | trace 'tracer'; 11 | warning 'this is a warning'; 12 | info 'just fyi'; 13 | fatal 'just a flesh wound'; 14 | } 15 | -------------------------------------------------------------------------------- /eg/simple.raku: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | use Log::Async; 4 | logger.send-to($*OUT); 5 | 6 | trace 'how'; 7 | debug 'now'; 8 | warning 'brown'; 9 | info 'cow'; 10 | fatal 'ow'; 11 | -------------------------------------------------------------------------------- /eg/use-args.raku: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | use Log::Async ; 4 | 5 | info 'hi'; 6 | debug 'there'; 7 | 8 | trace 'tracer'; 9 | debug 'debugging is so fun'; 10 | warning 'this is a warning'; 11 | info 'just fyi'; 12 | fatal 'just a flesh wound'; 13 | -------------------------------------------------------------------------------- /lib/Log/Async.rakumod: -------------------------------------------------------------------------------- 1 | enum Loglevels <<:TRACE(1) DEBUG INFO WARNING ERROR FATAL>>; 2 | 3 | use Log::Async::Context; 4 | 5 | use Terminal::ANSI::OO 't'; 6 | 7 | our %LOGCOLORS = 8 | trace => '#7bb274', # Sage green 9 | debug => '#6b8ba4', # Dusty blue 10 | info => '#cba560', # Goldenrod 11 | warning => '#ab6ca2', # Wisteria 12 | error => '#d9544d', # Coral red 13 | fatal => '#ff0000', # Red 14 | ; 15 | 16 | class Log::Async:ver<0.0.7>:auth { 17 | has $.source = Supplier.new; 18 | has Tap @.taps; 19 | has Supply $.messages; 20 | has $.contextualizer is rw; 21 | has $.untapped-ok is rw = False; 22 | 23 | my $instance; 24 | method instance { 25 | return $instance; 26 | } 27 | method set-instance($i) { 28 | $instance = $i; 29 | } 30 | method add-context(:$context = Log::Async::Context.new) { 31 | $.contextualizer = $context; 32 | } 33 | 34 | method close-taps { 35 | .close for @.taps; 36 | } 37 | 38 | method add-tap(Code $c, :$level, :$msg --> Tap ) { 39 | $!messages //= self.source.Supply; 40 | my $supply = $!messages; 41 | $supply = $supply.grep( { $^m ~~ $level }) with $level; 42 | $supply = $supply.grep( { $^m ~~ $msg }) with $msg; 43 | my $tap = $supply.act($c); 44 | @.taps.push: $tap; 45 | return $tap; 46 | } 47 | 48 | method remove-tap(Tap $t) { 49 | my ($i) = @.taps.grep( { $_ eq $t }, :k ); 50 | $t.close; 51 | @.taps.splice($i,1,()); 52 | } 53 | 54 | multi method send-to( IO::Handle:D $fh, Code :$formatter is copy, |args --> Tap) { 55 | $formatter //= -> $m, :$fh { 56 | $fh.say: "{ $m } ({$m.id}) { $m.lc }: { $m }", 57 | } 58 | my $fmt = $formatter but role { method is-hidden-from-backtrace { True } }; 59 | self.add-tap: -> $m { 60 | $fmt($m,:$fh); 61 | $fh.flush; 62 | }, done => { $fh.close }, quit => { $fh.close }, |args 63 | } 64 | 65 | multi method send-to(Str $path, Code :$formatter, Bool :$out-buffer = False, |args --> Tap) { 66 | my $fh = open($path,:a,:$out-buffer) or die "error opening $path"; 67 | self.send-to($fh, :$formatter, |args); 68 | } 69 | 70 | multi method send-to(IO::Path $path, Code :$formatter, Bool :$out-buffer = False, |args --> Tap) { 71 | my $fh = $path.open(:a,:$out-buffer) or die "error opening $path"; 72 | self.send-to($fh, :$formatter, |args); 73 | } 74 | 75 | method log( 76 | Str :$msg, 77 | Loglevels:D :$level, 78 | CallFrame :$frame = callframe(1), 79 | DateTime :$when = DateTime.now 80 | ) is hidden-from-backtrace { 81 | my $ctx = $_.generate with self.contextualizer; 82 | my $m = { :$msg, :$level, :$when, :$*THREAD, :$frame, :$ctx }; 83 | if @.taps == 0 and not $!untapped-ok { 84 | note 'Message sent without taps.'; 85 | note 'Try "logger.send-to($*ERR)" or "logger.untapped-ok = True"'; 86 | $!untapped-ok = True; 87 | } 88 | (start $.source.emit($m)) 89 | .then({ say $^p.cause unless $^p.status == Kept }); 90 | } 91 | 92 | method done() { 93 | start { sleep 0.1; $.source.done }; 94 | $.source.Supply.wait; 95 | } 96 | } 97 | 98 | sub set-logger($new) is export(:MANDATORY) { 99 | Log::Async.set-instance($new); 100 | } 101 | sub logger is export(:MANDATORY) { 102 | Log::Async.instance; 103 | } 104 | 105 | sub trace($msg) is export(:MANDATORY) is hidden-from-backtrace { logger.log( :$msg :level(TRACE) :frame(callframe(1))) } 106 | sub debug($msg) is export(:MANDATORY) is hidden-from-backtrace { logger.log( :$msg :level(DEBUG) :frame(callframe(1))) } 107 | sub info($msg) is export(:MANDATORY) is hidden-from-backtrace { logger.log( :$msg :level(INFO) :frame(callframe(1))) } 108 | sub error($msg) is export(:MANDATORY) is hidden-from-backtrace { logger.log( :$msg :level(ERROR) :frame(callframe(1))) } 109 | sub warning($msg) is export(:MANDATORY) is hidden-from-backtrace { logger.log( :$msg :level(WARNING):frame(callframe(1))) } 110 | sub fatal($msg) is export(:MANDATORY) is hidden-from-backtrace { logger.log( :$msg :level(FATAL) :frame(callframe(1))) } 111 | 112 | sub EXPORT($arg = Nil, $arg2 = Nil, $arg3 = Nil) { 113 | return { } unless $arg || $arg2; 114 | my $using-args = False; 115 | my $level = WARNING; 116 | my $to = $*ERR; 117 | my @opts = ($arg, $arg2, $arg3).grep: *.defined; 118 | my $formatter = -> $m, :$fh { $fh.say: "{ $m } ({$m.id}) { $m.lc }: { $m }" } 119 | for @opts { 120 | when 'trace' { $level = ( * >= TRACE ) } 121 | when 'debug' { $level = ( * >= DEBUG ) } 122 | when 'info' { $level = ( * >= INFO ) } 123 | when 'warn' | 'warning' { $level = ( * >= WARNING ) } 124 | when 'error' { $level = ( * >= ERROR ) } 125 | when 'fatal' { $level = ( * >= FATAL ) } 126 | when 'color' | 'colour' { 127 | my %colors = %LOGCOLORS; 128 | 129 | $formatter = sub ( $m, :$fh ) { 130 | $fh.say: ($fh.t ?? t.color( %colors{$m.lc} // '#ff0001' ) !! '' ) 131 | ~ $m 132 | ~ ' ' ~ ('[' ~ $m.lc ~ ']').fmt('%-9s') 133 | ~ (' (' ~ $*THREAD.id ~ ')').fmt('%2s') 134 | ~ ' ' ~ $m 135 | ~ ($fh.t ?? t.text-reset !! '' ); 136 | } 137 | } 138 | when 'use-args' { 139 | $using-args = True; 140 | my regex opt { 'trace' | 'debug' | 'info' | 'warn' | 'warning' | 'error' | 'fatal' } 141 | my regex filename { \S+ } 142 | if @*ARGS.first( { / '--' 'logfile=' / }) -> $opt { 143 | $opt ~~ /'--logfile=' (.*)/ and $to = ~$0; 144 | @*ARGS .= grep: { ! / '--' 'logfile=' / }; 145 | } 146 | if @*ARGS.grep( { / '--' 'log=' / } ) { 147 | @*ARGS = @*ARGS.grep( { ! / '--' 'log=' / } ); 148 | $level = * ≥ Loglevels::{$.uc}; 149 | } 150 | if @*ARGS.grep( '-v' ) { 151 | @*ARGS .= grep: * ne '-v'; 152 | $level = * ≥ Loglevels::INFO; 153 | } 154 | if @*ARGS.grep( '-d' ) { 155 | @*ARGS .= grep: * ne '-d'; 156 | $level = * ≥ Loglevels::DEBUG; 157 | } 158 | } 159 | } 160 | if @opts.elems { 161 | logger.send-to: $to, :$level, :$formatter; 162 | } 163 | return { } unless $using-args; 164 | return { 165 | '&GENERATE-USAGE' => sub (&main,|args) { 166 | my $orig = &*GENERATE-USAGE(&main, |args); 167 | return $orig ~ q:to/ARGS/; 168 | 169 | 170 | Additional arguments for logging: 171 | 172 | --log=[trace|debug|info|warn|warning|error|fatal] 173 | -v # be verbose 174 | -d # show debug output 175 | --logfile=filename # send logs to a file 176 | ARGS 177 | } 178 | } 179 | } 180 | 181 | INIT { 182 | set-logger(Log::Async.new) unless logger; 183 | } 184 | END { 185 | Log::Async.instance.done if Log::Async.instance; 186 | } 187 | -------------------------------------------------------------------------------- /lib/Log/Async/CommandLine.rakumod: -------------------------------------------------------------------------------- 1 | unit module Log::Async::CommandLine; 2 | 3 | use Log::Async; 4 | 5 | sub parse-log-args { 6 | my @keepargs; 7 | 8 | my $loglevel = WARNING; 9 | my $logfh = $*OUT; 10 | my $silent; 11 | my $color; 12 | my $threadid; 13 | 14 | for @*ARGS { 15 | when '--silent'|'-q' { 16 | $silent = True; 17 | logger.untapped-ok = True; 18 | } 19 | when '-v' { 20 | $loglevel = INFO; 21 | } 22 | when /^'--'(trace|debug|info|warning|error|fatal)$/ { 23 | $loglevel = Loglevels::{$0.uc}; 24 | } 25 | when /^'--logfile='(.+)$/ { 26 | $logfh = open($0.Str, :a); 27 | } 28 | when '--logcolor' { 29 | $color = True; 30 | } 31 | when '--logthreadid' { 32 | $threadid = True; 33 | } 34 | default { 35 | push @keepargs, $_; 36 | } 37 | } 38 | 39 | my &print-log = $color 40 | ?? sub ($logfh, $m, $threadid) { 41 | state %colors = 42 | TRACE => "\e[35;1m", # magenta 43 | DEBUG => "\e[34;1m", # blue 44 | INFO => "\e[32;1m", # green 45 | WARNING => "\e[33;1m", # yellow 46 | ERROR => "\e[31;1m", # red 47 | FATAL => "\e[31;1m"; # red 48 | 49 | $logfh.say("$m " ~ 50 | ("($m.id()) " if $threadid) ~ 51 | "%colors{$m}$m.lc()" ~ 52 | ("\e[0m" unless $m ~~ ERROR|FATAL) ~ 53 | ": $m\e[0m"); 54 | } 55 | !! sub ($logfh, $m, $threadid) { 56 | $logfh.say("$m " ~ 57 | ("($m.id()) " if $threadid) ~ 58 | "$m.lc(): $m"); 59 | }; 60 | 61 | logger.add-tap(-> $m { &print-log($logfh, $m, $threadid) }, 62 | :level(* >= $loglevel)) 63 | unless $silent; 64 | 65 | @*ARGS = @keepargs; 66 | } 67 | 68 | INIT { 69 | parse-log-args; 70 | } 71 | 72 | =begin pod 73 | 74 | =head1 NAME 75 | 76 | Log::Async::CommandLine 77 | 78 | =head1 SYNOPSIS 79 | 80 | use Log::Async::CommandLine; 81 | 82 | ./someprogram [--trace] 83 | [--debug] 84 | [--info | -v] 85 | [--warning] # Default 86 | [--error] 87 | [--fatal] 88 | [--silent | -q] 89 | 90 | ./someprogram [--logcolor] # Colorize log output 91 | 92 | ./someprogram [--logthreadid] # Include thread id in log msg 93 | 94 | ./someprogram [--logfile=/var/log/mylogfile] 95 | 96 | =head1 DESCRIPTION 97 | 98 | A tiny wrapper around Log::Async to set some basic logging 99 | configuration stuff from the commandline. 100 | 101 | It will log either to $*OUT or the specified logfile messages with a 102 | level >= the specified level (or warning by default). 103 | 104 | Adding the '--logcolor' option will colorize the log output a little. 105 | 106 | Adding '--logthreadid' will include the thread id in the logged message. 107 | 108 | =end pod 109 | -------------------------------------------------------------------------------- /lib/Log/Async/Context.rakumod: -------------------------------------------------------------------------------- 1 | 2 | unit class Log::Async::Context; 3 | 4 | has @!backtrace; 5 | has $.file; 6 | has $.line; 7 | 8 | method generate is hidden-from-backtrace { 9 | my $exception = Exception.new; 10 | try $exception.throw; 11 | if ($!) { 12 | @!backtrace = $exception.backtrace.grep({ !.is-hidden and !.is-setting }); 13 | @!backtrace.shift; # remove exception creation 14 | @!backtrace.shift while @!backtrace[0].file.Str.contains( 'Log/Async' ); 15 | } else { 16 | die "error throwing exception"; 17 | } 18 | $!file = @!backtrace[0].file; 19 | $!line = @!backtrace[0].line; 20 | return self; 21 | } 22 | 23 | method stack is hidden-from-backtrace { 24 | @!backtrace; 25 | } 26 | -------------------------------------------------------------------------------- /sparrow.yaml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - 3 | name: main 4 | default: true 5 | language: Bash 6 | code: | 7 | set -e 8 | raku --version 9 | zef --version 10 | cd source/ 11 | zef install . --deps-only --test-depends --build-depends --/test 12 | zef test . 13 | -------------------------------------------------------------------------------- /t/01-basic.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | 4 | plan 2; 5 | 6 | use-ok 'Log::Async', "Use Log::Async"; 7 | 8 | use Log::Async; 9 | 10 | cmp-ok Log::Async.^ver, '>', v0.0.0, 'version is > 0.0.0'; 11 | -------------------------------------------------------------------------------- /t/02-levels.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 2; 6 | 7 | cmp-ok DEBUG, '>', TRACE, 'level order'; 8 | cmp-ok TRACE, '~~', ( DEBUG | TRACE | ERROR ), "smart match"; 9 | -------------------------------------------------------------------------------- /t/03-log.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 12; 6 | 7 | my $out; 8 | my $out-channel = Channel.new; 9 | sub wait-for-out { 10 | react { 11 | whenever $out-channel { $out = $_; done } 12 | whenever Promise.in(20) { $out = Nil; done } 13 | } 14 | } 15 | $*OUT = $*OUT but role { method say($arg) { $out-channel.send: $arg } }; 16 | 17 | set-logger(Log::Async.new); 18 | logger.send-to($*OUT); 19 | 20 | trace "albatross"; 21 | wait-for-out; 22 | like $out, /albatross/, 'found message'; 23 | like $out, /trace/, 'found level'; 24 | 25 | debug "soup"; 26 | wait-for-out; 27 | like $out, /soup/, 'found message'; 28 | like $out, /debug/, 'found level'; 29 | 30 | info "logic"; 31 | wait-for-out; 32 | like $out, /logic/, 'found message'; 33 | like $out, /info/, 'found level'; 34 | 35 | warning "problem"; 36 | wait-for-out; 37 | like $out, /problem/, 'found message'; 38 | like $out, /warning/, 'found level'; 39 | 40 | error "danger"; 41 | wait-for-out; 42 | like $out, /danger/, 'found message'; 43 | like $out, /error/, 'found level'; 44 | 45 | fatal "will robinson"; 46 | wait-for-out; 47 | like $out, /'will robinson'/, 'found message'; 48 | like $out, /fatal/, 'found level'; 49 | -------------------------------------------------------------------------------- /t/04-filter.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 10; 6 | 7 | my @last = "my"; 8 | my $channel = Channel.new; 9 | sub wait-for-out { 10 | my $p1 = start $channel.receive; 11 | my $p2 = Promise.in(5); 12 | await Promise.anyof($p1,$p2); 13 | if $p1 { 14 | @last.push: $p1.result; 15 | } else { 16 | diag "timed out waiting for message"; 17 | diag "last contains " ~ @last.raku; 18 | } 19 | } 20 | 21 | logger.add-tap({ $channel.send: $^message }, :level(TRACE)); 22 | 23 | trace "name"; 24 | wait-for-out; 25 | is @last.tail, 'name', 'got trace message'; 26 | 27 | debug 'is'; 28 | trace "trace1"; 29 | wait-for-out; 30 | is @last.tail, 'trace1', 'debug message not sent to trace log'; 31 | 32 | warning 'Inigo'; 33 | trace "trace2"; 34 | wait-for-out; 35 | is @last.tail, 'trace2', 'warning message not sent to trace log'; 36 | 37 | error 'Montoya'; 38 | trace "trace3"; 39 | wait-for-out; 40 | is @last.tail, 'trace3', 'error message not sent to trace log'; 41 | 42 | my $debug-or-error = Channel.new; 43 | my $severe = Channel.new; 44 | my $not-severe = Channel.new; 45 | logger.add-tap({ $debug-or-error.send: $^m }, level => (DEBUG | ERROR) ); 46 | logger.add-tap({ $severe .send: $^m }, :level(* >= ERROR) ); 47 | logger.add-tap({ $not-severe .send: $^m }, :level(TRACE..INFO) ); 48 | info '1'; 49 | trace '2'; 50 | debug '3'; 51 | error '4'; 52 | fatal '5'; 53 | error '6'; 54 | 55 | wait-for-out; 56 | is @last.tail, '2', 'trace messages are still sent'; 57 | 58 | sub wait-for-channel($channel) { 59 | my @out; 60 | react { 61 | my $count = 0; 62 | whenever $channel { @out.push: $_; done if ++$count == 3 } 63 | whenever Promise.in(20) { done } 64 | } 65 | @out; 66 | } 67 | is (wait-for-channel $debug-or-error), <3 4 6>, 'filter with junction'; 68 | is (wait-for-channel $severe ), <4 5 6>, 'filter with whatever'; 69 | is (wait-for-channel $not-severe ), <1 2 3>, 'not severe'; 70 | 71 | logger.add-tap({ $channel.send: $^message }, :msg(rx/cat/)); 72 | debug 'cat alert1'; 73 | debug 'dog alog'; 74 | error 'cat alert2'; 75 | wait-for-out; 76 | is @last.tail, 'cat alert1', 'filtered by msg'; 77 | wait-for-out; 78 | is @last.tail, 'cat alert2', 'filtered by msg'; 79 | 80 | -------------------------------------------------------------------------------- /t/05-concurrent.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 3; 6 | 7 | my $when = now + 1; 8 | 9 | my @messages; 10 | logger.add-tap({ push @messages, $^message }); 11 | 12 | my $string; 13 | logger.add-tap({ $string ~= $^message }); 14 | 15 | for ^100 { 16 | Promise.at($when) 17 | .then({ debug "a" x 10 }) 18 | .then({ debug "b" x 10 }) 19 | .then({ debug "c" x 10 }) 20 | .then({ debug "d" x 10 }) 21 | ; 22 | } 23 | 24 | sleep 2; 25 | 26 | is +@messages, 400, 'four hundred messages'; 27 | is $string.chars, 4000, '4000 characters'; 28 | like $string, /^ ('aaaaaaaaaa' | 'bbbbbbbbbb' | 'cccccccccc' | 'dddddddddd' )+ $/, 29 | 'messages are all separate'; 30 | -------------------------------------------------------------------------------- /t/06-sleep.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 2; 6 | 7 | my $message; 8 | logger.add-tap({ sleep 1; $message = $^m }); 9 | 10 | info 'hi'; 11 | ok !$message, 'no message yet'; 12 | sleep 2; 13 | is $message, 'hi', 'now there is a message'; 14 | 15 | -------------------------------------------------------------------------------- /t/07-done.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 2; 6 | 7 | my $out = ""; 8 | $*OUT = $*OUT but role { method say($arg) { $out ~= $arg } }; 9 | set-logger(Log::Async.new); 10 | logger.send-to($*OUT); 11 | 12 | info 'first'; 13 | info 'second'; 14 | 15 | logger.done; 16 | 17 | like $out, /first/, 'found first in output'; 18 | like $out, /second/, 'found second in output'; 19 | -------------------------------------------------------------------------------- /t/08-use.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | 3 | plan 3; 4 | 5 | use lib ~$*PROGRAM.parent.child('lib'); 6 | 7 | use-ok 'one'; 8 | use-ok 'two'; 9 | 10 | ok 2, "didn't die"; 11 | -------------------------------------------------------------------------------- /t/10-formatter.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 5; 6 | 7 | sub tempfile { 8 | return $*TMPDIR.child("log-async-{ now.Rat }-{ $*PID }" ); 9 | } 10 | 11 | my regex zone { 12 | [ <[+-]> \d+ ':' \d+ | 'Z' ] 13 | } 14 | my regex date { 15 | \d+ '-' \d+ '-' \d+ 'T' \d+ ':' \d+ ':' \d+ '.' \d+ 16 | } 17 | 18 | { 19 | my $output will leave { .unlink } = tempfile; 20 | logger.send-to($output); 21 | info "this is some interesting info"; 22 | logger.done; 23 | my $lines = $output.slurp; 24 | like $lines, / ' (' \d+ ') info: this is some interesting info' /, 25 | 'default format'; 26 | logger.close-taps; 27 | } 28 | 29 | { 30 | Log::Async.set-instance(Log::Async.new); 31 | my $output will leave { .unlink } = tempfile; 32 | logger.send-to($output); 33 | info "this is some more interesting info"; 34 | logger.done; 35 | my $lines = $output.slurp; 36 | like $lines, / ' (' \d+ ') info: this is some more interesting info' /, 37 | 'default format again'; 38 | logger.close-taps; 39 | } 40 | 41 | { 42 | Log::Async.set-instance(Log::Async.new); 43 | my $output will leave { .unlink } = tempfile; 44 | logger.send-to($output, formatter => -> $m, :$fh { $fh.say: "this is my own format" }); 45 | info "this will not be printed"; 46 | logger.done; 47 | my $lines = $output.slurp; 48 | is $lines, "this is my own format\n", "custom format"; 49 | logger.close-taps; 50 | } 51 | 52 | { 53 | Log::Async.set-instance(Log::Async.new); 54 | my $output will leave { .unlink } = tempfile; 55 | logger.send-to($output, formatter => -> $m, :$fh { $fh.say: "{$m.lc}: $m" }); 56 | trace "tracing paper"; 57 | debug "this is not a bug"; 58 | warning "this is your final warning"; 59 | logger.done; 60 | my @lines = $output.slurp.lines; 61 | is-deeply @lines.sort, ["trace: tracing paper", 62 | "debug: this is not a bug", 63 | "warning: this is your final warning"].sort, "custom format again"; 64 | logger.close-taps; 65 | } 66 | 67 | { 68 | Log::Async.set-instance(Log::Async.new); 69 | my $output will leave { .unlink } = tempfile; 70 | logger.send-to($output, :level(DEBUG), formatter => -> $m, :$fh { $fh.say: "{$m.lc}: $m" }); 71 | trace "tracing paper"; 72 | debug "this is not a bug"; 73 | warning "this is your final warning"; 74 | logger.done; 75 | my @lines = $output.slurp.lines; 76 | is-deeply @lines, [ "debug: this is not a bug" ], "custom format with filter"; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /t/11-log-async-context.rakutest: -------------------------------------------------------------------------------- 1 | 2 | use Test; 3 | use lib 'lib'; 4 | use Log::Async::Context; 5 | 6 | plan 5; 7 | 8 | ok 1, 'no compilation errors'; 9 | 10 | my $ctx = Log::Async::Context.new.generate; 11 | my $line = $?LINE - 1; 12 | 13 | ok $ctx, 'contructor'; 14 | 15 | like $ctx.file, / { $?FILE } $$/, "current file ($?FILE)"; 16 | 17 | exit 0 & skip-rest('coverage interferes with line numbers') if ?%*ENV; # interferes with line numbers 18 | 19 | is $ctx.line, $line, "right line ($line)"; 20 | 21 | my @stack; 22 | 23 | class SomeClass { 24 | method some-method { 25 | @stack.unshift("line $?LINE"); return Log::Async::Context.new.generate.stack; 26 | } 27 | } 28 | 29 | sub some-sub { 30 | @stack.unshift("line $?LINE"); return SomeClass.some-method; 31 | } 32 | @stack.unshift("line $?LINE"); my @trace = some-sub; 33 | my @trace-strings = @trace.map( -> $s {"line {$s.line}"} ); 34 | is-deeply @stack, @trace-strings, 'stack trace looks good'; 35 | -------------------------------------------------------------------------------- /t/12-context.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 1; 6 | 7 | exit 0 & skip-rest('coverage interferes with line numbers') if ?%*ENV; # interferes with line numbers 8 | 9 | my @lines; 10 | my $out = $*OUT but role { method say($arg) { @lines.push: $arg } }; 11 | logger.add-context; 12 | logger.send-to($out, 13 | formatter => -> $m, :$fh { 14 | $fh.say: "file { $m.file}, line { $m.line }, message { $m }" 15 | } 16 | ); 17 | my $msg = "yàsu"; 18 | trace $msg; 19 | my $line = $?LINE - 1; 20 | logger.done; 21 | 22 | my $file = $?FILE.subst(/^^ "{ $*CWD }/" /,''); 23 | is-deeply @lines, [ "file $file, line $line, message $msg" ], "Got context"; 24 | -------------------------------------------------------------------------------- /t/13-remove-tap.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | use Log::Async; 4 | 5 | plan 4; 6 | 7 | my @lines; 8 | my $out = $*OUT but role { method say($arg) { @lines.push: $arg }; method flush { } }; 9 | 10 | my $one = logger.send-to($out, formatter => -> $m, :$fh { @lines.push: "one" } ); 11 | my $two = logger.send-to($out, formatter => -> $m, :$fh { @lines.push: "two" } ); 12 | my $three = logger.send-to($out, formatter => -> $m, :$fh { @lines.push: "three" } ); 13 | 14 | isa-ok $one, Tap, 'got a tap'; 15 | isa-ok $two, Tap, 'got a tap'; 16 | isa-ok $three, Tap, 'got a tap'; 17 | 18 | logger.remove-tap($two); 19 | 20 | info "hello"; 21 | logger.done; 22 | 23 | is-deeply @lines, [ "one", "three" ], "two out of three taps still there"; 24 | -------------------------------------------------------------------------------- /t/14-frame.rakutest: -------------------------------------------------------------------------------- 1 | 2 | use Test; 3 | use lib 'lib'; 4 | use Log::Async; 5 | plan 6; # NB: line numbers are hard coded below, modify with care 6 | exit 0 & skip-rest('coverage interferes with line numbers') if ?%*ENV; # interferes with line numbers 7 | 8 | my @all; 9 | my $out = $*OUT but role { method say($str) { @all.push: $str }; method flush { } }; 10 | 11 | logger.send-to($out, 12 | formatter => -> $m, :$fh { 13 | $fh.say: "{ $m.file } { $m.line } { $m.code.name }: $m" 14 | }); 15 | 16 | sub foo { 17 | trace "hello"; 18 | trace "hello 1"; 19 | trace "hello 2"; 20 | } 21 | 22 | class Foo { 23 | method bar { 24 | trace "very"; 25 | trace "nice"; 26 | } 27 | } 28 | 29 | foo(); 30 | Foo.bar(); 31 | trace "world"; 32 | 33 | logger.done; 34 | @all .= sort; 35 | 36 | my $file = callframe.file; 37 | is @all[0], "$file 17 foo: hello", 'right frame output in sub'; 38 | is @all[1], "$file 18 foo: hello 1", 'right frame output in sub'; 39 | is @all[2], "$file 19 foo: hello 2", 'right frame output in sub'; 40 | is @all[3], "$file 24 bar: very", 'right frame output in method'; 41 | is @all[4], "$file 25 bar: nice", 'right frame output in method'; 42 | is @all[5], "$file 31 : world", 'right frame output in main'; 43 | 44 | -------------------------------------------------------------------------------- /t/15-untapped.rakutest: -------------------------------------------------------------------------------- 1 | use Test; 2 | use lib 'lib'; 3 | 4 | plan 2; 5 | 6 | use Log::Async; 7 | 8 | ok logger.untapped-ok == False, 'untapped-ok set to false'; 9 | logger.untapped-ok = True; 10 | ok logger.untapped-ok, 'set to true'; 11 | 12 | -------------------------------------------------------------------------------- /t/16-imports.rakutest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | use Test; 4 | 5 | use lib 'lib'; 6 | use Test; 7 | plan 2; 8 | 9 | my $args; 10 | 11 | BEGIN { 12 | use Log::Async; 13 | 14 | Log::Async.^find_method('add-tap').wrap: -> \s, |q { 15 | $args = q; 16 | callsame; 17 | } 18 | } 19 | 20 | use Log::Async ; 21 | ok $args(INFO), 'level info will be logged'; 22 | nok $args(DEBUG), 'level debug will not be logged'; 23 | 24 | -------------------------------------------------------------------------------- /t/16-imports2.rakutest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env raku 2 | 3 | use Test; 4 | 5 | use lib 'lib'; 6 | use Test; 7 | 8 | plan 1; 9 | 10 | my $args; 11 | 12 | BEGIN { 13 | use Log::Async; 14 | 15 | Log::Async.^find_method('add-tap').wrap: -> \s, |q { 16 | $args = q; 17 | callsame; 18 | } 19 | } 20 | 21 | #use Log::Async ; 22 | #ok $args(INFO), 'level info will be logged'; 23 | #nok $args(DEBUG), 'level debug will not be logged'; 24 | 25 | my @*ARGS = '--level=trace'; 26 | use Log::Async "use-args"; 27 | ok $args(TRACE), 'trace will be logged'; 28 | -------------------------------------------------------------------------------- /t/99-meta.rakutest: -------------------------------------------------------------------------------- 1 | use lib 'lib'; 2 | use Test; 3 | plan 1; 4 | 5 | constant AUTHOR = ?%*ENV; 6 | 7 | if AUTHOR { 8 | require Test::META <&meta-ok>; 9 | meta-ok; 10 | done-testing; 11 | } 12 | else { 13 | skip-rest "Skipping author test"; 14 | exit; 15 | } 16 | -------------------------------------------------------------------------------- /t/TestModule.rakumod: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use Log::Async; 4 | 5 | unit module TestModule; 6 | 7 | sub sub_a is export 8 | { 9 | trace 'sub_a'; 10 | debug 'sub_a'; 11 | info 'sub_a'; 12 | warning 'sub_a'; 13 | error 'sub_a'; 14 | fatal 'sub_a'; 15 | } 16 | 17 | sub sub_b is export 18 | { 19 | trace 'sub_b'; 20 | debug 'sub_b'; 21 | info 'sub_b'; 22 | warning 'sub_b'; 23 | error 'sub_b'; 24 | fatal 'sub_b'; 25 | } 26 | -------------------------------------------------------------------------------- /t/lib/one.rakumod: -------------------------------------------------------------------------------- 1 | use Log::Async; 2 | 3 | -------------------------------------------------------------------------------- /t/lib/two.rakumod: -------------------------------------------------------------------------------- 1 | use Log::Async; 2 | 3 | --------------------------------------------------------------------------------