
[![nycrc Coverage](https://img.shields.io/nycrc/thimpat/analogger?preferredThreshold=lines)](https://github.com/thimpat/analogger/blob/main/README.md)
[![npm version](https://badge.fury.io/js/analogger.svg)](https://www.npmjs.com/package/analogger)
<img alt="semantic-release" src="https://img.shields.io/badge/semantic--release-19.0.2-e10079?logo=semantic-release">

---

Analogger is a logger for Node and Browser environments that is highly customizable.
It logs to terminals, browser DOM and inspectors and files.

## Installation

```shell
npm install analogger
```

<br/>

---

## Usage

### CommonJs (in Node)

```javascript
const {anaLogger}  = require("analogger");
```

<br/>

### ESM (in Node)

```javascript
import {anaLogger} from "analogger"
```

<br/>

### As ESM module (In the Browser)

```html
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Demo</title>

 <!-- AnaLogger Theme -->
 <link rel="stylesheet" href="../dist/analogger.min.css">

 <!-- or another AnaLogger Theme 
  <link rel="stylesheet" href="../dist/ana-light.min.css">
 -->
 
</head>
<body>

<div id="analogger" class="analogger">
</div>

<!-- AnaLogger Main -->
<script type="module">
 import {anaLogger} from "../dist/analogger-browser.min.mjs";
 anaLogger.applyPredefinedFormat();
 anaLogger.setOptions({logToDom: "#analogger"});
 anaLogger.log({lid: 100000}, "Test Log example C1");
</script>

</body>
</html>
```

<br/>

### In the Browser via a module bundler

```javascript
// Read your module bundler documentation to load a style file
import "./node_modules/analogger/dist/analogger.min.css"
import {anaLogger} from "./node_modules/analogger/dist/analogger-browser.min.mjs";
```

<br/>

---


### Preview

#### Terminal
![img_1.png](https://github.com/thimpat/analogger/blob/main/docs/images/img_3.png)

#### Inspector
![img.png](https://github.com/thimpat/analogger/blob/main/docs/images/img_2.png)

#### DOM
![img.png](https://github.com/thimpat/analogger/blob/main/docs/images/img_4.png)

#### FileSystem
![img.png](https://github.com/thimpat/analogger/blob/main/docs/images/img_5.png)

#### Remote logging
![Preview](https://github.com/thimpat/demos/blob/main/remote-logging/images/preview-1.png "Remote Logs")

<br/>

---

## Quick start

### Start logging with AnaLogger

```javascript
const {anaLogger}  = require("analogger");
anaLogger.log("something");
```

### Start logging with AnaLogger and override the console

```javascript
const {AnaLogger}  = require("analogger");
// This call will override the console methods
AnaLogger.startLogger();

// Use console method with new formatting
console.log("something");
```

<br/>

### Generate another AnaLogger instance
If you want to have different logger with different purposes, you can create multiple instances.
You may want to have a logger that writes to a file specific information, while another one sends its logs to a remote server for instance.

```javascript
// Get the class first
const {AnaLogger}  = require("analogger");
const analogger = new AnaLogger();
analogger.log("something");
```

<br/>

### Using main instance

```javascript
// Get the main instance directly
const {anaLogger}  = require("analogger");
analogger.log("something");
```

<br/>

### Getting AnaLogger main instance

Now, that you have multiple instances, you may want to retrieve the main instance by calling the getInstance method.

```javascript
const {AnaLogger}  = require("analogger");
const myConsole = AnaLogger.getInstance(0);      // Retrieve first instance => (0)
myConsole.log("something");
```

<br/>



---


## API

<br/>

### log() / info() / warn() / error()

Display a message in the terminal or the inspector, depending on where the process runs.

#### Example 1

```javascript
anaLogger.log(`I'am some log`);
anaLogger.info(`I'am some log`);
anaLogger.warn(`I'am some log`);
anaLogger.error(`I'am some log`);
```
<br/>


#### Example 2

###### Display log in red

```javascript
anaLogger.log({color: "red"}, `I am some log in red`);
```

<br/>

###### Display log in blue on red in bold and underlined 

```javascript
anaLogger.log({color: "blue", bgColor: "red", underline: true, bold: true}, `I am some log in blue on a red 
background in bold and underlined`);
```

<br/>

#### Example 3

###### Change log color and symbol

```javascript
anaLogger.log({color: "#00FFAA", symbol: "🎧"}, `I'am some log in red`);
```

<br/>

#### Example 4

###### Set Log ID

```javascript
anaLogger.log({lid: 1234}, `I'am some log with log id`);
```

<br/>

#### Example 5

###### Set symbol by name

```javascript
anaLogger.log({lid: 1234, symbol: "check"}, `I'am some log with a check symbol`);
anaLogger.log({lid: 1235, symbol: "radioactive"}, `I'am some log with a radioactive symbol`);
anaLogger.log({lid: 1236, symbol: "scissors"}, `I'am some log with some scissors symbol`);
```

💻  ↴
>```shell
> [01:16:11]           : (1234) ✔  "I'am some log with a check symbol"
> [01:16:11]           : (1235) ☢  "I'am some log with a radioactive symbol"
> [01:16:11]           : (1236) ✂  "I'am some log with some scissors symbol"
>```

<br/>

#### Example 6: The "only" filter

###### You can filter logs globally so that only specific identifiers are displayed. This supports exact strings, partial matches, or regular expressions.

```javascript
// Only show logs where the LID or context contains "API"
anaLogger.setOptions({ only: "API" });

anaLogger.log({ lid: "API_123" }, "This matches and will be seen");
anaLogger.log({ lid: "WEB_456" }, "This is blocked and hidden");
```


---

#### Example 7: The "order" option

###### Enforce a call sequence by assigning an `order` number to each log entry. If a log with a lower order value appears after one with a higher value, AnaLogger emits a console warning. Calls without an `order` are transparent and do not affect the sequence.

```javascript
anaLogger.log({lid: "API",     order: 1},  "This matches");
anaLogger.log({lid: "API_123", order: 2},  "This shows API_123");
anaLogger.log({lid: "WEB_456", order: 35}, "This is blocked");
```

If the calls arrive out of sequence — for example, `API_123` (order 2) appearing after `WEB_456` (order 35) — the following warning is printed to the console:

```
! Order mismatch:  [API_123| order: 2] appeared after [WEB_456| order: 35]
```

> **Notes:**
> - The check is per-instance, so different AnaLogger instances track their own sequences independently.
> - Calls filtered out by `only`, targets, or log levels are not counted toward the sequence.
> - Equal order values (non-decreasing) are always allowed; only strictly lower values trigger the warning.

---

#### Example 8: The "maxSeen" option

###### Limit how many times a given lid may be logged. Once the count exceeds `maxSeen`, every subsequent call emits a console warning. The limit and the counter are both tied to the lid string, so different lids track independently.

```javascript
anaLogger.log({lid: "API", maxSeen: 1}, "I'm first");   // OK — seen 1 time
anaLogger.log({lid: "API", maxSeen: 1}, "I'm second");  // Warning — seen 2 times
anaLogger.log({lid: "API", maxSeen: 1}, "I'm third");   // Warning — seen 3 times
```

When the limit is exceeded the following warning is printed to the console:

```
! MaxSeen exceeded: [API| maxSeen: 1] has been seen 2 time(s)
! MaxSeen exceeded: [API| maxSeen: 1] has been seen 3 time(s)
```

> **Notes:**
> - The counter is per-instance and per-lid — different lids each have their own count.
> - Calls filtered out by `only`, targets, or log levels are not counted.
> - `maxSeen` can be combined with `order` in the same context object.

---

#### Example 9: The "test" option and `report()`

###### Attach an inline assertion to any log call. Pass `test: false` (or a function that returns `false`) and AnaLogger records a failure and emits a warning immediately. Call `report()` at any point to print a summary of all pass/fail results.

```javascript
anaLogger.log({lid: "API_1", test: false},      "I'm first");   // fails — boolean
anaLogger.log({lid: "API_2", test: () => false}, "I'm second");  // fails — function
anaLogger.log({lid: "API_3", test: true},        "I'm third");   // passes
anaLogger.log({lid: "API_4", test: () => true},  "I'm fourth");  // passes

anaLogger.report();
```

Failing calls emit an immediate warning:

```
! Test failed: [API_1] I'm first
! Test failed: [API_2] I'm second
```

`report()` then prints a bordered summary. If any test failed the output is **bold red**; if all tests passed it is **bold green**:

```
================== ANALOGGER TEST RESULT ================
  Total  : 4
  Passed : 2
  Failed : 2
==========================================================
```

`report()` also returns the counts as an object, so you can assert on them programmatically:

```javascript
const {total, passed, failed} = anaLogger.report();
```

> **Notes:**
> - `test` accepts a plain boolean or a zero-argument function — both are resolved to a boolean.
> - Every call that carries a `test` key is recorded, whether it passes or fails.
> - Calls without a `test` key are completely transparent and never appear in the report.
> - Results accumulate across the lifetime of the instance. Call `anaLogger._testResults = []` to reset between suites if needed.


---

### listSymbols()

Display the list of supported symbols.

```javascript
Analogger.listSymbols()
```

💻  ↴
>```prettier
> ✈   airplane 
> ⚓   anchor
> ◀   arrow_backward
> ↘   arrow_lower_right
> ↙   arrow_lower_left
>
>... (And more)
>```

---


### alert()

```javascript
anaLogger.alert(`I'am some log`);
```

Display the browser native message box if run from it; otherwise, it displays the message in the terminal.

<br/>

---


### setOptions()


| **Options**           | **default** | **Expect**                        | **Description**                                                                    | 
|-----------------------|-------------|-----------------------------------|------------------------------------------------------------------------------------|
| silent                | false       | boolean                           | _Hide logs from console (not errors)_                                              |
| hideLog               | false       | boolean                           | _Same as above (silent has precedence over hideLog)_                               |              
| hideError             | false       | boolean                           | _Hide errors from console_                                                         |              
| hideHookMessage       | false       | boolean                           | _Hide the automatic message shown when some native console methods are overridden_ |
| hidePassingTests      | false       | boolean                           | _Hide Live test results_                                                           |           
| keepBreadcrumb      | false       | boolean                           | _Show brief history graph_                                                           |
| logToDom              | false       | string (DOM Selector)             | _display log in a DOM container_                                                   |
| logToFile             | false       | string (File path)                | _write log to a file if running from Node_                                         |
| logToRemote           | undefined   | string (URL)                      | _Send log to a remote (more info in the next version)_                             |
| logMaxSize            | 0           | number                            | _Set maximum size for the log file_                                                |
| compressArchives      | false       | boolean                           | _Whether to archive and compress the logs after deleting an archive_               |
| compressionLevel      | 1           | number                            | _Archive compression level (0 to 9 with 0 = no compression)_                       |
| addArchiveTimestamp   | true        | boolean                           | _Whether to add a timestamp to the generated rotated logs_                         |
| logMaxArchives        | 3           | number                            | _Maximum number of log files to use_                                               |
| requiredLogLevel      | "LOG"       | "LOG" / "INFO" / "WARN" / "ERROR" | _Define the log level from which the system can show a log entry_                  |
| enableDate            | false       | boolean                           | _Show date + time (instead of time only)_                                          |
| enableMillisec       | false       | boolean                           | _Show milliseconds in the log time (e.g., 12:22:04,555)_                          |
| logToLocalStorage     | false       | 	boolean                          | _Persist logs in browser localStorage_                                             |
| logToLocalStorageMax	 | 50	         | number                            | _Max entries to keep in localStorage_                                             |
| logToLocalStorageSize	 | 10000	     | number                            | _Max size in bytes for localStorage logs_                                         |
| logToRemoteMaxEntries  | undefined   | number                            | _Number of entries to keep before relaying the data to the remote_                |
| logToRemoteDebounce   | undefined   | number                            | _Time in ms between two posts where the data can be sent to the server_           |
| compressArchives      | 	false	     | boolean                           | _If true, rotates log files into a .tar.gz archive_                                |
| compressionLevel      | 	1	         | number                            | _Gzip compression level (0-9)_                                                     |
| addArchiveTimestamp   | 	true       | boolean                           | 	_Appends a consistent timestamp to rotated files_                                 |
| forceLidOn            | 	false      | boolean                           | 	_Automatically generates a short hash LID if one isn't provided_                  |
| only | undefined | string/Regex/Array | Filter logs to show only those matching specific IDs or patterns |
| resetMaxSeen | 	undefined |	array | Resets maxSeen counters |
| resetOrder | 	undefined |	boolean | Resets the internal tracking for the order sequence |
| order | undefined | number | _Enforce call order — emits a console warning if a log with a lower order value appears after one with a higher value_ |
| maxSeen | undefined | number | _Emits a console warning when the same lid is logged more times than this limit_ |
| test | undefined | boolean / () => boolean | _Records a pass/fail result for the log call; emits a warning immediately on failure. Use_ `report()` _to see the full summary_ |

<br/>

```javascript
// No hook alert message + Log messages in the div #analogger
anaLogger.setOptions({hideHookMessage: true, logToDom: "#analogger"})
```

<br/>

#### Examples

##### Write logs to file only

```javascript
anaLogger.setOptions({silent: true, logToFile: logFilePath});
```

<br/>

##### Write logs to both file and console

```javascript
anaLogger.setOptions({silent: false, logToFile: logFilePath});
```

<br/>
<br/>

---

##### Write logs to a remote server

```javascript
// Use a predefined remote server
anaLogger.setOptions({logToRemote: true});                                           

// Use your remote server (You are responsible for the back-end implementation)
anaLogger.setOptions({logToRemoteUrl: "http://your.server.com/data"});                  

// Batch remote logs (Send when 10 entries are reached OR after 5 seconds)
anaLogger.setOptions({
    logToRemote: true,
    logToRemoteMaxEntries: 10,
    logToRemoteDebounce: 5000
});
```

> Your server must support the POST method.


<br/>

##### Example

_The data received by your server may look like this:_

```json
[
    [{"lid": 123888, "color": "blue"}, "My message 1"], 
    [{"lid": 123999, "color": "#654785"}, "My message 2"]
]
```

> Your server must support the POST method.


<br/>

##### Backend implementation example

To receive remote logs from the frontend, you can implement a simple endpoint in your Node.js/Express-like backend:

```javascript
// Endpoint to receive remote logs from the frontend
router.post("/api/logs", (req, res) => {
    try {
        const logs = req.body;
        if (Array.isArray(logs)) {
            if (logs.length > 0) {
                for (const log of logs) {
                    const context = log[0] || {};
                    const message = log[1];
                    anaLogger.log({
                        lid: "REMOTE00",
                        symbol: "airplane"
                    }, `[${context.lid}][${context.contextName}] ${message}`);
                }
            }
        }
        res.json({ success: true });
    } catch (error) {
        anaLogger.error({ lid: "API10017" }, "Error processing remote logs:", error);
        res.status(status.INTERNAL_SERVER_ERROR).json({ success: false, error: status[status.INTERNAL_SERVER_ERROR] });
    }
});
```

<br/>
<br/>

---

<br/>

### Write logs to the Remote Logging module

You can also use the Remote-Logging module if you don't want to implement the back-end.


> ###### https://www.npmjs.com/package/remote-logging

> ##### Note that Remote Logging is free to use, with no license as there is no use in bundle it in an application.

<br/>

#### 1- Launch a remote from the command line

```shell
$> npx remote-logging

# or if AnaLogger is installed globally
$> analogger
```

> ##### If you're on Windows, the system may ask you permission to reach the port. Select private access.

> ##### On Linux, You will have to open port 12000 by default. To change it, pass the option --port number to the command above.

<br/>

#### 2- Copy the server URL in the AnaLogger options (In your client code)

![Copy URL](https://github.com/thimpat/analogger/blob/main/docs/images/img_7.png "Remote Logs")

```javascript

// Enable log to remote
anaLogger.setOptions({logToRemote: true});                  // <= By default, if only this option is set,
                                                            // logToRemote will be set to "http://localhost:12000/analogger"


// Enter server URLs
anaLogger.setOptions({logToRemoteUrl: "http://192.168.1.20:2000/analogger"});           // Standard message       
anaLogger.setOptions({logToRemoteBinaryUrl: "http://192.168.1.20:2000/uploaded"});      // Screenshots            
```

<br/>


#### 3- That's it


Every call to anaLogger.log will send the log to your server

```javascript
anaLogger.log({lid: 1000}, `Example 1`)
```

```javascript
anaLogger.lid({lid: 1000}, `Example 1`)
```

```shell
# Test the server is listening from the command line
> curl --request POST 'http://localhost:12000/analogger' --header 'Content-Type: application/json' --data-raw '[[{"lid": 123888, "color": "blue"}, "My message 1"], [{"lid": 123999, "color": "blue"}, "My message 2"]]
```

> ![Test from CLI](https://github.com/thimpat/analogger/blob/main/docs/images/img_8.png "Remote Logs")

<br/>

##### Example

_Data received by your server may look like this:_

```json
[
    [{"lid": 123888, "color": "blue"}, "My message 1"], 
    [{"lid": 123999, "color": "#654785"}, "My message 2"]
]
```

<br/>

> ##### 
> ```
> Scroll down to the bottom to see a complete example
> ```
>  


<br/>

---

### attachConsole();

Allows to use of the methods defined in the anaLogger instance directly from the console

```javascript

// Attach methods like keepLogHistory(), hasSeenLid(), etc. to the console
anaLogger.attachConsole();

console.keepLogHistory();

[1, -1, 3, -1, -1].forEach((n) =>
{
    if (n === -1)
    {
      if (!console.hasSeenLid(3000))
      {
         console.log({lid: 3000}, `-1 is not allowed`);          
      }        
    }
})

```

<br/>

---

### overrideConsole()

```javascript
anaLogger.setOptions({silent: true, hideError: false})
console.log(`Log Before override`);
anaLogger.overrideConsole()
console.log(`Log After override`);
```

Override **console.log**, **console.info** and **console.warn**. If you already have many console.log running in your system,
it allows hiding them all in one go.

In this example, the terminal (or inspector) will not show the message "Log After override". All following messages
either.

<br/>

---

### overrideError()

Same as above, but for **console.error**.

<br/>

---


### removeOverride() | removeOverrideError()

Remove overridden console methods.

<br/>

---


### rawLog() | rawInfo() | rawWarn() | rawInfo()

Use native console format after overrides.

```javascript
anaLogger.overrideConsole();
console.log(`Example 1`);              // <= Will use new format
console.rawLog(`Example 2`);           // <= Will use native format

console.log({raw: true}, `Example 2`); // <= Will use native format
```

<br/>

---


### setContext()

#### Context

## Using `setContext` Method

The `setContext` method allows you to register a custom context with your `AnaLogger` instance. 
This context can then be used to provide specific formatting in your log messages.

### Example

```javascript
// Define a context
anaLogger.setContext('EMAIL_WORK', {
    lid: 1234,
    symbol: 'email',
    color: 'orange'
});

// Use the logger with the new context
anaLogger.log({lid: "EM1234", contextName: "EMAIL_WORK"}, 'This is a log message with the custom context.');
anaLogger.log({lid: "EM1235", contextName: "EMAIL_WORK"}, 'This is another log message using the same formatting as the above.');

```
---


### setContexts() 

#### Contexts

Context allow grouping logs by functionality.


##### Examples

```javascript
const LOG_CONTEXTS = {STANDARD: null, TEST: {color: "#B18904"}, C1: null, C2: null, C3: null, DEFAULT: {}}
const DEFAULT_LOG_TARGETS = {ALL: "ALL", DEV1: "TOM", DEV2: "TIM", USER: "USER"};

// LOG_CONTEXTS will be modified by setContexts
anaLogger.setContexts(LOG_CONTEXTS);

anaLogger.log(LOG_CONTEXTS.C1, `Test Log example C1`);
anaLogger.log(LOG_CONTEXTS.C2, `Test Log example C2`);
anaLogger.log(LOG_CONTEXTS.C3, `Test Log example C3`);
```

See LOG_CONTEXTS.C1 in this example to categorize the functionality we want to monitor.
For instance, LOG_CONTEXTS.INVESTIGATING_TIMER_EFFECT could display output related to something that has to
do with a timer.

The "Testing log 2" log will not appear in the console or the terminal.

**Preview In a terminal (NodeJs)**

![img.png](https://github.com/thimpat/analogger/blob/main/docs/images/img.png)

**Preview In a browser (ESM)**

![img_1.png](https://github.com/thimpat/analogger/blob/main/docs/images/img_1.png)


<br/>

---


### setDefaultContext()


You can apply a default settings for every output with setDefaultContext.
**All contexts inherit from the default context.**

##### Examples

```javascript

anaLogger.setDefaultContext({color: "gray", symbol: "check", contextName: "LOG"});

anaLogger.log({lid: 100000}, `Test Log example C1`);        // <- Use default (grey color and the check symbol)
anaLogger.log({lid: 100010}, `Test Log example C2`);        // <- Use default
anaLogger.log({lid: 100020}, `Test Log example C3`);        // <- Use default

anaLogger.log({contextName: "LOG", lid: 100030, symbol: "cross"}, "Test Log example C4");
anaLogger.log({contextName: "INFO", lid: 100040, symbol: "no_entry"}, "Test Log example C4");
anaLogger.log({contextName: "WARN", lid: 100050, symbol: "raised_hand"}, "Test Log example C4");

```

💻  ↴
> ```
> [04:32:38]        LOG: (100000) ✔  "Test Log example C1"
> [04:32:38]        LOG: (100010) ✔  "Test Log example C2"
> [04:32:38]        LOG: (100020) ✔  "Test Log example C3"
> [04:32:38]        LOG: (100030) ❌  "Test Log example C4"
> [04:32:38]       INFO: (100040) ⛔  "Test Log example C5"
> [04:32:38]       WARN: (100050) ✋  "Test Log example C6"
> ```


<br/>

#### Callbacks

You can also set some callbacks to be called when a log is about to be displayed. 

```javascript
anaLogger.setDefaultContext({
    /**
     * Allow to modify the context before it is used
     * @param context
     */
    onContext: function (context) {
        context.lid = "MOD12345";
    },
    /**
     * Allow to modify the raw message before it is displayed
     * @param rawMessage
     * @param extras
     * @returns {string}
     */
    onMessage: function (rawMessage, extras) {
        return rawMessage + " ↘";
    },
    /**
     * Allow to modify the formatted message before it is displayed
     * @param formattedMessage
     * @param logCounter
     * @returns {`${string}: ${string}`}
     */
    onOutput: function (formattedMessage, {logCounter}) {

        return `${logCounter}: ${formattedMessage}`;
    }
});

```

---


### setTargets() / setActiveTarget()

#### Targets

Targets allow filtering logs by targets. 
For example, they can be developers, testers, roles, etc.
and setActiveTarget() will ignore logs for non-targeted users.

##### Examples

```javascript

// "ALL" & "USER" are predefined targets
const LOG_TARGETS = ["GROUP1", "GROUP2", "TOM", "TIM"/*, "ALL", "USER"*/];

// Contexts define how the log should be seen 
const LOG_CONTEXTS = {STANDARD: null, TEST: {color: "#B18904", symbol: "⏰"}, C1: null, C2: null, C3: null, DEFAULT: {}}

// Important. Set the default behaviour.
// By default, no log will be displayed when the target is missing from `anaLogger.log`
anaLogger.setDefaultContext({
    target: DEFAULT_LOG_TARGETS.NONE // or "NONE" (value can be "ALL", "NONE", "USER")
})

// LOG_CONTEXTS will be modified by setContexts
anaLogger.setContexts(LOG_CONTEXTS);

// Allowed targets = "ALL", "TOM", "TIM", "USER"
anaLogger.setTargets("GROUP1", "GROUP2", "TOM", "TIM"/*, "ALL", "USER"*/); 

// Assign an active target
anaLogger.setActiveTarget("TOM");                          // <- You are "TOM"

// Seen because you're TOM
anaLogger.log({target: "TOM"}, `Testing log 1`);           // You will see this

// Not seen (only for TIM)
anaLogger.log({target: "MATT"}, `Testing log 2`);           // You will not see this

// Here we set the allowed targets
anaLogger.setTargets({TOM: "TOM", GROUP1: "GROUP1", GROUP2: "GROUP2"});
anaLogger.setActiveTargets(["TOM", "GROUP1"]);
anaLogger.log({target: "TOM"}, `You will see this`);
anaLogger.log({target: "TIM"}, `You will not see this`);
anaLogger.log({target: "GROUP1"}, `You will see this`);

// By default, the system will not display logs for non-targeted users (@see setDefaultContext above)
anaLogger.setActiveTarget("NonDefinedTarget");
anaLogger.log({lid: "WEB35388"}, `You will not see this`);
anaLogger.log({lid: "WEB35388", target: "NonDefinedTarget"}, `You will not see this`);

// Clear the active target, so we can see everything
anaLogger.setActiveTarget(null);
anaLogger.log({lid: "WEB35388"}, `You will see this`);

// No target defined the active target will see this
anaLogger.log({context: null}, `Testing log 6`);           // You will see this    
anaLogger.log(`Testing log 4`);                            // You will see this


```

To assign the active target, you could use IPs, read a file, read an environment variable, etc. 
It is all up to your implementation.

Examples:

###### IP Based

```javascript
anaLogger.setTargets({DEV1: "192.168.12.45", DEV: "192.168.12.46"});
// or anaLogger.setTargets("192.168.12.45", "192.168.12.46")
anaLogger.setActiveTarget(require('ip').address());   
```
<br/>

###### File based

```javascript
// Example 2: File  
anaLogger.setTargets({DEV1: "fg890234ru20u93r2303092pkid0293"});
// or anaLogger.setTargets("fg890234ru20u93r2303092pkid0293")
anaLogger.setActiveTarget(require('./something.json').key);
```

<br/>

###### Fetch
```javascript
// Example 3: Fetch
anaLogger.setTargets({DEV1: "fg890234ru20u93r2303092pkid0293"});
const key = (await (await fetch('/private-api/me')).json()).key
anaLogger.setActiveTarget(key);
```

<br/>

###### Environment system based

```javascript
// Example 4: Environment variables
anaLogger.setActiveTarget(process.env.DEVELOPER);     // <= Assuming it has been set on the system host
```

<br/>

> Note that two targets cannot be overridden: {ALL: "ALL", USER: "USER"}.
> The system always adds them to the allowed list, so they will still be set even if a call to setTargets() is empty.

```javascript
// Two implicit targets "ALL" and "USER"  
analogger.setTargets()
```

<br/>

---

### loadLids()

#### Context

## Using `loadLids` for Predefined Log Messages

The `loadLids` method allows you to register a collection of predefined log message templates, identified by unique Log IDs (Lids). This promotes consistency and simplifies logging by allowing you to reference these templates instead of writing out full messages repeatedly.

### Example

With `loadLids`, you can define a set of log messages that can be reused throughout your application.
Instead of scattered, repetitive messages, Log IDs (lid) offer clear, centralized definitions.

```javascript
// lids.js
const LIDS = {
    API35390: {
        message: "API logging initialized",
        contextName: "TEST",
        color      : "green",
        symbol     : "check"
    },
    API35391: {
        message: "Error API logging initialized",
    },
    API65341: {
        message: "The username doesn't match the userID: {{username1}} !== {{username2}}"
    },
    AUTH1001: {
        message: "Authentication failed for user: {{username}}",
        color: "red",
        symbol: "alert"
    },
    DB205: {
        message: "Database query took {{duration}}ms"
    }
};

module.exports = LIDS;
```


```javascript
// main.js
const {anaLogger} = require("analogger");
const ligs = require("./lids.js");
anaLogger.loadLids(LIDS);

anaLogger.log({lid: "API35390", color: "green"}, "API logging about to be initialized");
// => DEFAULT: (API35390) ✔  API logging about to be initialized

anaLogger.log(LIDS.API35390);
// =>  TEST: (API35390) ✔  API logging initialized

anaLogger.error(LIDS.API35391);
// => ERROR: (API35391) ❌  Error API logging initialized

anaLogger.log(LIDS.API65341, {username1: "test", username2: "test2"});
// => DEFAULT: (API65341) ✔  The username doesn't match the userID: test !== test2

anaLogger.log(LIDS.API65341, "Some other messages");
// => DEFAULT: (API65341) ✔  The username doesn't match the userID: {{username1}} !== {{username2}}•Some other messages
```

<br/>

---
### getLids

Returns all loaded lids

```javascript
const lids = anaLogger.getLids();

{
    API35390: {
        message: 'API logging initialized',
                contextName: 'TEST',
                color: 'green',
                symbol: 'check',
                lid: 'API35390',
                callCount: 2,
                callTimes: [ 1745412629482, 1745412629504 ],
                target: [ 'ALL', 'USER', 'NONE' ],
                dates: [ '2025-04-23 13:50:29.482', '2025-04-23 13:50:29.504' ]
    },
    API35391: {
        message: 'Error API logging initialized',
                lid: 'API35391',
                callCount: 1,
                callTimes: [ 1745412629507 ],
                target: [ 'ALL', 'USER', 'NONE' ],
                dates: [ '2025-04-23 13:50:29.507' ]
    },
    API65341: {
        message: "The username doesn't match the userID: {{username1}} !== {{username2}}",
                lid: 'API65341',
                callCount: 2,
                callTimes: [ 1745412629509, 1745412629510 ],
                target: [ 'ALL', 'USER', 'NONE' ],
                dates: [ '2025-04-23 13:50:29.509', '2025-04-23 13:50:29.510' ]
    },
    WEB35382: {
        message: 'Some log message',
                lid: 'WEB35382',
                callCount: 5,
                callTimes: [
            1745412629511,
            1745412629512,
            1745412629514,
            1745412629515,
            1745412629517
        ],
                dates: [
            '2025-04-23 13:50:29.511',
            '2025-04-23 13:50:29.512',
            '2025-04-23 13:50:29.514',
            '2025-04-23 13:50:29.515',
            '2025-04-23 13:50:29.517'
        ]
    }
}

```



<br/>

---

### forceLid(true);

Force the system to generate the lid even if it is not defined

```javascript
anaLogger.forceLid(true);
```

<br/>

---

### forceResolveErrorLineCall(true);

Add the stack trace to the error message context

```javascript
anaLogger.forceResolveErrorLineCall(true);
```

<br/>

---

### forceResolveLineCall(true);

Add the stack trace to the log message context

```javascript
anaLogger.forceResolveLineCall(true);
```

<br/>

---


### assert()

You can set some tests directly in the code. It serves as early feedback.
It is helpful to guarantee that the code runs straight away rather than waiting for the CI to send its feedback.


```javascript
anaLogger.assert(1 === 1)
anaLogger.assert(1 === 2)
anaLogger.assert(()=>true, true)
anaLogger.assert((a, b)=> a === b, true, 2, 2)
```

<br/>

---


### setErrorHandlerForUserTarget()

It tells whether a log has already been displayed. keepLogHistory must be activated

```javascript
anaLogger.keepLogHistory()

anaLogger.log({lid: 1234}, `My name is log`)
anaLogger.hasSeenLid(1234)          // true
anaLogger.hasSeenLid(1000)          // false

// Optional
anaLogger.releaseLogHistory()
```
<br/>
---

### isBrowser()()

It tells whether the console runs from the browser

```javascript
anaLogger.isBrowser()
```
<br/>

---
### keepLogHistory()

Keeps log entries in memory

```javascript
anaLogger.keepLogHistory()
```
<br/>
---

### releaseLogHistory()

It tells the system to no longer keep log entries in memory

```javascript
anaLogger.releaseLogHistory()
```
<br/>
---

### resetLogHistory()

Delete memorized log entries

```javascript
anaLogger.resetLogHistory()
```

<br/>
---

### getLogHistory()

Returns log entries

```javascript
anaLogger.getLogHistory()
```

<br/>
---


### setErrorHandlerForUserTarget()

When an error is detected and should be seen by your app consumers explicitly (for instance, you want to display a 
dialogue box
to them), you can set a handler here. All other console.error will be working as usual (logging messages). 

```javascript
    anaLogger.setErrorHandlerForUserTarget(function (context/*, ...args*/)
    {
        // Detect whether we are in a browser
        if (context.environment === anaLogger.ENVIRONMENT_TYPE.BROWSER)
        {
            // When the Browser detects an error, users will see this message
            anaLogger.alert(`Users explicitly see this message`)
        }
    });

    anaLogger.setActiveTarget(LOG_TARGETS.USER);
    anaLogger.error({target: LOG_TARGETS.USER}, "Salut user!");     // Display an alert box
    anaLogger.error("Hi user!");                                    // Log the message to the inspector
```

<br/>

---

## Multiple instances

It is possible to generate multiple AnaLogger instances so that they can point to different targets. 

### Example

```javascript
const AnaLogger = require("analogger");                 // or import AnaLogger from "analogger"
const anaLoggerInstance1 = new AnaLogger();
const anaLoggerInstance2 = new AnaLogger();

anaLoggerInstance1.setOptions({logToDom: ".analogger"});
anaLoggerInstance2.setOptions({logToFile: "./logme.log"});
```


<br/>

---

## Automated Tests

When combined with Remote-logger, you can use AnaLogger as a Mocha reporter.

See: https://www.npmjs.com/package/remote-logging

<br/>

![Preview](https://github.com/thimpat/demos/blob/main/remote-logging/images/rl-report.gif "Mocha Reporter")


<br/>

---

## Take a screenshot

### takeScreenshot()

You can take a screenshot via the "html-to-image" plugin (integrated in the module).

> **"html-to-image"** is an external npm package this module uses to take screenshots.

<br/>

1. **Run a logging server from the command line**

```shell
$> analogger --port 8754
```
<br/>

2. **Report the address displayed in the console into your client code**

![Browser to Remote Logging view](https://github.com/thimpat/demos/blob/main/remote-logging/images/rl-setup-1.png)

<br/>

3. **Set up the client (Browser in our case)**

HTML (Client-Side)

<br/>

4. **Link AnaLogger to the remote**

<br/>

```javascript
// Load the AnaLogger library ( Available in ./node_modules/analogger/browser/ )
import {anaLogger} from "./browser/ana-logger.mjs";

anaLogger.setOptions({logToRemote: "http://localhost:8754/analogger", loadHtmlToImage: true});
anaLogger.takeScreenshot({selector: "body"});
```

<br/>

---

## Plugins

### Implement a plugin

```javascript
// Load anaLogger
import {anaLogger}  from "./ana-logger.mjs";

// Implement
const doSomething = ({node = document.body, container = document.body} = {}) =>
{
    console.log(`Hello plugin`)
};

// Register the plugin
anaLogger.addPlugin("doSomething", doSomething);



```

---

[![Test workflow](https://github.com/thimpat/analogger/actions/workflows/test.yml/badge.svg)](https://github.com/thimpat/analogger/actions/workflows/test.yml)
[![nycrc Coverage](https://img.shields.io/nycrc/thimpat/analogger?preferredThreshold=lines)](https://github.com/thimpat/analogger/blob/main/README.md)
[![npm version](https://badge.fury.io/js/analogger.svg)](https://www.npmjs.com/package/analogger)

Analogger is a highly customizable logger for Node and Browser environments. It logs to terminals, browser DOM, files, and remote servers.

---

## 🚀 New Features (v1.24+)

### 💾 Local Storage Persistence (Browser)
You can now persist logs in the browser's Local Storage. This is useful for debugging issues that occur across page refreshes.

```javascript
// Enable local storage logging
anaLogger.setOptions({ 
    logToLocalStorage: true, 
    logToLocalStorageMax: 100, // Keep last 100 logs
    logToLocalStorageSize: 50000 // Limit to 50KB
});

// Restore logs from previous session after page reload
anaLogger.restoreLogs();
```

🗄️ Advanced Log Rotation & Archiving (Node)
Manage large log files with automatic rotation and .tar.gz compression.

```javascript
anaLogger.setOptions({
    logToFile: "./app.log",
    logMaxSize: 5 * 1024 * 1024, // 5MB
    logMaxArchives: 5,
    compressArchives: true,      // Archive rotated logs into .tar.gz
    compressionLevel: 6,         // Gzip level (1-9)
    addArchiveTimestamp: true    // Add ISO timestamps to filenames
});
```

### New Helper Methods

> restoreLogs()

(Browser only) Reads the history from localStorage and re-prints the logs to the current console. Perfect for diagnosing crashes that trigger a page reload.

> getLids()

Returns a deep clone of all loaded Lids, including statistics like callCount, callTimes, and human-readable dates.

> forceResolveLineCall(bool)

When enabled, AnaLogger captures the exact file and line number where the log was called and attaches it to the context.

### Manual LID strings

If you don't want to pass objects, you can use a shorthand string format:

```javascript
// AnaLogger will parse this string as a context object
anaLogger.log("lid: USR_LOGIN, color: purple", "User logged in");
```

---

## Package

```
📁 package                
│
└───📁 src
│   │
│   └─ 📝 ana-logger.cjs                         ⇽ AnaLogger Node version for CommonJs
│   
└───📁 esm
│   │
│   └─ 📝 ana-logger.mjs                         ⇽ AnaLogger Node version for ES Modules
│   
└───📁 browser (ESM)
│   │─ 📝 ana-logger.mjs                         ⇽ AnaLogger browser version
│ 
└───📁 dist (minified)
│   │─ 📝 analogger.min.css                      ⇽ Default Theme file
│   │─ 📝 ana-light.min.css                      ⇽ Another Theme file
│   │─ 📝 analogger-browser.min.mjs              ⇽ AnaLogger browser version

```

<br/>

---


---

## Snapshots

Snapshots let you capture the set of log IDs (lids) seen at a specific point in time, persist them, and later compare two captures side by side. This is useful for regression checks — verifying that the same lids fire in two different runs, or that a code change does not add or remove unexpected log calls.

Snapshots work in both **Node.js** (persisted as JSON files under `os.tmpdir()/analogger-snapshots/`) and the **browser** (persisted in `localStorage`).

> `keepLogHistory()` must be called before any logging starts, as snapshots read from the in-memory log history.

---

### startSnapshotProcess()

Marks the start of a snapshot window. Any subsequent `takeSnapshot()` call will only capture lids logged **from this point forward**, ignoring everything already in the history. Calling it again moves the window start to the current position.

```javascript
anaLogger.keepLogHistory();

anaLogger.log({ lid: "BOOT_001" }, "application boot");  // excluded
anaLogger.log({ lid: "BOOT_002" }, "config loaded");     // excluded

anaLogger.startSnapshotProcess();

anaLogger.log({ lid: "WEB_001" }, "request received");   // included
anaLogger.log({ lid: "WEB_002" }, "response sent");      // included

anaLogger.takeSnapshot("SNAP01");   // captures only WEB_001 and WEB_002
```

If `startSnapshotProcess()` is never called, `takeSnapshot()` captures the entire history.

<br/>

---

### takeSnapshot()

Captures all lids logged since the last `startSnapshotProcess()` (or since session start) and persists the snapshot under a unique ID.

Returns the array of captured entries so you can inspect or assert on them programmatically.

```javascript
takeSnapshot(snapshotID, options?)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `snapshotID` | `string` | — | Unique label for this snapshot, e.g. `"SNAP01"` |
| `options.messages` | `boolean` | `true` | Store the log message alongside each lid |
| `options.context` | `boolean` | `true` | Store the full context object alongside each lid |

```javascript
anaLogger.keepLogHistory();
anaLogger.log({ lid: "API_001" }, "initialised");
anaLogger.log({ lid: "WEB_002" }, "request in");

// Capture lids + messages + context (defaults)
const entries = anaLogger.takeSnapshot("SNAP01");
// entries = [{ lid: "API_001", message: "initialised", context: {...} }, ...]

// Capture lids only (lighter footprint)
anaLogger.takeSnapshot("SNAP02", { messages: false, context: false });
```

In Node.js the snapshot is written to a file and its path is logged:

```
[AnaLogger] Snapshot "SNAP01" saved - 2 lid(s) captured.
[AnaLogger] Snapshot persisted to: /tmp/analogger-snapshots/analogger_snapshot_default_SNAP01.json
```

Because the file lives in `os.tmpdir()`, snapshots persist across Node process restarts — you can take `SNAP01` in one run and compare it in a later one.

<br/>

---

### compareSnapshots()

Loads two previously saved snapshots and prints a side-by-side diff table using `console.table`, which gives automatic column alignment in both Node.js terminals and browser devtools.

```javascript
compareSnapshots(snapshotID1, snapshotID2, displayOpts?)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `snapshotID1` | `string` | — | ID of the first snapshot |
| `snapshotID2` | `string` | — | ID of the second snapshot |
| `displayOpts[0].messages` | `boolean` | `false` | Show a message column for snapshot 1 |
| `displayOpts[1].messages` | `boolean` | `false` | Show a message column for snapshot 2 |

Returns `{ onlyInSnap1, onlyInSnap2, inBoth }` for programmatic use, or `null` if either snapshot is not found.

#### Basic comparison (lids only)

```javascript
anaLogger.compareSnapshots("SNAP01", "SNAP02");
```

```
Snapshot comparison: "SNAP01"  vs  "SNAP02"
  SNAP01 captured at 09:04:28 - 3 lid(s)
  SNAP02 captured at 09:04:31 - 4 lid(s)

┌─────────┬─────────┬─────────┐
│ (index) │  SNAP01 │  SNAP02 │
├─────────┼─────────┼─────────┤
│    0    │ API_001 │ API_001 │
│    1    │ WEB_002 │ WEB_002 │
│    2    │ DB_003  │         │
│    3    │         │ WEB_004 │
└─────────┴─────────┴─────────┘
```

#### With message columns

```javascript
anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
```

```
┌─────────┬─────────┬──────────────────────┬─────────┬──────────────────────┐
│ (index) │  SNAP01 │   SNAP01 message     │  SNAP02 │   SNAP02 message     │
├─────────┼─────────┼──────────────────────┼─────────┼──────────────────────┤
│    0    │ API_001 │ 'initialised'        │ API_001 │ 'initialised'        │
│    1    │ WEB_002 │ 'request in'         │         │                      │
│    2    │         │                      │ WEB_004 │ 'new endpoint added' │
└─────────┴─────────┴──────────────────────┴─────────┴──────────────────────┘
```

You can also enable messages for only one side:

```javascript
// Messages only for SNAP02
anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: false }, { messages: true }]);
```

In the browser, `compareSnapshots` also injects a colour-coded HTML widget into the `#analogger` DOM container (or `document.body` as a fallback): lids present in both snapshots are shown in neutral, snap1-only rows are highlighted in red, and snap2-only rows in green.

#### Using the return value

```javascript
const { onlyInSnap1, onlyInSnap2, inBoth } = anaLogger.compareSnapshots("SNAP01", "SNAP02");

if (onlyInSnap2.length > 0) {
    console.warn("New lids appeared in SNAP02:", onlyInSnap2);
}
```

#### Full workflow example

```javascript
const { anaLogger } = require("analogger");

anaLogger.keepLogHistory();

// --- First run (or first phase) ---
anaLogger.startSnapshotProcess();
anaLogger.log({ lid: "API_001" }, "initialised");
anaLogger.log({ lid: "WEB_002" }, "request received");
anaLogger.log({ lid: "DB_003"  }, "query executed");
anaLogger.takeSnapshot("SNAP01");

// --- Second run (or second phase, e.g. after a code change) ---
anaLogger.startSnapshotProcess();
anaLogger.log({ lid: "API_001" }, "initialised");
anaLogger.log({ lid: "WEB_002" }, "request received");
// DB_003 is gone, WEB_004 is new
anaLogger.log({ lid: "WEB_004" }, "cache hit");
anaLogger.takeSnapshot("SNAP02");

// --- Compare ---
const diff = anaLogger.compareSnapshots("SNAP01", "SNAP02", [{ messages: true }, { messages: true }]);
// diff.onlyInSnap1 => ["DB_003"]   (removed)
// diff.onlyInSnap2 => ["WEB_004"]  (added)
// diff.inBoth      => ["API_001", "WEB_002"]
```

<br/>

---

## Changelog

##### current:
*  Add an option to show the date and time in logs
*  Make the alert method display the lid when detected
*  Keep format when displaying multiple lines for one entry
*  Add `order` option to detect and warn on out-of-sequence log calls
*  Add `maxSeen` option to warn when a lid is logged more times than allowed
*  Add `test` context option and `report()` method for inline assertions and test summaries
*  Add `startSnapshotProcess()` to mark the start of a snapshot window
*  Add `takeSnapshot(id, opts?)` to capture lids at a point in time; persists to `localStorage` (browser) or `os.tmpdir()` (Node); returns captured entries
*  Add `compareSnapshots(id1, id2, displayOpts?)` to diff two snapshots side by side using `console.table`; optional message columns via `[{ messages: true }, { messages: true }]`


##### 1.23.2:
*  Remove the message 'No AnaLogger instance found'


##### 1.23.0:
*  Add startLogger method to automatically override the console


##### 1.22.1:
*  Fix screenshot endpoint
*  Fix remote breaking in browser due to misconfiguration


##### 1.22.0:
*  Review display for js primitive types
*  Use native console table
*  Set up AnaLogger as a Mocha reporter
*  Launch a remote from AnaLogger
*  Fix remote-related options passed to the setOptions method
*  Launch a remote server from the CLI


##### 1.21.4:
*  Default the remote to localhost when enabled

##### 1.21.0:
*  Restore the same behaviour as before for getLogHistory
*  Add functionalities to AnaLogger instances


##### 1.20.4:
*  Fix log level applied one level down
*  Update themes


##### 1.20.3:
*  Apply empty log id by default for contexts
*  Apply missing symbols


##### 1.20.2:
*  Add missing files from the build
*  Recolor title bar
*  Add a theme file
*  Add a header and o footer to the layout
*  Use native console table on browsers
*  Apply better environment detection
*  Review logToRemote option


##### 1.20.1:
*  Review sentence failure for screenshot
*  Fix plugin validation
*  Export and validate plugin names


##### 1.20.0:
*  Fix default address for remote binary
*  Fix error on a missing callback
*  Fix log displayed twice
*  Add takeScreenshot plugin

---
