Skip to content

A better console.log for the log-driven debugging junkies

License

Notifications You must be signed in to change notification settings

CollinMonahanLab49/ololog

 
 

Repository files navigation

Ololog!

Build Status Windows Build Status NPM

Examples (NEW!)

These examples demonstrate some non-trivial complex behaviors that could be achieved with Ololog by plugging into it's rendering pipeline. For simpler examples read further docs!

  1. Logging to a file and on screen at the same time (with different log levels)

  2. Collapsing repeated messages (with an incrementing counter)

    screen shot 2018-05-11 at 19 32 48
  3. Displaying log levels and custom tags

    screen shot 2019-01-22 at 22 46 59

TODO

  • HTML/DOM rendering
  • Improve tests coverage

Importing

For use with Node or with module bundlers (Browserify / WebPack / Rollup):

npm install ololog
const log = require ('ololog')

Using With Mocha

mocha --reporter ololog/reporter
  • Aligns log messages nicely
  • Supresses log output for nonfailed tests (disable with .only or this.verbose=true for a suite/test)
  • Automatically manages empty lines / whitespace for better legibility
  • Prints unhandled exceptions and promise rejections as nice stacktraces
  • Animated execution progress

NOTE: It is highly experimental yet, and things may not work as expected...

Browser Bundle

...for those who still uses <script> tag for module importing ;) Exposes the global ololog and ansicolor objects. Installs String extensions for ANSI styles. Not compressed.

<script src="https://unpkg.com/ololog"></script> <!-- from unpkg.com CDN -->
<script>
    log = ololog
    log ('something'.red)
</script>

Basic Usage

At first, it's similar to console.log:

log ('foo', 'bar', 'baz') // foo bar baz

Configuration

It exposes a method called .configure, which produces a new log instance with the new settings applied (not mutating the original one), which can be saved and re-used subsequently:

const log = require ('ololog').configure ({ concat: { separator: '' }})
log ('foo', 'bar', 'baz') // foobarbaz

...or you can apply the configuration method ad-hoc:

log.configure ({ concat: { separator: '' }}) ('foo', 'bar', 'baz') // foobarbaz

And you can chain the configuration calls, applying them subsequently:

log1 = log.configure ({ locate: false }) // removes the code location tag
log1 ('foo')

log2 = log1.configure ({ time: true }) // preserves previous settings + enables timestamps
log2 ('bar')

The variety of possible options will be covered just below — there is a plenty of them!

Configuration engine is implemented as a separate external library, for everyone's use — you can read more about it here. Contributions are welcome.

Debugging Of Functional Expressions

Ololog returns its first argument (a feature that console.log doesn't have), and it greatly simplifies debugging of functional expressions, as you can simply wrap part of an expression to log:

array.map (x => log (x) + 1)

It is far less ugly than with console.log:

array.map (x => { console.log (x); return x + 1 })

Also, if you don't like that behavior, you can override it. For example, returning the last argument instead of first:

log = log.configure ({

    returnValue: (text, { initialArguments }) => initialArguments[initialArguments.length - 1]
})

ANSI Styling

Backed by the ansicolor library, colored output is supported for the terminal environment and for the Chrome DevTools console. On other platforms, ANSI codes are safely stripped from the output, so they don't mess up anything.

Apply styling by calling the ansicolor methods on arbitrary strings:

require ('ansicolor').nice // importing in .nice mode extends the String prototype, but there's a safe functional mode as well (see the docs...)

log (('foo'.dim.red + 'bar'.bgLightCyan).underline)

...or by using the built-in shorthand methods (no need to import ansicolor, but we lose the ability to colorize just a part of a string):

log.red ('red text')
log.bright.red.underline ('multiple styles combined')

See all the supported styling options here.

Smart Indentation / Newline Handling

To add indentation to a multiline text or complex objects, you can simply provide the indentation symbols as a first argument:

log ('    ', 'foo\nbar\nbar')
    foo
    bar
    bar

The remarkable thing is that you can provide any text that would be used to offset what's coming after it. This is especially useful with printing long objects that span across many lines:

log ('This is my object:', { foo: 10, bar: 20, qux: 30 })
This is my object: { foo: 10,
                     bar: 20,
                     qux: 30  }

Compare it to the crappy console.log output, which doesn't care about readability:

This is my object: { foo: 10,
 bar: 20,
 qux: 30  }

Ololog also handles the ANSI escape codes correctly while computing the proper the indentation width:

const { bright } = require ('ansicolor')

log.magenta (bright.green ('This is my object:'), { foo: 10, bar: 20, qux: 30 })

Screen Shot 2019-06-27 at 13 39 08

Using The indent Option

The other way is to use the indent config option:

log.configure ({ indent: { level: 3 } }) ('foo\nbar\nbaz\n')

Shorthand method:

log.indent (2) ('foo\n', 'bar\n', 'baz')

You can also set the indentation pattern should be used:

log = log.configure ({ indent: { pattern: '\t' } })

Smart Object Printing

All magic is provided by the external String.ify library. Read the docs to see all the available configuration options. There are plenty of them! Contributions are welcome.

GIF Animation

Default output:

log (obj) // prints example object
{ asks: [ { price: "1000", amt: 10 },
          { price: "2000", amt: 10 }  ],
  bids: [ { price: "500", amt: 10 },
          { price: "100", amt: 10 }  ]   }

Longer strings:

log.maxLength (70) (obj)
{ asks: [{ price: "1000", amt: 10 }, { price: "2000", amt: 10 }],
  bids: [{ price: "500", amt: 10 }, { price: "100", amt: 10 }]    }

Shorter strings:

log.maxLength (20) (obj)
{ asks: [ {  price: "1000",
               amt:  10     },
          {  price: "2000",
               amt:  10     }  ],
  bids: [ {  price: "500",
               amt:  10    },
          {  price: "100",
               amt:  10    }  ]   }

Disabling right keys alignment:

log.noRightAlignKeys (anotherObj)
{ obj: [ { someLongPropertyName: 1,
           propertyName: 2,
           anotherProp: 4,
           moreProps: 5             },
         { propertyName: { someVeryLongPropertyName: true,
                           qux: 6,
                           zap: "lol"                      } } ] }

Disabling fancy nesting:

log.noFancy (anotherObj)
{
    obj: [
        {
            someLongPropertyName: 1,
            propertyName: 2,
            anotherProp: 4,
            moreProps: 5
        },
        {
            propertyName: {
                someVeryLongPropertyName: true,
                qux: 6,
                zap: "lol"
            }
        }
    ]
}

No fancy nesting + setting indentation width to 2 spaces:

log.configure ({ stringify: { fancy: false, indentation: '  ' } }) (yetAnotherObj)
{
  obj: [
    {
      propertyName: 2,
      moreProps: 5
    }
  ]
}

Single line mode:

log.noPretty (obj)
{ asks: [{ price: "1000", amount: 10 }, { price: "2000", amount: 10 }], bids: [{ price: "500", amount: 10 }, { price: "100", amount: 10 }] }

Changing max print depth / max array length:

log.maxDepth (1).maxArrayLength (100) (obj) // or log.configure ({ stringify: { maxDepth: 1, maxArrayLength: 100 } })
log.unlimited (obj) // disables limiting

Setting floating-point output precision:

log.precision (2) ({ foo: 123.456789 })
{ foo: 123.45 }

Passing other configuration options to string.ify (read the its docs for more info):

log.configure ({

    stringify: {
        pure:            false,
        json:            false,
        maxDepth:        5,
        maxLength:       50,
        maxArrayLength:  60,
        maxObjectLength: 200,
        maxStringLength: 60,
        precision:       undefined,
        formatter:       undefined,
        pretty:         'auto',
        rightAlignKeys:  true,
        fancy:           true,
        indentation:    '    '
    }
    
}) (obj)

Avoid Too Long Call Chains

Please not that in case of multiple configuration options it is preferable to do that:

log.configure ({ stringify: { precision: 2, maxLength: 20, noFancy: true, maxDepth: 8 }})

...instead of:

log.precision (2).maxLength (20).noFancy.maxDepth (8)

...because the latter generates too deep callstack which could disrupt the displaying of the call location tag along with the message! The problem is yet to be solved in future pipez versions.

Using With Custom Stringifier

Replacing the default printer with q-i (as an example):

const log = require ('ololog').configure ({ stringify: { print: require ('q-i').stringify } })
log ({ foo: true, bar: 42 })

pic

You can also override the arguments stringification stage completely (see more on overriding default behavior):

log = require ('ololog').configure ({ stringify (args, cfg) { return args.map (x => myCustomStringifier (x, cfg)) } })

// ...providing additional configuration somewhere later...
log = log.configure ({ stringify: { /* this object will be passed down as `cfg` to myCustomStringifier */ }})

Pretty Printing Error Instances

This feature is implemented in the StackTracey library. See it's docs for more (you can configure the path shortening / library calls skipping).

log.bright.red (e) // where `e` is an instance of Error

or (if you want the output go to stderr and supress the grey location badge):

log.bright.red.error.noLocate (e)

Screen Shot 2019-04-06 at 00 56 17

Using As The Default Exception Printer In Node

process.on ('uncaughtException',  e => { log.bright.red.error.noLocate (e); process.exit (1) })
process.on ('unhandledRejection', e => { log.bright.red.error.noLocate (e); process.exit (1) })

Or you can simply call the handleNodeErrors helper when importing Ololog:

const log = require ('ololog').handleNodeErrors ()

See Also: panic-overlay

You can improve the error reporting not only in Node projects, but also in browsers. See the panic-overlay library which shares the same codebase with Ololog:

Displaying Call Location

Have you ever encountered a situation where you need to quickly find in the code the place where the logging is called, but it's not so easy to do? With call location tags it's really easy. And it's enabled by default.

log message

call

Disabling:

log.configure ({ locate: false }) (...)

...or:

log.noLocate (...)

Custom printer:

log.configure ({ locate: { print: ({ calleeShort, fileName, line }) => ... } }) (...)

Displaying outer call location (upwards the stack), can be useful when implementing library code / wrappers:

log.configure ({ locate: { shift: 1 }}) (...)

Manually setting call location (see the StackTracey library, which serves the purpose):

log.configure ({ locate: { where: new StackTracey ().at (2) } }) (...)

Timestamping

Disabled by default. To enable (with default options):

log = log.configure ({ time: true })

Configure formatting:

log = log.configure ({ time: { yes: true, format: 'iso' } })

Here is the correspondence between the format option value and the related Date method used for rendering:

format value Date method
"locale" (default) .toLocaleString ()
"iso" .toISOString ()
"utc" .toUTCString ()
null .toString ()

Providing locale and timezone options (available when format is set to locale):

log.configure ({
    time: {
        yes: true,
        format: 'locale',
        locale: 'en-US',
        options: { timeZone: 'America/Denver' }
    }
})

Providing a custom printer:

log.configure ({ time: { yes: true, print: x => (String (x) + ' | ').bright.cyan }}) ('Lorem ipsum dolor sit amet\nconsectetur adipiscing elit..')

pic

Backdating:

log.configure ({ time: { yes: true, when: new Date ('2017-02-27T12:45:19.951Z') }}) (...)

Specifying Additional Semantics (errors / warnings / info messages)

You can add the .error call modifier, which tells Ololog to render with the console.error instead of the console.log:

log.error ('this goes to stderr')
log.bright.red.error ('bright red error!')

Other console methods are supported as well:

log.info ('calls console.info')
log.warn ('calls console.warn')
log.debug ('calls console.debug')

Displaying The INFO / WARN / ERROR / DEBUG Tags

There is a tag stage (disabled by default) that displays the log level:

const log = require ('ololog').configure ({ tag: true })

log       ('a regular message')
log.info  ('an info message')
log.warn  ('a warning')
log.error ('an error')
log.debug ('a debug message')

screen shot 2019-01-22 at 22 22 44

Customized Tag Printer

You can completely override the tag stage, introducing new parameters and behavior (a clusterId in this example):

const bullet = require ('string.bullet') // NB: these packages are part of Ololog, no need to install them separately
const { cyan, yellow, red, dim } = require ('ansicolor')

const log = require ('ololog').configure ({

    locate: false,
    time: true,
    tag: (lines, {
            level = '',
            levelColor = { 'info': cyan, 'warn': yellow, 'error': red.bright.inverse, 'debug': blue},
            clusterId
          }) => {
        
        const clusterStr = clusterId ? ('CLUSTER[' + (clusterId + '').padStart (2, '0') + ']') : ''
        const levelStr = level && (levelColor[level] || (s => s)) (level.toUpperCase ())

        return bullet (dim (clusterStr.padStart (10)) + '\t' + levelStr.padStart (6) + '\t', lines)
    }
})
log.configure ({ tag: { clusterId: 1  } })       ('foo')
log.configure ({ tag: { clusterId: 3  } }).info  ('bar')
log.configure ({ tag: { clusterId: 27 } }).error ('a multiline\nerror\nmessage')

The output:

screen shot 2019-01-22 at 22 46 59

You can also use Custom Methods feature to make it even more concise:

log = log.methods ({

    // adds `clusterId` helper
    clusterId (n) { return this.configure ({ tag: { clusterId: n } }) }
})

log.clusterId (1)        ('foo')
log.clusterId (3) .info  ('bar')
log.clusterId (27).error ('a multiline\nerror\nmessage')

Limiting Max Argument Length

log.configure ({ trim: { max: 5 } }) ('1234567890', 'abcdefgh') // 1234… abcd…

Getting The Rendered Text

The following will execute all stages before the 'render' (screen output) stage, returning its argument:

log.before ('render') ({ foo: 42 }) // '{ foo: 42 }'

The other way of getting the text (for example, if you want to intercept it and output to your custom renderer, like Blessed) is to override the default render step, see below...

Overriding The Default Behavior

You can provide a custom implementation for certain steps in the Ololog's pipeline. For example, you can replace the render step to output the rendered text to somewhere else other than console.log:

log = log.configure ({

    render (text, { consoleMethod = '' /* can be also debug/info/error/warn */ }) {

        // receives '{ foo: 42 }' and outputs it to the Blessed library
        box.pushLine (text)
        box.scroll (1)
        screen.render ()
    }
})

log ({ foo: 42 })

You can look up all the default steps you could replace here:

https://github.com/xpl/ololog/blob/master/ololog.js#L67

Injecting Custom Code Before/After Steps

You can also bind new code to the existing methods in an aspect-oriented programming style, executing it before, after or instead – and thus overriding the default behavior. See the pipez library, which provides all the fun — with its help you could build incredibly configurable things similar to Ololog easily.

For example, if you want to write .error calls not just on screen, but to a separate file, you can do following (by injecting a custom hook after the render call):

const ololog = require ('ololog')
    , ansi   = require ('ansicolor')
    , fs     = require ('fs')
    
const log = require ('ololog').configure ({

    'render+' (text, { consoleMethod = '' }) { // adds this method after `render`
        if (consoleMethod === 'error') {
            fs.appendToFile ('error.log', '\n' + ansi.strip (text)) // strip ANSI styling codes from output
        }
        return text
    }
})

Here's a complete example on how to set up a file logging that supports different log levels:

Here's another trick that you could do by injecting a handler before the render step (that would be +render instead of render+):

Adding Custom Helper Methods

You can add your own shorthand methods/properties (will add new properties globally for any instance of the ololog, but this may change in future). An example, demonstrating how the actual indent and red chain-style helpers were implemented:

log.methods ({

    indent (level) { return this.configure ({ indent: { level: level }}) }
    get red ()     { return this.configure ({ 'concat+': lines => lines.map (ansicolor.red) }) } // executes it after the 'concat'
})

No-op Device

Use .noop to obtain a reduced instance that does nothing apart from returning its first argument:

const doesNothing = log.noop
doesNothing.bright.red ('this never shows') // simply returns 'this never shows'

Powered By

Projects That Use Ololog

  • CCXT – a cryptocurrency trading library with 100+ exchanges.

About

A better console.log for the log-driven debugging junkies

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 100.0%