- Customised output
- Tracing logs
- All logs with
.trace(will be automatically tagged, no matter where in your app it is. - use
traceTriggerto filter a set of logs to only when there is a threshold is met or exceeded - Can trace incoming requests with the w3c trace-context headers
- With
headersorheadersMappingconfigured, scribbles will intercept outgoinghttp/httpsrequestandgetcalls and the globalfetchmade inside a trace and inject trace headers π - Can automatically inject IDs into outgoing headers. Works with axios, request, global
fetch& http/https
- All logs with
- More insight in your logs
- Git repository name
- Current branch
- Last commit hash
- Environment: local / dev / prod
- Static source code analysis
- Resolve the calling location without the expensive of a stacktrace
- Automatic variable name extraction - displays variable names in log output
- Can load map file to report the correct source file and line
- Helpful when your Service is build/bundled
- Deploying Lambda style functions
- Generate performance reports
- Detailed metrics on service and host
- Flag when the eventloop is blocking. This can happen when your app is over-loaded.
scribbles is 100% agnostic. Connect your logging service of choice to either: stdOut to get a string of the entry OR dataOut to get an enriched object representing the log event
If you like it, β
it on github,
and/or share π»
The shortest possible path to a working install, by runtime. Pick one and you're done.
Node CJS (require('scribbles') β the default):
npm install --save scribblesconst scribbles = require('scribbles');
scribbles.log('hello', { user: 'alice' });
// ... <log> entry.js:2 hello { user:"alice" }No flags, no preload, no config file.
Node ESM (import ... from 'scribbles'):
npm install --save scribblesnode --import scribbles/register app.mjsimport scribbles, { config } from 'scribbles';
scribbles.log('hello', { user: 'alice' });(Equivalent via environment: NODE_OPTIONS="--import scribbles/register".)
Bun (bun run, bun test):
bun add scribbles# bunfig.toml β project root
preload = ["scribbles/register"]
[test]
preload = ["scribbles/register"]import scribbles, { config } from 'scribbles';
scribbles.log('hello', { user: 'alice' });That's the whole setup. If something looks off, the next-stop references are:
docs/troubleshooting.mdβ common errors and fixesdocs/runtime-setup.mdβ deeper runtime-specific referenceregister.status()β Scribbles' self-diagnostic; always the first thing to check when debugging
Scribbles supports Node and Bun.
| Runtime | Minimum version | Setup |
|---|---|---|
Node CJS (require('scribbles')) |
8.5.0 | None β works out of the box |
Node ESM (import scribbles from 'scribbles') |
20.6 | One-line preload (below) |
Bun (runtime, bun test, ESM) |
1.0 | One-line bunfig.toml entry (below) |
npm install --save scribbles
yarn add scribbles
bun add scribbles
For Node CJS (require('scribbles')), automatic variable-name extraction
works out of the box β no flags or setup. You're done, just use the library.
For Node ESM (import scribbles from 'scribbles'), add one preload to
your Node invocation so Scribbles can install its source-transform loader
before your module graph is parsed:
node --import scribbles/register app.mjsEquivalent via NODE_OPTIONS:
NODE_OPTIONS="--import scribbles/register" node app.mjsFor Bun (bun run, bun test), add the following to bunfig.toml:
preload = ["scribbles/register"]
[test]
preload = ["scribbles/register"]Bun treats bun run and bun test as separate execution contexts with
separate preload configuration. The top-level entry covers bun run
and the bun <file> shorthand; the [test] section covers bun test.
Setting both is the idiomatic full-coverage configuration. Detailed manual-setup instructions, troubleshooting, and
edge cases are in docs/runtime-setup.md.
What Bun can and cannot rewrite (Bun runs its entry file itself and a runtime plugin can only return ES modules, so this differs from Node):
- Variable names appear for
.mjs,.ts,.tsx,.jsxmodules (the preload plugin) and for CommonJS.jsmodules that arerequired after scribbles loads. - They do not appear for the entry file when it is a
.js/.cjsfile, nor for ES-module files named.js. Put thescribbles.<level>(...)calls in another module, or name the file.mjs/.ts. (node --require scribbles/register app.jsdoes cover a CommonJS entry on Node.) bun --hotandbun --watchreload when you edit a module that logs, with or without the preload.- A
bun build --compileexecutable needs thescribbles/bun-pluginbuild plugin to keep file and variable names; seedocs/runtime-setup.md.
If you run Scribbles in an ESM context without the preload, it still
works β you get correct file/line/col from stack traces β but automatic
variable-name extraction is silently unavailable. A one-line warning will
print to stderr on every startup until you wire the preload.
You can programmatically verify the preload at boot from a CI or production init script:
const { register } = require('scribbles');
register.assert(); // throws with a helpful message if transform not activeStart with docs/troubleshooting.md. It
indexes the common errors by their exact message text and walks through
each fix. The first diagnostic is always:
const { register } = require('scribbles');
console.log(register.status());β¦which returns a structured { runtime, cjsInstalled, esmPreloaded, transformActive, instructions? } object that tells you which half of
the transform is (or isn't) active right now.
See MIGRATION.md. The TL;DR:
- Your existing CJS code (
const scribbles = require('scribbles'); scribbles.config(...)) continues to work unchanged. - v2 adds ESM support, Bun support (
bun run,bun test, andbun buildbundles with thescribbles/bun-pluginbuild plugin), a newregisternamed export, and fixes a v1.7.0 bug where automatic variable-name extraction silently never activated. - In ESM, the v2.0.0 "hard cut" applies:
scribbles.config(...)on the default import isundefined. Use named imports βimport scribbles, { config, trace, register } from 'scribbles'β for anything that is not a log level. The CJS entry keeps both property access and destructuring available for back-compat.
const scribbles = require('scribbles');
scribbles.log("hello world")
// myRepo:local:master [ ] 2022-06-27T16:24:06.473 #3d608bf <log> index.js:174 hello worldscribbles[logLevel](message, [value, [error]])
Each log level function has an .at() method that allows you to override the source location. This is useful when building logging wrappers or libraries:
// Override file and line number in log output
scribbles.log.at({ file: 'my-module.js', line: 42, col: 0, args: [] }, 'Custom location');
// All log levels support .at()
scribbles.error.at(from, 'Error message', errorObject);
scribbles.warn.at(from, 'Warning message');
scribbles.info.at(from, 'Info message', { data: 123 });The from object requires:
file: Source file nameline: Line numbercol: Column number (can be 0)args: Array for template argument names (use[]for basic usage)
Scribbles ships both CJS (index.d.ts) and ESM (index.d.mts)
TypeScript definitions. The exports map in package.json routes
import consumers to the ESM types automatically. Key interfaces:
ScribblesConfigβ Configuration optionsLogEntryβ Structured log entry returned by log functionsTraceOptionsβ Options for thetracefunctionLogFunctionβ Type for log level functionsRegister/RegisterStatusβ v2 runtime-registration helpers
// ESM (v2 idiom β named import for config)
import scribbles, { config, LogEntry, ScribblesConfig } from 'scribbles';
const opts: Partial<ScribblesConfig> = {
logLevel: 'info',
dataOut: (entry: LogEntry) => console.log(entry)
};
config(opts);
scribbles.log('hello');// CJS (v1-style property access still works)
import scribbles = require('scribbles');
scribbles.config({ logLevel: 'info' });
scribbles.log('hello');There is a config that takes a configuration object.
- stdOut [function] - defaults:
console- Redirect the string output of the log entry
- dataOut [function]
- A callback to receive an object representing the log entry
- onSinkError [function] -
(error, sink, entry) => void- A failing
stdOutordataOutnever throws into the code that logged; see When a destination fails
- A failing
- stringify [function]
- A function used in stdOut to parsing for values into a string
- hijack [boolean] - default:
true- Enable or disable automatic HTTP/HTTPS request interception for trace header injection
- Set to
falseto disable automatic header injection into outgoing requests - Only a
"scribbles": { "hijack": false }block in yourpackage.jsontakes effect: the wrappers are installed once, while scribbles loads, soscribbles.config({ hijack: false })afterwards does not remove them
- mode [string] - default: 'dev'
- Can use NODE_ENV from environment variables
- format [string] - defaults: "{repo}:{mode}:{branch} [{spanLabel} {spanId}] {time} #{hash} <{logLevel}> {fileName}:{lineNumber} {message} {value} {stackTrace}"
- git:
repo: The git repository name as it appears on the originbranch: The current git branchhash: Short git hash of current commit
- trace:
traceId: Used for distributed tracing in microservices [more]spanId: the execution of a client call [more]span64: the base64 encoded version ofspanIdspanLabel: A label to identify this tracetracestate: Ordered list of key/value hopsurl: Full request URL with query string (middleware only)path: URL path without query string (middleware only)query: Query string parameters object (middleware only)params: Route parameters object (middleware only)method: HTTP method e.g. GET, POST (middleware only)
- info:
time: Time of logginglogLevel: The logging level for this entryhostname: The hostname of the operating system.instance: a base16 value representing the current instancemode: The environment your application is running in. e.g. local, dev, prod etc..
- context:
fileName: The file namelineNumber: The line in the filemethod: The calling method or function name
- input:
message: Message to logvalue: Values to logstackTrace: The stack trace if an Error object was passed
v: The version of scribbles used to create this entry. This allows matching log body with parsers. As the layout may change with new versions.
- git:
- time [string] - defaults: "YYYY-MM-DDTHH:mm:ss.SSS"
- traceTrigger [string] *
- Used within trace call.
- Log events will be stored and only push out if the scribbles[logLevel] >= the traceTrigger level
- logLevel [string] - defaults: "debug"
- Report on this level and higher
- Can use LOG_LEVEL from environment variables
- Must be one of
levels(exact, case-sensitive). Anything else ("warning"with the default levels,LOG_LEVEL=bogus) makesconfig()throw anErrorthat names the bad value and lists the valid levels, and the previous setup is kept. It used to silence every log call, errors included. The environment variable is applied when scribbles loads, so a badLOG_LEVELstops the app at start-up instead of running without logs: scribbles prints one line (scribbles: Unknown logLevel "bogus". It must be one of the configured levels: ...) and exits with status 1; atry { require('scribbles') } catch {}cannot catch it. The same applies to an unknownlogLevelin yourpackage.jsonscribblesblock. Other start-up errors still throw to the caller. If you changelevels, pass a matchinglogLevelin the same call.
- levels [array] - defaults:
["error", "warn", "log", "info", "debug"]- Messages will be filtered from the
logLevelto the start of the array - These log levels will also be available as functions on scribbles
- Reserved names (cannot be used as level names):
config,trace,middleware,status,timer,timerEnd,group
- Messages will be filtered from the
- headers [string/array/null]
- activated when using scribbles.middleware...
stringof a header name to forwardRegExpto match header names to forwardarrayof header names to forward. Can exact matchingstrings and/orRegExps.nullto disable forwarding headers
- headersMapping [object:[array/string]]
- An
objectof output keys with input selector values. - Values can be a
stringORarray of strings: that will be taken in order of preference to be selected (i.e. If an incoming request has a header named the same as first index. Use that, else check the next index and so on)
- An
- gitEnv [Object] - the attribute names at the end of their
process.envhash: attribute name of git short hashrepo: attribute name of git repository namebranch: attribute name of git branch- Scribbles reads the repo, branch and commit from git when it starts. Outside a git checkout (or without the
gitcommand) it prints nothing and leaves them empty unlessgitEnvsupplies them.
- pretty [Object]
inlineCharacterLimit[number]: Will inline values up to this length. ~ In Dev Mode, this will be set to the width of your terminal or default to80indent[string]: preferred indentation. Defaults" "~ 2 spacesdepth[number]: This represents how many nested steps in the object/array tree are to be walkedsingleQuotes[string]: Set to true to get single-quoted strings. Default:falsefilter(object, key) [function]: Expected to return a boolean of whether to include the property in the output.transform(object, key, val) [function]: Expected to return a string that transforms the string that resulted from stringifying a given property.- This can be used to detect special types of objects that need to be stringified in a particular way, or to return an alternate string in this case. e.g. given a field named "password" return "****"
- global [string/object]
"console"- Attach log functions to the console object (replaces console.log, console.error, etc.)"global"- Attach log functions to the global objectobject- Attach log functions to a custom object
- edgeLookupHash [boolean] - default:
false- When enabled, replaces
tracestateheaders with a short hash for reduced bandwidth - The full tracestate is stored in memory and restored on incoming requests
- See Edge Lookup Hash for details
- When enabled, replaces
- colors [boolean/undefined] - default: auto-detect
- Enable or disable colored terminal output
- When
undefined: enabled in dev mode (if TTY), disabled in production - Respects
NO_COLORandFORCE_COLORenvironment variables
- colorScheme [object]
- Custom color mapping for log levels
- Available colors:
red,green,yellow,blue,magenta,cyan,white,gray,brightRed,brightGreen,brightYellow,brightBlue,brightMagenta,brightCyan,dim,bold - Default scheme:
{ error: 'red', warn: 'yellow', log: 'cyan', info: 'green', debug: 'gray' }
- colorblindMode [boolean] - default:
false- Use colorblind-friendly color scheme with higher contrast and distinct brightness levels
Scribbles automatically serializes values passed to log functions into readable string representations. Here's how different data types are formatted:
| Type | Example | Output |
|---|---|---|
null |
null |
null |
undefined |
undefined |
undefined |
number |
123 |
123 |
boolean |
true |
true |
string |
"hello" |
'hello' |
NaN |
NaN |
NaN |
Symbol |
Symbol('s') |
Symbol(s) |
| Type | Example | Output |
|---|---|---|
object |
{foo:'bar'} |
{ foo:'bar' } |
array |
[1,2,3] |
[ 1, 2, 3 ] |
| nested array | [[1,2]] |
[ [ 1, 2 ] ] |
| nested object | {a:{b:1}} |
{ a:{ b:1 } } |
| Type | Example | Output |
|---|---|---|
Date |
new Date('2024-01-01') |
Date(2024-01-01T00:00:00.000Z) |
Error |
new Error('oops') |
Error("oops") |
Buffer |
Buffer.from([1,2,3]) |
Buffer[ 1, 2, 3 ] |
Map |
new Map([['a',1]]) |
Map{ a:1 } |
Set |
new Set([1,2]) |
Set[ 1, 2 ] |
RegExp |
/foo/gi |
/foo/gi |
| Type | Example | Output |
|---|---|---|
| arrow function | (a,b)=>{} |
(a,b)=>{-} |
| named function | function foo(){} |
foo(){-} |
| anonymous function | function(){} |
Ζ(){-} |
Circular references are detected and displayed safely:
const obj = { name: 'test' };
obj.self = obj;
scribbles.log(obj);
// Output: { name:'test', self:{ ...! } }
const arr = [1, 2];
arr.push(arr);
scribbles.log(arr);
// Output: [ 1, 2, [ ...! ] ]Use pretty.depth to limit how deep nested structures are displayed:
scribbles.config({ pretty: { depth: 2 } });
scribbles.log({ a: { b: { c: { d: 1 } } } });
// Output: { a:{ b:{ + } } }Strings that look like JSON are prefixed to avoid confusion:
scribbles.log("{not_json}");
// Output: String"{not_json}"
scribbles.log("[1,2,3]");
// Output: String"[1,2,3]"Scribbles performs static code analysis at load time to automatically detect and display the names of variables passed to log functions. This makes it easier to identify which variable you're logging without manually adding labels.
When you call scribbles.log(myVariable), scribbles automatically extracts "myVariable" from the source code and includes it in the output:
const userData = { name: 'Alice', age: 30 };
scribbles.log(userData);
// Output: ... userData:{ name:'Alice', age:30 }
const count = 42;
scribbles.log(count);
// Output: ... count:42Variable names are extracted for array access, object properties, and more:
const users = [{ name: 'Alice' }, { name: 'Bob' }];
scribbles.log(users[0]);
// Output: ... users[0]:{ name:'Alice' }
const config = { db: { host: 'localhost' } };
scribbles.log(config.db);
// Output: ... config.db:{ host:'localhost' }When logging values, scribbles adds type annotations to help identify the data type:
| Type | Annotation |
|---|---|
| Function | varName:f(){..} |
| Error | varName:Error-Error() |
| Date | varName:Date(2024-01-01T00:00:00.000Z) |
| Buffer | varName:Buffer[..] |
| Map | varName:Map{..} |
| Set | varName:Set[..] |
| Array | varName:[..] |
| Object | varName:{..} |
| String | varName:"value" |
- Works across Node.js (
require/import) and Bun (bun run,bun test,bun build) β ESM contexts require thescribbles/registerpreload (see "Runtime support" above). ABun.buildbundle runs without the preload; add thescribbles/bun-pluginbuild plugin (seedocs/runtime-setup.md) to keep variable names in it, otherwise it logs without them. Bundled/transpiled code still needs source maps for file/line accuracy. - Literal values (strings, numbers, booleans) don't get variable name extraction since there's no variable name to extract
Via package.json
Just add a "scribbles" attribute
{
"name": "myrepo",
"version": "0.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
},
"scribbles":{
"mode":"test-runner",
"logLevel":"warn",
"levels":["danger", "error", "warn", "log", "info", "debug"],
"format":"{time} [{mode}#{hash}] <{logLevel}> {message}"
}
}Via .config(...) function
scribbles.config({
mode:'test-runner',
logLevel:"warn", //only output warning messages or higher
levels:["danger", "error", "warn", "log", "info", "debug"],
format:'{time} [{mode}#{hash}] <{logLevel}> {message}'
})
scribbles.danger("hello world")
// 2022-06-27T16:24:06.473 [test-runner#3d608bf] <danger> hello worldstdOut and dataOut are your code (a log vendor, a socket, a formatter) and can throw. A logging call never throws because of them: the error is caught, the other destination still receives the entry, and your request handler carries on.
scribbles.config({
dataOut: (entry) => vendor.send(entry), // may throw when the vendor is down
onSinkError: (error, sink, entry) => {
// sink is 'stdOut' or 'dataOut'; entry is the log entry that was not delivered
metrics.increment('logging.sink_failed', { sink })
}
})
scribbles.info('order placed') // never throws, even if vendor.send throwssinkis'stdOut'when the printer (or the formatting of the line, e.g. a throwingstringify) failed and'dataOut'whendataOutfailed.- If
onSinkErroritself throws, that is ignored too. - Without
onSinkError, the first failure of each destination function is written once toconsole.error(scribbles: dataOut failed and the entry was not delivered: ...) and later failures of that same function are silent, so a dead shipper is visible without flooding stderr. Setting a new destination function re-arms the report. - A
stdOutobject that has no function for an enabled level (and nologfunction to fall back on) is a setup mistake:config()throws<level> was not found on stdOutand keeps the previous setup. Levels switched off bylogLeveldo not need a function. The special levelstimer,groupandstatusare only known when they are logged, so a missing function for them is reported throughonSinkErrorlike any other destination failure.
There is also a option in config to set the dataOut that will receive an object representing the log entry.
scribbles.config({
dataOut:function(data){
console.log(data);
}
})
scribbles.log("hello world")
/*{
v:"1.2.3",
git:{
repo:"myRepo",
branch:"master",
hash:"3d608bf"
},
trace:{
traceId: "...",
parentId: "...", // caller's span id, when traceId was a full traceparent
spanId: "...",
spanLabel: "...",
...
},
info:{
time:2022-06-27T16:24:06.473Z,
mode:"local",
hostname:"box",
logLevel:"log",
instance:"bfc977a"
},
context:{
fileName:"index.js",
lineNumber:174,
method:"myFunction", // The calling function name
groupLevel:0, // Nesting depth when inside groups
groupLabel:"" // Labels of parent groups
},
input:{
message: "hello world",
value: undefined, // The value argument if provided
stackTrace: undefined, // Error stack trace if Error was passed
originalMessage: undefined, // Original error message when logging with separate message
from: ["at Object.<anonymous> (index.js:174:1)", ...] // Full call stack
},
}*/You can configure scribbles to attach its log functions to the console or global object:
scribbles.config({ global: "console" })
// Now you can use:
console.log("hello world") // Uses scribbles formatting
console.error("something went wrong") // Uses scribbles formattingThis is helpful for gradually adopting scribbles in an existing codebase, or for ensuring all logging uses the same format without changing existing console.log calls.
π You can also poll the performance of your service. By calling scribbles.status(...)
This will attach an additional attribute to the dataOut and will not be available in stdOut.
- status:
state: the state of the services. e.g. "up", "blocking"cpu: CPU infocores: number of available coresmodel: description of the processorspeed: MHz frequency speedpercUsed: load on process as percentagepercFree: available on process as percentage
sys: System infostartedAt: when the system was startedarch: platform architecture. e.g "x64"platform: the operating system platformtotalMem: the total megabytes of memory being usedfreeMem: the total megabytes of memory freeusedMem: the total megabytes of memory being used
process: Node process infopercUsedCpu: the percentage of processing power being used by this processpercFreeMem: the percentage of memory being used by this processusedMem: the total megabytes of memory being used by this processstartedAt: when it's process was startedpTitle: the current process title (i.e. returns the current value of ps)pid: the ID of the processppid: the ID of the current parent processuser: node the name of the user who started nodevNode: version of node
network: Networking infoport: listening on this Portconnections: number of current established connections
scribbles.config({
dataOut:console.log
})
setInterval(function(){
scribbles.status();
}, 5000);This will give you a performance snapshot every 5 seconds.
You can get Scribbles to store the Git info at build time, in order to show in the run time logs
Via webpack.config.js
Here is how to add git status to you logs.
//...
const ScribblesWithGitInBundle = require('scribbles/gitStatus');
//...
module.exports = {
//...
plugins: [
ScribblesWithGitInBundle,
// ...
]
//...
};That's it!
You can select then via the config opts.
Via package.js
{
...,
"scribbles":{
...,
"gitEnv": {
"hash":"GITHUB_SHA",
"repo":"GITHUB_REPOSITORY",
"branch":"GITHUB_REF"
}
}
}Tip: If you are using Heroku, the git hash is storted in the "SOURCE_VERSION"
To get accurate file & line references when transpiling your code, you will need to generate source maps as part of your build.
-
Generate source maps as part of the build
- In your
tsconfig.jsonadd"sourceMap": trueunder "compilerOptions" - For esbuild, add
--sourcemapflag - For webpack, set
devtool: 'source-map'in your config
- In your
-
Source map support in Node
- Scribbles automatically installs source-map-support, so transpiled code will resolve to original file locations out of the box
- Alternatively for Node v12.12+ : you can add the
--enable-source-mapsflag to the node command for native support
You can start a timer to calculate the duration of a specific operation. To start one, call the scribbles.timer(tag,[message]) function, giving it a name and an optional message. To stop the timer, just call the scribbles.timerEnd(tag,[message]) function, again passing the timer's name as the first parameter.
There are 3 functions:
scribbles.timer()- Starts and/or logs the current value a timer based on the tag passed to the function.scribbles.timerEnd()- logs the current value a timer and removes it, to be used later.
scribbles.timer("Yo")
setTimeout(()=>{
scribbles.timer("Yo","123")
setTimeout(()=>{
scribbles.timerEnd("Yo","done!")
}, 300)
}, 400)Output: You see the time it took to run the code
myRepo:local:master [ ] 2022-06-27T13:04:52.133 <timer> app.js:18 Yo (+0.00ms|0.00ms)
myRepo:local:master [ ] 2022-06-27T13:04:52.552 <timer> app.js:20 Yo:123 (+419.40ms|419.40ms)
myRepo:local:master [ ] 2022-06-27T13:04:52.875 <timerEnd> app.js:22 Yo:done! (+323.21ms|742.61ms)
You can group related log messages together using scribbles.group.start() and scribbles.group.end(). This is useful for organizing logs from complex operations.
const groupId = scribbles.group.start('User Authentication')
scribbles.log('Checking credentials')
scribbles.log('Validating token')
scribbles.group.end()Output:
myRepo:local:master [ ] 2022-06-27T16:24:06.473 #3d608bf <group> index.js:10 User Authentication
myRepo:local:master [ ] 2022-06-27T16:24:06.474 #3d608bf <log> index.js:11 Checking credentials
myRepo:local:master [ ] 2022-06-27T16:24:06.475 #3d608bf <log> index.js:12 Validating token
myRepo:local:master [ ] 2022-06-27T16:24:06.476 #3d608bf <groupEnd> index.js:13 User Authentication
scribbles.group.start(label)- Starts a new group with an optional label. Returns a unique group ID.scribbles.group.collapsed(label)- Starts a collapsed group (for log viewers that support collapsing).scribbles.group.end(...)- Ends a group. LIFO:end()orend("caption"). By id:end(groupId)orend(groupId, "caption")(closes that group and nested groups). ThegroupEndline shows the start label, an override string, or"Group"as documented indocs/argument-combinations.md.
Groups can be nested for hierarchical organization:
scribbles.group.start('Request Handler')
scribbles.log('Received request')
scribbles.group.start('Database Query')
scribbles.log('Connecting to database')
scribbles.log('Executing query')
scribbles.group.end()
scribbles.log('Sending response')
scribbles.group.end()You can close a specific group (and all groups nested inside it) by passing its ID:
const outerGroup = scribbles.group.start('Outer')
scribbles.group.start('Inner 1')
scribbles.group.start('Inner 2')
scribbles.group.end(outerGroup) // Closes all three groups at onceFor enhanced visual grouping, enable ASCII brackets with the pretty.groupBrackets option:
scribbles.config({
pretty: { groupBrackets: true }
})
scribbles.group.start('User Authentication')
scribbles.log('Checking credentials')
scribbles.log('Validating token')
scribbles.group.end()Output (tree rails; with colors + colorScheme, each depth can use a distinct lane hue):
β myRepo:local:master [ ] ... <group> User Authentication
β myRepo:local:master [ ] ... <log> Checking credentials
β myRepo:local:master [ ] ... <log> Validating token
β myRepo:local:master [ ] ... <groupEnd> index.js:13 User Authentication
Scribbles supports colored terminal output to make logs easier to read. Colors are automatically enabled in development mode and disabled in production.
Colors work automatically - just use scribbles as normal:
scribbles.error('Something went wrong') // Red
scribbles.warn('Be careful') // Yellow
scribbles.log('Processing...') // Cyan
scribbles.info('Connected') // Green
scribbles.debug('Variable: x') // Gray// Force enable colors
scribbles.config({ colors: true })
// Force disable colors
scribbles.config({ colors: false })
// Custom color scheme
scribbles.config({
colors: true,
colorScheme: {
error: 'brightRed',
warn: 'brightYellow',
log: 'blue',
info: 'cyan',
debug: 'dim'
}
})
// Colorblind-friendly mode
scribbles.config({
colors: true,
colorblindMode: true
})
// Syntax-highlighted values in the `{value}` column are on by default when `colors` is true.
// Opt out (whole-line level color only, no key/bracket roles):
scribbles.config({
colors: true,
pretty: { syntaxHighlight: false }
})
// Optional: tune role colors (syntax highlighting stays on unless you set syntaxHighlight: false)
scribbles.config({
colors: true,
pretty: {
syntaxColorScheme: { number: 'brightBlue' }
}
})When pretty.syntaxHighlight is true (the default) together with colors, scribbles paints Map/Set/Date/Error shapes, keys, brackets, strings, numbers, circular markers ({ ...! }), depth truncation ({ + }), function shorthands, variable-name labels (userData:), and String"β¦" disambiguation using distinct ANSI roles. The template prefix before {value} is not wrapped in a single outer level color: either semantic prose (38;2 tiers from pretty.proseColor, default on) or, when pretty.prosePrefix is false, a flat colorScheme[level] wash on that prefix onlyβso inner syntax colors are not clipped by one outer reset. dataOut still receives raw structured entries without ANSI. Custom stringify bypasses automatic value highlighting.
When colors, pretty.syntaxHighlight, and pretty.prosePrefix (default true) are on, the template prefix uses 24-bit foreground sequences (\x1b[38;2;r;g;bm) from an anchor pretty.proseColor (default #DDDDDD) β even when the {value} column is empty or has no syntax SGR, so early βmessage-onlyβ lines match later object logs. Layout still comes from config.format (see default in src/core/config.js); brightness comes from token names, not placeholder order.
| Tier | Tokens | Default darken (toward black) |
|---|---|---|
| A β git | repo, mode, branch, hash |
0.55 |
| B β trace | spanLabel, spanId |
0.50 |
| C β time | time |
0.30 |
| D β locate + severity | fileName, lineNumber, logLevel |
0.12 |
| E β caption | message |
0 |
Literals (:, #, spaces, β¦) use the same darken as their enclosing region (or max of neighbors outside wrappers). Wrappers [], <>, (), {} are paired best-effort; delimiters are slightly darker than the inner reference (pretty.proseWrapperExtraDarken, default 0.05). Empty inner spans use pretty.proseEmptyRegionDarken (default 0.75). Empty {message} uses that same empty-region treatment. Non-empty {message} can be lifted toward the anchor with pretty.proseMessageLift. {logLevel} adds colorScheme[level] as a nested 16-color accent inside the prose dim band. Override per-token factors with pretty.proseFieldDarken (shallow merge over defaults) and scale everything with pretty.proseImportanceScale.
Multi-line: After compile, the line is split on \n. Line 0 gets the prose walk on the prefix before {value}; following lines (e.g. {stackTrace}) are dimmed with pretty.proseContinuationDarken (default 0.65). If {value} ever embeds newlines, prefix detection on line 0 could desyncβavoid that shape or set prosePrefix: false.
Tuning keys: proseColor, prosePrefix, proseImportanceScale, proseFieldDarken, proseMessageLift, proseWrapperExtraDarken, proseEmptyRegionDarken, proseContinuationDarken. NO_COLOR / colors: false disables all SGR, including prose.
Examples
- Minimal β default
format,proseColor: '#DDDDDD',scribbles.log('hello', { x: 1 }): git block muted, time mid,<log>with cyan level inside dim prose, message brighter, value column syntax-highlighted. - Green anchor β
proseColor: '#00FF00': tiers interpolate toward black from green instead of gray. - Multi-line β
scribbles.error('ctx', {}, err)with{stackTrace}informat: first line full prose; stack lines a single continuation38;2per line.
Integration coverage: __tests__/50-prose-prefix.test.js.
When it runs. Role-based ANSI is applied to the rendered {value} column when colors is true, pretty.syntaxHighlight is not set to false (it defaults to on), and you are not using a custom config.stringify. A custom stringify receives the raw pretty options but scribbles does not post-paint its return value. Setting colors: false or honoring NO_COLOR disables syntax SGR along with all other colors.
Palettes. In normal mode, defaults follow src/formatting/stringifySyntax.js (defaultSyntaxColorScheme). With colorblindMode: true, the base map is syntaxColorblindScheme; pretty.syntaxColorScheme is shallow-merged on top for whichever mode is active, so you can override individual roles without redefining the full map.
Line layout. Prefix coloring assumes the {value} format token is filled with the same string scribbles uses for detection (the rendered value column, including optional name: labels). Keep the default {message} {value} ordering unless you know duplicated substrings could make the prefix split ambiguous; integration coverage for stdout lives in __tests__/49-syntax-highlight.test.js.
syntaxColorScheme role keys. Each value must be a color name accepted by the formatter (see Available Colors in this file). Roles: bracket, comma, colon, typeName, key, string, number, boolean, nullish, symbol, regex, function, arrayIndex, circular, truncation, identifierLabel (text before : for extracted variable names), disambiguationPrefix (the String prefix in String"β¦"), fallback (unknown role), transformOutput (segments produced after pretty.transform that are not re-lexed).
Scribbles respects standard environment variables:
NO_COLOR- Disables all colors (https://no-color.org/)FORCE_COLOR- Forces colors even when not a TTYCI- Colors are automatically enabled in CI environments (when in dev mode)
Standard: red, green, yellow, blue, magenta, cyan, white, gray
Bright: brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan
Styles: bold, dim, underline
Note:
scribbles.trace()is for distributed tracing (correlating logs across microservices), not forconsole.trace()-style stack trace output. For stack traces, pass an Error object to any log function (e.g.,scribbles.log("msg", new Error())) and use the{stackTrace}format token. ThedataOutcallback also receives afromproperty with the full call stack.
When trying to debug a problem with logs that are intertwined. A stacktrace will give you limited information. You can see where the Error occurred and a message. However you cannot see the values as it flow through your system.
Using the trace system. Each log entry will be able to be connected, as it flows through your system.
To use the trace system you only need to pass in a root function that acts as the start of the execution. Everything that is executed within this function will be matched to the same trace ID, and name if provided.
scribbles.trace([label/opt,]next_fu) // returns whatever next_fu returns
next_fu is called with two arguments: the span id string, next_fu(spanId, { traceId, spanId }).
Code that only reads the first argument as the span id keeps working; the second is the
{ traceId, spanId } context. scribbles.trace returns exactly what next_fu returns, so a
synchronous handler's value reaches the caller, and an async handler's promise is returned as-is:
you can await the result and its rejection reaches you instead of becoming unhandled.
const reply = await scribbles.trace("job", async (spanId, { traceId }) => {
const data = await fetchData() // logs written here carry the same trace id
return data
})The first argument to can be an options object. Here you can specify a spanLabel to tag your entries, atraceId & the tracestate that are using in distributed tracing.
index.js
// for fun lets set a custom format for our logs
scribbles.config({format:`[{traceName} {spanId}] {message}`})
// an example of an event handler
function incoming(dataIn){
// wrap the work we want to do in `scribbles.trace`
scribbles.trace("eventstream",(spanId)=>{
// Event message logged from with here will have this correlation ID
// spanId = 090e8e40000005
workToDo(dataIn); // kick of the work
})
}eventstream.js
function workToDo(dataIn){
// ...
// user scribbles as normal
scribbles.log("Doing something with the eventstream")
// [eventstream 090e8e40000005] Doing something with the eventstream
// ...
}Log just the timeline of events you care about.
By setting a "traceTrigger" level. All scribbles calls within a trace context will respect it. If a call is make that matchs or exceds this level. Past, current & new events in this context will be outputted, else they will be suppressed
scribbles.config({
// Any call to error or higher will trigger all logs from logLevel for this specific this execution context
traceTrigger:"error",
// logLevel dont not have to be set. Will default to All
logLevel:'warning',
// Levels do not need to be set this is only for demonstration purposes
levels:['fatal','error','warning','info'],
})
scribbles.trace('in_trace',()=>{
scribbles.info(" --- Will NEVER be shown") // as this is below the logLevel
setTimeout(()=>{
// will be store, but will not be sent out at this point
scribbles.warning(" --- Wait")
setTimeout(()=>{
// traceTrigger exceded! store event will be sent out + this event
scribbles.fatal(" --- Now!")
setTimeout(()=>{
// will be sent out at the traceTrigger was already hit, for this *trace context*
scribbles.warning(" --- More!")
}, 500)
}, 500)
}, 500)
})
/*
myRepo:local:master [in_trace 3b3c6000000001] 2022-06-27T09:22:52.588 #3d608bf <warning> app.js:14 --- Wait
myRepo:local:master [in_trace 3b3c6000000001] 2022-06-27T09:22:52.890 #3d608bf <fatal> app.js:16 --- Now!
myRepo:local:master [in_trace 3b3c6000000001] 2022-06-27T09:22:53.199 #3d608bf <warning> app.js:18 --- More!
*/in accordance with W3C trace-context
Distributed tracing is powerful and makes it easy for developers to find the causes of issues in highly-distributed microservices applications, as they track how a single interaction was processed across multiple services. But while tracing has exploded in popularity in recent years, there still isnβt much built-in support for it in languages, web frameworks, load balancers, and other components, which can lead to missing components or broken traces.
π€ Generating and attaching trace-context values to request headers is a standardized way of addressing this problem.
Instrumenting web frameworks, storage clients, application code, etc. to make tracing work out of the box. π₯³
const scribbles = require('scribbles');
const axios = require('axios');
const express = require('express');
scribbles.config({
headers:["X-Amzn-Trace-Id"]
});
const app = express();
// start a trace for each incoming request.
app.use(scribbles.middleware.express);
app.get('/', function (req, res){
scribbles.log("incoming");
// myRepo:local:master [198.10.120.12 090e8e40000005] 2022-06-27T16:24:06.473 #3d608bf <log> index.js:174 incoming
// Just by calling this other service normally, scribbles will inject the tracing headers
axios.get('https://some.domain.com/foo/')
.then(response => {
scribbles.log(response.data);
res.send("fin")
})
.catch(error => {
scribbles.error(error);
});
}) // END app.get '/'
app.listen(port, () => scribbles.status(`App is ready!`))Example above is for axios but it will also work with http/https and request
Injection needs headers or headersMapping in the config, and a trace to propagate (a call made outside any scribbles.trace(...) leaves exactly as you wrote it).
http.request,http.get,https.requestandhttps.get, with any call shape (url,url, callback,url, options, callback,options, callback). Headers you set yourself are kept, and if you settraceparentyourself yours wins.- The global
fetch(Node 18+ and Bun):fetch(url),fetch(new URL(...)),fetch(request)andfetch(url, init), withinit.headersas an object, aHeadersor an array of pairs. Your method, body, signal and headers are untouched (asfetchitself does,init.headersreplaces aRequest's headers), and yourHeaders/Requestobjects are not modified. - Anything you reach through the module after scribbles loads:
http.get(...),const { get } = require('http')below yourrequire('scribbles'), and on Nodeimport { get } from 'node:http'. - Not covered: a function copied out of
http/httpsbefore scribbles loads (const { request } = require('http')aboverequire('scribbles')) is the native function. Load scribbles first. Bun does not refreshimport { get } from 'node:http'named imports; userequireor callhttp.get(...)through the module there. - A
fetchreference copied before scribbles loads (const f = fetchaboverequire('scribbles')) is the native function, as above. A runtime with no globalfetchsimply gets the http wrappers. - Libraries that bundle their own HTTP client rather than calling Node's
http(orfetch) are not covered either; passscribbles.trace.headers()to them yourself.
It may look something like this
function traceMiddleware({headers}, res, next){
scribbles.trace({
// You can pass the traceparent as the traceId
// or you can pull the traceId from the traceparent and pass that
traceId:headers.traceparent,
tracestate:headers.tracestate,
// lets tag the current trace/span with the caller's IP
spanLabel:headers['x-forwarded-for']
},(spanId) => next());
} // END expressSecurity Note: Headers like
x-forwarded-forcan be spoofed by clients. Only trust these headers when your application runs behind a trusted reverse proxy (e.g., AWS ALB, Nginx, CloudFlare). For Express apps, configureapp.set('trust proxy', ...)appropriately. ThespanLabelis intended for logging/tracing context onlyβdo not use it for security-critical decisions like authentication or rate limiting. For more robust proxy detection, consider using IP intelligence API services.
app.get('/', function (req, res){
scribbles.log("incoming");
// myRepo:local:master [198.10.120.12 090e8e40000005] 2022-06-27T16:24:06.473 #3d608bf <log> index.js:174 incoming
axios.get('https://some.domain.com/foo/',{
headers:scribbles.trace.headers() // tracing header IDs
})
.then(response => {
scribbles.log(response.data);
res.send("fin")
})
.catch(error => {
scribbles.error(error);
});
}) // END app.get '/'The trace.headers() function returns an object containing:
traceparent- W3C trace context parent headertracestate- W3C trace context state headerx-git-hash- The current git commit hash (automatically included, omitted when scribbles cannot find a git hash, for example in a build without a.gitfolder)
Call it inside a trace. Outside a trace there is nothing to propagate, so trace.headers() returns only the headers you passed in (none of the three above), and automatic injection into outgoing requests sends nothing either.
Use headersMapping to rename headers from incoming requests to different names for outgoing requests. This is useful when different services use different header naming conventions.
scribbles.config({
// Simple mapping: rename 'x-request-id' to 'x-trace-id'
headersMapping: {
'x-trace-id': 'x-request-id'
}
});
// With fallback priority: try 'x-correlation-id' first, then 'x-request-id'
scribbles.config({
headersMapping: {
'x-trace-id': ['x-correlation-id', 'x-request-id']
}
});The array form checks headers in order - the first matching header is used.
When tracing across many microservices, the tracestate header can grow large (up to 512 bytes with 32 vendors per W3C spec). This can cause performance issues, especially at network edges where bandwidth matters.
The edge lookup hash feature lets you replace the full tracestate with a short hash at your infrastructure edge. The original tracestate is stored in an in-memory lookup table and can be retrieved by downstream services.
scribbles.config({
edgeLookupHash: true
});How it works:
- When
edgeLookupHashis enabled, outgoing requests will have theirtracestateheader replaced with a short hash (e.g.,h:a1b2c3d4e5f67890) - The full tracestate is stored in memory, keyed by the hash
- When an incoming request has a hash-format tracestate, scribbles automatically looks up and restores the original value
Example flow:
βββββββββββββββββββ tracestate: h:abc123... βββββββββββββββββββ
β Edge Gateway β ββββββββββββββββββββββββββββββΊ β Internal Svc β
β (scribbles) β β (scribbles) β
βββββββββββββββββββ βββββββββββββββββββ
β β
β stores full tracestate β looks up hash
β in memory lookup β restores full
βΌ βΌ
h:abc123... β "vendor1=val1,vendor2=val2,..." gets original tracestate
When to use:
- High-traffic edge proxies where header size impacts performance
- Internal microservices that don't need to see all vendor hops
- Environments where you control both the edge and internal services
Limitations:
- Uses in-memory storage (not shared across process instances)
- Hash lookup only works within the same process that created it
- For distributed deployments, consider using Redis or another shared store
When using the Express middleware, you can include request details in your logs:
scribbles.config({
format: '{method} {path} [{spanId}] {time} <{logLevel}> {message}'
});
app.use(scribbles.middleware.express);
app.get('/users/:id', (req, res) => {
scribbles.log("Fetching user");
// GET /users/123 [090e8e40000005] 2022-06-27T16:24:06.473 <log> Fetching user
});The available request tokens are:
{method}- HTTP method (GET, POST, PUT, DELETE, etc.){url}- Full URL including query string (e.g.,/users/123?sort=name){path}- URL path without query string (e.g.,/users/123){query}- Query parameters as object (e.g.,{ sort: 'name' }){params}- Route parameters as object (e.g.,{ id: '123' })
These tokens are only populated when using scribbles.middleware.express.
Scribbles uses a modular architecture with the main entry point (index.js) serving as a thin orchestration layer that wires together specialized modules in the src/ directory.
| Module | Purpose |
|---|---|
scribble.js |
Core logging function - creates structured log entries |
scribblesConfig.js |
Configuration and log level setup |
trace.js |
Distributed tracing with W3C trace-context support |
middleware.js |
Express middleware for trace context propagation |
namespace.js |
CLS namespace for async trace correlation |
hijacker.js |
HTTP/HTTPS and fetch interception for header injection |
outbound-args.js |
Call-shape and header handling for the traced http / https wrappers |
outbound-fetch.js |
Argument and header handling for the traced global fetch wrapper |
stringify.js / stringifyImpl.js |
Value serialization (pretty-printed; pretty.syntaxHighlight defaults true with colors) |
stringifySyntax.js |
Syntax color palettes and painters for the {value} column |
status.js |
System status collection (CPU, memory, process) |
helpers.js |
Utility functions (deepMerge, getSource) |
args2keys.js |
Argument parsing for log functions |
parceStringVals.js |
Template string parsing with type annotations |
regexUtils.js |
Regex validation and conversion |
loader.js |
Code instrumentation loader |
config.js |
Default configuration values |
getGitStatus.js |
Git repository information retrieval |
checkNodeVer.js |
Node.js version validation |
utils.js |
Trace state parsing utilities |
See src/files.md for detailed module documentation.
small print:
MIT - If you use this module(or part), credit it in the readme of your project and failing to do so constitutes an irritating social faux pas. Besides this, do what you want with this code but don't blame me if it does not work. If you find any problems with this module, open issue on Github. However reading the Source Code is suggested for experience JavaScript and node engineer's and may be unsuitable for overly sensitive persons with low self-esteem or no sense of humour. Unless the word tnetennba has been used in it's correct context somewhere other than in this warning, it does not have any legal or grammatical use and may be ignored. No animals were harmed in the making of this module, although the yorkshire terrier next door is living on borrowed time, let me tell you. Those of you with an overwhelming fear of the unknown will be gratified to learn that there is no hidden message revealed by reading this warning backwards, I think.