Author of uniforms.
Everything is at radekmie.dev.
Core contributor of Meteor.js.
A log is a timestamped text record, either structured (recommended) or unstructured, with optional metadata. [...]
A log is a timestamped text record,
Most programming languages have built-in logging capabilities or well-known, widely used logging libraries.
A log is a timestamped text record,
async function installPayments(c: Config, r: Id) {
logger.info('Install started.');
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info('Install finished.');
}
async function createStripeAccount(c: Config) {
const response = await fetch(...);
if (!response.ok)
throw new Error('StripeError');
const result = await response.json();
return result.id;
}
async function installPayments(c: Config, r: Id) {
logger.info('Install started.');
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info('Install finished.');
}
async function createStripeAccount(c: Config) {
const response = await fetch(...);
if (!response.ok)
throw new Error('StripeError');
const result = await response.json();
return result.id;
}
Given these logs, answer these questions:
async function installPayments(c: Config, r: Id) {
logger.info('Install started.');
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info('Install finished.');
}
async function createStripeAccount(c: Config) {
const response = await fetch(...);
if (!response.ok)
throw new Error('StripeError');
const result = await response.json();
return result.id;
}
Given these logs, answer these questions:
async function installPayments(c: Config, r: Id) {
logger.info(`Install started for ${r}.`);
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info(`Install finished for ${r}.`);
}
async function createStripeAccount(c: Config) {
const response = await fetch(...);
if (!response.ok)
throw new Error(`StripeError${response.status}`);
const result = await response.json();
return result.id;
}
Unstructured!
async function checkIfChargeAlreadyExists(...) {
const existingTransactions =
await transactions.search(spaceId, { ... });
if (existingTransactions.body.length > 0) {
logger.warn(
'Duplicate charge transaction detected.',
{ referenceId, restaurantId, spaceId, token },
);
throw Errors.DUPLICATE_CHARGE_DETECTED;
}
}
Structured!
filter @message like /error/
| parse @message /"restaurantId":"(?<id>[^"]+)"/
| stats count(*) as total by message, id
| sort total desc
Lots of operators, including pattern detection.
async function installPayments(c: Config, r: Id) {
logger.info(`Install started for ${r}.`);
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info(`Install finished for ${r}.`);
}
[2026-06-11 18:25:10] Install started for X.
Which one is which?
[2026-06-11 18:25:11] Install started for X.
[2026-06-11 18:25:19] Install finished for X.
[2026-06-11 18:25:20] Install finished for X.
async function installPayments(c: Config, r: Id) {
logger.info(`Install started for ${r}.`);
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info(`Install finished for ${r}.`);
}
async function installPayments(c: Config, r: Id) {
logger.info(`Install started for ${r}.`);
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info(`Install finished for ${r}.`);
}
async function installPayments(c: Config, r: Id) {
const x = getRandomExecutionId();
logger.info(`Install started for ${r}.`);
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info(`Install finished for ${r}.`);
}
async function installPayments(c: Config, r: Id) {
const x = getRandomExecutionId();
logger.info( `Install started for ${r}.`);
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info( `Install finished for ${r}.`);
}
async function installPayments(c: Config, r: Id) {
const x = getRandomExecutionId();
logger.info(`(${x}) Install started for ${r}.`);
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
logger.info(`(${x}) Install finished for ${r}.`);
}
[2026-06-11 18:25:10] (A) Install started for X.
[2026-06-11 18:25:11] (B) Install started for X.
[2026-06-11 18:25:19] (B) Install finished for X.
[2026-06-11 18:25:20] (A) Install finished for X.
Every "operation" could generate its own ID and add it to all of its logs.
Nothing is stopping us from passing IDs down to child functions, so they could correlate with parent's logs...
...even if they are executed on a different machine. We just need to send the ID.
Congrats, we just invented tracing!
Traces give us the big picture of what happens when a request is made to an application. Whether your application is a monolith with a single database or a sophisticated mesh of services, traces are essential to understanding the full “path” a request takes in your application.
Traces give us the big picture of what happens when a request is made to an application. Whether your application is a monolith with a single database or a sophisticated mesh of services, traces are essential to understanding the full “path” a request takes in your application.
Traces give us the big picture of what happens when a request is made to an application. Whether your application is a monolith with a single database or a sophisticated mesh of services, traces are essential to understanding the full “path” a request takes in your application.
async function installPayments(c: Config, r: Id) {
await trace('installPayments', { r }, async () => {
if (await database.getById(r))
throw new Error('PaymentsAlreadyInstalled');
const account = await createStripeAccount(c);
await database.insert(r, account);
});
}
function createStripeAccount(c: Config) {
return trace('createStripeAccount', async () => {
const response = await fetch(...);
if (!response.ok)
throw new Error('StripeError');
const result = await response.json();
return result.id;
});
}
trace() calls can be nested!
| Provider | Read | Write | Storage |
|---|---|---|---|
| Dynatrace | $0.0035/GB | $0.20/GB | $0.21/GB |
| Grafana Labs | $0.05/GB | $0.40/GB | $0.10/GB |
| Signoz | (included) | $0.40/GB | (included) |
Self hosting is a viable option, too! Just make sure to account for the time spent on maintaining it.