# Developer Guidelines

These guidelines standardize how to write application logs across our backend services. Application logs should complement our infrastructure logs and APM to support troubleshooting in production. Our applications already emit standardized startup logs, as well as logs for inbound and outbound requests. As a developer, focus on logging actionable insights with a consistent structure and minimal noise.

{% hint style="info" %}
These guidelines apply to [island.is](http://island.is) application logs, usually logged through a Winston logger object. [Audit logs](https://github.com/island-is/island.is/tree/main/libs/nest/audit) have different goals and principles.
{% endhint %}

## **Principles**

* Log with purpose: every log line should help diagnose, alert, or audit.
* Prefer observability over verbosity: use metrics/traces for high-volume signals, logs for sparse, contextual events.
* Protect users: never log secrets, sensitive PII, or regulated data.
* Make correlation easy: ensure logs can be tied to traces, requests, and users without exposing sensitive data.

### What to log?

Log sparse, high-signal events that help you reconstruct what happened, when, and why—without duplicating what existing logs / traces already cover.

* Domain milestones and state changes.
* Background work.
* External dependencies and resilience.
* Unexpected but handled conditions.
* Context, ownership and stable non-sensitive identifiers for correlation.

Rule of thumb: log milestones and state transitions, not every step. If it’s actionable for on-call or essential to reconstruct a timeline—and not already covered by standard request/startup logs—log it.

### What not to log?

* Secrets: passwords, tokens, API keys, client secrets, private keys, session IDs.
* Personal data: kennitala, addresses, emails, phone numbers, full names, date of birth, financial details, health data.
* Full request/response bodies, especially for auth or external providers.
* Large objects/binaries, stack traces on known/handled errors, repeated identical messages in loops.
* Anything already logged by standard handlers (avoid duplicates).

## How to log?

### Use the appropriate log level

#### **Debug**

Use for developer diagnostics and detailed control flow. Typically visible only in local environments, but may be selectively enabled in production for short periods.

Example:

```tsx
logger.debug('Refund: Starting', { refundId })
logger.debug('Refund: Fetching payment information', { refundId })
logger.debug('Refund: Calling payment gateway', { refundId })
logger.debug('Refund: Finished', { refundId })
```

#### Info

Use for lifecycle events, domain events and meaningful state changes in production (e.g., service lifecycle, migrations, record changes, job processing, circuit closed, retry).

Be sure to include stable identifiers (non-sensitive) for correlation.

Example:

```tsx
logger.info(`Submitted application to ${organisation}`, { applicationId })
logger.info(`Finished processing attachments`, { jobId, applicationId })
logger.info(`Download failed, retry ${attempt} of 3`, { fileId, attempt })
logger.info(`Circuit breaker ${circuitName} closed`, { circuitName })
logger.info(`Could not update all licenses, returning from cache`, { error })
```

#### Warning

You should use warnings for unusual but handled conditions that may need attention (fallbacks, unexpected input, degraded mode, circuit open).

Example:

```tsx
logger.warn(`Error fetching ${organisation} licenses, skipping.`, { error, organisation })
logger.warn(`Validation failed`, { fieldErrors })
logger.warn(`Circuit breaker ${circuitName} open`, { circuitName })
```

#### Error

Log unexpected failures that require action or indicate deviating behavior.

In most cases, don’t log errors directly—let them bubble up to the standard error handler. The key exception is when you must catch all errors to apply fallback logic; in that case, log the unexpected errors at this level.

See more about handling and logging errors below.

Example:

```tsx
logger.error('Job failed', { error })
```

### **Group logs with context and ownership**

Use two stable fields on every log to make filtering and ownership clear:

* `context`: Identifies the producing component (e.g., SomeService, OrdersController, PaymentClient). Create a child logger per component so every log from it carries the same context.
* `codeOwner`: Identifies the development team responsible for the service, endpoint or resource (eg application template).

How to set `codeOwner`:

* Service-wide (infra): `app.codeOwner(CodeOwners.Name)` — applies to the entire backend service.
* Endpoint/class level: `@CodeOwner(CodeOwners.Name)` — on a controller, handler, or resolver.
* Call scope: `withCodeOwner(CodeOwners.Name, () => {})` — wraps a specific function or record-level operation.

Example:

```tsx
import type { Logger } from '@island.is/logging'

@Injectable()
export class SomeService {
	constructor(@Inject(LOGGER_PROVIDER) logger: Logger) {
		this.logger = logger.child({ context: 'SomeService' });
	}

	async doWork(id: string) {
		withCodeOwner(CodeOwners.SomeTeam, async () => {
			callSomethingWithLogging() // logs include codeOwner: CodeOwners.SomeTeam
			this.logger.info('Finished work', { id }); // log includes both context: 'SomeService' and codeOwner: CodeOwners.SomeTeam
		});
	}
}
```

You may also provide custom context fields to multiple log entries using `withLoggingContext`:

```tsx
*withLoggingContext*({ applicationId }, () => {
  callSomethingWithLogging() // logs include applicationId
})
```

### How to log errors

{% hint style="info" %}
Remember to think twice before logging errors (see below).
{% endhint %}

JavaScript Error objects have non-enumerable fields (e.g., stack) that won’t appear if you spread them into fields. Log the Error as the log message so the formatter can include those fields.

```tsx
// Don't:
logger.warn('Validation error', { error })
logger.error('Unexpected error', { error })
logger.error('Could not submit application', { error, applicationId })
logger.error('Error saving to database', { error })

// Do:
logger.warn(error)
logger.error(error)
logger.error({ message: error, applicationId })
logger.error({ message: error, action: 'Saving to database' })
// These log entries will include "message", "name", "code", "stack", "cause" and more from error.
```

### How to catch and throw new errors

When you need to translate an unexpected error into a different one (e.g., to drive frontend behavior or enrich request-level handling), wrap the original error using the [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) option. This preserves the original error for logs while keeping it hidden from clients.

```tsx
try {
  // ...
} catch (err) {
  throw new Error('Failed saving to database', { cause: err });
}
```

### Think twice before logging errors

Generally, you should let unexpected errors bubble up to our request middleware, where they’ll be handled and logged. This centralizes error handling and prevents duplicate logs.

Different error types require different treatment.

#### Did it happen because of invalid user input?

Errors caused by invalid input should almost never be logged at the Error level to avoid noise—regardless of whether the bad input came from a user, a frontend bug, or environmental issues (e.g., outdated browser, network changes).

If unexpected input triggers an error in a library, database, or external API, catch it and rethrow a 4xx error (e.g., ValidationFailedProblem) so the middleware does not log it as an error. By default, 4xx errors are not logged by our middleware.

Before throwing a 4xx error, you may log the event at Info or Warn:

* Use Info for anticipated validation failures or common user mistakes.
* Use Warn when the input is unexpected or indicates a potential upstream/client issue.

```tsx
// Don't: log invalid user input as errors.
if (!title) {
	logger.error('Validation failed, title missing')
}

// Don't: throw errors which will be logged as errors by request middleware.
if (!title) {
  throw new Error('Validation failed, title missing')
}

// Do: throw 4xx errors, optionally log as info / warn.
if (!title) {
  logger.info('Validation failed, title missing', { id })
  throw new ValidationFailedProblem({ title: 'Title missing' })
}
```

#### Did it happen in an outgoing request?

Use our Enhanced Fetch library for all API integrations; it provides standardized error handling and logging for outbound requests.

* Don’t catch and log unexpected errors from outbound requests. Let them bubble up to the request middleware for consistent handling and logging.
* Do catch expected outcomes, such as 4xx responses caused by invalid input. When appropriate, use handle204 or handle404 to return null instead. (See the “Invalid user input” section above.)
* Don’t blanket-rethrow all 4xx responses. Some (e.g., 401) may indicate an environment/configuration issue and should bubble up to be handled and logged as unexpected errors.

When you must handle outbound errors to provide a fallback or degraded response, log the original error at the appropriate level:

* Network failures and 5xx responses: log at Error.
* 4xx responses: log at Warn (unless you’ve determined they represent a broader incident, in which case Error is appropriate).

```tsx
// Do: use enhanced fetch which has built-in error handling and logging.
const myFetch = createEnhancedFetch({ name: 'my-integration' })
await myFetch(url)

// Don't: log failed requests. It is done for you in enhanced fetch.
await myFetch.catch(err => {
  logger.error(err)
  throw err
})

// Do: rethrow outgoing request errors related to user input
try {
  await myFetch(url, { body: resource })
} catch (err) {
  if (err.problem?.type === ProblemType.VALIDATION_FAILED) {
	  throw new ValidationFailedError(err.problem.fields, { cause: err })
  }
}

// Do: Log unexpected responses as warnings if you must swallow them.
try {
	return await myFetch(url)
} catch (err) {
  logger.log(err.statusCode < 500 ? 'warn' : 'error', err)
  return fallbackValue
}
```

## **Standard fields**

<table data-header-hidden><thead><tr><th width="136.69140625">Field</th><th width="296.1953125">Description</th><th>Source</th></tr></thead><tbody><tr><td><code>timestamp</code></td><td>Time of the log entry.</td><td>Automatic (Logger)</td></tr><tr><td><code>level</code></td><td>Debug, info, warn or error depending on log method used.</td><td>Manual</td></tr><tr><td><code>message</code></td><td>Concise, human-readable summary of the log entry. The Error message when logging errors.</td><td>Manual</td></tr><tr><td><code>context</code></td><td>Which software component or class the log entry comes from.</td><td>Automatic (Child logger)</td></tr><tr><td><code>codeOwner</code></td><td>The team responsible for this part of our backend architecture.</td><td>Automatic (Service, Endpoint, Resource)</td></tr><tr><td><code>name</code></td><td>The Error name.</td><td>Automatic (Error)</td></tr><tr><td><code>stack</code></td><td>The Error stack trace.</td><td>Automatic (Error)</td></tr><tr><td><code>dd</code></td><td>Extra metadata provided by datadog agent. Includes <code>env</code>, <code>service</code> and <code>version</code> sub fields.</td><td>Automatic (Datadog)</td></tr><tr><td><code>traceSid</code></td><td>Trace Session ID to correlate logs with users without exposing PII.</td><td>Automatic (Authentication)</td></tr><tr><td><code>http</code></td><td>Information about outgoing requests and responses. May include these subfields: <code>status_code</code>, <code>status_text</code> and <code>url</code>.</td><td>Automatic (Enhanced Fetch)</td></tr><tr><td><code>fetch.name</code></td><td>Name of the enhanced fetch integration.</td><td>Automatic (Enhanced Fetch)</td></tr><tr><td><code>organizationSlug</code></td><td>Reference to a government organization which a log entry or error relates to.</td><td>Automatic (Enhanced Fetch)</td></tr></tbody></table>
