# Crowdin Logs Formatter

Patches the standard console methods to emit JSON records the ELK stack understands, attributes
every record to the tenant that produced it, reports critical levels to Sentry, and forwards
browser-side logs of Crowdin app UI modules to the server.

[![coverage report](https://gitlab.crowdbox.in/crowdin-apps/modules/crowdin-logs-formatter/badges/master/coverage.svg)](https://gitlab.crowdbox.in/crowdin-apps/modules/crowdin-logs-formatter/-/commits/master)

Release notes and the 2.x → 3.0.0 migration guide: [CHANGELOG.md](./CHANGELOG.md).

## Setup

```javascript
const logsFormatter = require('@crowdin/logs-formatter');
logsFormatter.setup({ appIdentifier: 'my-app' }); // exactly once, at startup

const app = express();
app.use(logsFormatter.contextResolverMiddleware()); // must come before expressMiddleware()
app.use(express.json({ limit: '50mb' }));
app.use(logsFormatter.expressMiddleware());
```

For fastify:

```javascript
fastify.addHook('onRequest', logsFormatter.contextResolverHook());
logsFormatter.applyFastifyRoutes(fastify);
```

`appIdentifier` is the app's identity — it becomes the `app_identifier` tag on every Sentry event
the process reports, so even a crash outside any request says which app crashed. It is not a field
of the stdout/ELK records.

### Console methods modified by `setup()`

```javascript
console.log();
console.debug();
console.info();
console.warn();
console.trace();
console.error();
console.fatal();
```

## Context

A context exists only inside a scope, and a scope belongs to one unit of work — one request, one
job, one message. Concurrent units cannot see each other's context, including in log calls that
resume after an `await`, and nothing is left behind to mislabel a later log line.

For Express requests `contextResolverMiddleware()` opens the scope, for fastify the
`contextResolverHook()` does — both fill it from the request's token (the `?jwtToken` query
parameter or an `Authorization: Bearer` header):

```javascript
app.get('/work', async (req, res) => {
  await somethingSlow();
  console.log('still this request'); // stamped with this request's project/user/organization
});
```

For anything else — cron jobs, queue consumers, workers, scripts — open the scope yourself with
`runWithContext(context, fn)`. Everything logged inside `fn`, at any depth and after any `await`,
is stamped with that context:

```javascript
await logsFormatter.runWithContext(
  { project: { id: 1, identifier: 'newproject', organization_id: 200000001, user_id: 12 } },
  async () => {
    await syncProject();
    console.error('sync failed'); // stamped with the project above
  },
);
```

Inside a scope, add to the context with `setContext({ ... })` and read it with `getContext()` (a
read-only snapshot) or the `context` export. Outside any scope there is nowhere for a context to
live: writes are ignored with a stderr warning naming the call site, and records simply carry no
context fields. The exported `Context` type describes the accepted shape — worth using, because
`setContext()` silently ignores a wrong one.

## Errors

`console.error(err)` is enough. `message` keeps `Class: text`, the frames go to `extra.backtrace`, and
whatever the thrower attached (`code`, `apiError`, …) goes to `extra.attributes`. An axios error is
the exception. It keeps its bare `Request failed with status code 422`, with the class left on the
first backtrace line. `error.cause` follows as `caused by:` lines, and several errors in one call get
one record each. In the browser the injected script reports `console.*`, page errors and unhandled
rejections. There too an error's text, stack and own properties travel as separate fields, so they
land in the same `message` / `extra.backtrace` / `extra.attributes` split a server-side error gets.

```javascript
console.error(err); // message: "CrowdinError: Language 'null' not found"
```

## Sanitizers

The formatter masks nothing on its own. It decides what a record is made of, not what may appear in
it. Masking is a sanitizer's job, and whoever uses the package registers the sanitizer.

```javascript
logsFormatter.registerSanitizer((entry) => ({ ...entry, record: mask(entry.record) }));
```

An app built on `@crowdin/app-project-module` gets `crowdinLogSanitizer` registered for it inside
`configure()`. That one masks by key (`authorization`, `cookie`, `set-cookie`, `password`, `api_key`
and the like) and by the shape of a value, catching a JWT, a `Bearer` token, or credentials inside a
URL. Using this package on its own means registering a sanitizer of your own. Without one, nothing is
masked.

Write a sanitizer as a pure function. It runs once per record, not once per log call, so a call that
carries two errors runs it twice, and the two calls share one `rawParams` array. A counter, a metric
or an outbound request inside a sanitizer therefore fires as many times as the call produced records,
and mutating what the sanitizer was handed applies that change on top of the previous one.

Sanitizers receive `extra.attributes` as an object and the formatter serialises the record only after
they run, so a key-based rule reaches it. This matters most for an error carried inside another one
through `originalError` or `cause`. The formatter expands that inner error in full, its HTTP config
and response headers included, and the sanitizer is what decides how much of it reaches the log.

## ELK attributes

While ELK supports a variety of attributes, note that not all of them are included in the current version of this formatter. Some examples of these attributes are:

```
"user.id", "user.login", "project.id", "project.identifier", "organization.id", "organization.domain", "client.ip" 
"user_agent.original", "http.request.referrer", "url.original", "extra.attributes", "extra.backtrace"
```
