npm package: @sqdgn/context-logging
Description: SQDGN context logging library
Author: Leszek Wiesner
Latest version: 0.1.1

# SQDGN Context Logging Library

## Usage

### Simple app w/o Kafka

#### `main.ts`

```typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { initializeLogger } from '@sqdgn/context-logging/logger';
import { ContextInterceptor } from '@sqdgn/context-logging/context';
// ...

async function bootstrap() {
  const logger = initializeLogger({
    appName: 'name-of-the-service',
    // Optionally some other options...
  });
  const app = await NestFactory.create(AppModule, {
    logger: logger.adapters.NestJs(),
  });
  app.useGlobalInterceptors(
    new ContextInterceptor({
      enableHttpDebugLogs: true, // optional
    }),
  );
  // ...
  await app.listen(process.env.PORT ?? 3000);
}

void bootstrap();
```

#### `app.module.ts`

```typescript
import { Module } from '@nestjs/common';
import { ExampleController } from './example.controller';
import { HttpRequestModule } from '@sqdgn/context-logging/http-request';

@Module({
  imports: [
    // ...
    HttpRequestModule.register(),
  ],
  controllers: [
    // ...
    ExampleController,
  ],
})
export class AppModule {}
```

#### `example.controller.ts`

```typescript
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
import { Context, LoggingContext } from '@sqdgn/context-logging/context';
import { HttpRequestService } from '@sqdgn/context-logging/http-request';

@Controller()
export class ExampleController {
  constructor(private readonly httpRequestService: HttpRequestService) {}

  @Get('example')
  example(@LoggingContext() ctx: Context, @Req() req: Request) {
    ctx.merge({ some_additional: 'context' });
    ctx.logger.info('Something happened');
    // Call another service and pass the current context
    await this.httpRequestService.get(ctx, `http://some-other-service/api/v1/example`);
  }
}
```

### Hybrid application with Kafka microservice

#### `main.ts`

```typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ContextInterceptor } from '@sqdgn/context-logging/context';
import { initializeLogger } from '@sqdgn/context-logging/logger';
import { KafkaOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const logger = initializeLogger({
    appName: 'name-of-the-service',
    // Optionally some other options...
  });

  const app = await NestFactory.create(AppModule, {
    logger: logger.adapters.NestJs(),
  });

  app.useGlobalInterceptors(
    new ContextInterceptor({
      enableHttpDebugLogs: true, // optional
      enableKafkaDebugLogs: true, // optional
    }),
  );

  app.connectMicroservice<KafkaOptions>(
    {
      transport: Transport.KAFKA,
      options: {
        client: {
          clientId: 'name-of-the-service',
          brokers: ['localhost:9092'],
        },
        consumer: {
          groupId: 'name-of-the-service-consumer',
        },
        subscribe: {
          fromBeginning: true,
        },
        // Optionally some other options...
      },
    },
    // Important!
    { inheritAppConfig: true },
  );

  // ...
  await app.startAllMicroservices();
  await app.listen(process.env.PORT ?? 3000);
}

void bootstrap();
```

#### `app.module.ts`

```typescript
import { Module } from '@nestjs/common';
import { ClientsModule, KafkaOptions, Transport } from '@nestjs/microservices';
import { KafkaProducerModule, KAFKA_PRODUCER_CLIENT } from '@sqdgn/context-logging/kafka-producer';
import { ExampleController } from './example.controller';

@Module({
  imports: [
    // ...
    ClientsModule.register({
      isGlobal: true, // required
      clients: [
        {
          name: KAFKA_PRODUCER_CLIENT,
          transport: Transport.KAFKA,
          options: {
            client: {
              clientId: 'name-of-the-service',
              brokers: ['localhost:9092'],
            },
            producerOnlyMode: true,
            // ... Optionally some other options
          },
        } satisfies KafkaOptions & { name: string },
      ],
    }),
    KafkaProducerModule,
  ],
  controllers: [
    // ...
    ExampleController,
  ],
})
export class AppModule {}
```

#### `example.controller.ts`

```typescript
import { EventPattern, Payload } from '@nestjs/microservices';
import { Context, LoggingContext } from '@sqdgn/context-logging/context';
import { Controller } from '@nestjs/common';
import { KafkaProducerService } from '@sqdgn/context-logging/kafka-producer';

@Controller()
export class ExampleController {
  constructor(private readonly kafkaProducerService: KafkaProducerService) {}

  @EventPattern('example-kafka-topic')
  handleKafkaMessage(@LoggingContext() ctx: Context, @Payload() actualMessage: unknown) {
    ctx.merge({ some_additional: 'context' });
    ctx.logger.info('Something happened');
    // Use KafkaProducerService to pass the current context to the produced message
    await this.kafkaProducerService.sendMessage<{ data: string }>({ data: 'some-value' }, ['other-kafka-topic']);
  }
}
```

## Ways of retrieving the `Context` object

### Using `@LoggingContext()` decorator in controllers

```typescript
import { Context, LoggingContext } from '@sqdgn/context-logging/context';
import { Controller, Get } from '@nestjs/common';

@Controller()
export class ExampleController {
  @Get('example')
  example(@LoggingContext() ctx: Context) {
    // ...
  }
}
```

### From `Request` object (HTTP only)

```typescript
import { Context } from '@sqdgn/context-logging/context';
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller()
export class ExampleController {
  @Get('example')
  example(@Req() req: Request) {
    const ctx = req.context as Context;
    // ...
  }
}
```

### Using `contextProvider` (`AsyncLocalStorage`)

```typescript
import { contextProvider } from '@sqdgn/context-logging/context';

export class ExampleService {
  private get logger() {
    const ctx = contextProvider.getContext();
    return ctx.child({ service: ExampleService.name }).logger;
  }

  example() {
    this.logger.info('Log something with current context');
    // ...
  }
}
```

## Logger Configuration

### **`.env`**

```
LOG_LEVEL=debug
SENTRY_DSN={sentry_dsn_uri}

# Defaults to `RAILWAY_ENVIRONMENT_NAME`
SENTRY_ENVIRONMENT_NAME={environment_name}

GOOGLE_APPLICATION_CREDENTIALS={path_to_credentials_file}
# OR:
GCP_CREDENTIALS_B64={base64_encoded_credentials_json}
```

- Omitting `SENTRY_DSN` disables logging to Sentry
- Omitting both `GOOGLE_APPLICATION_CREDENTIALS` and `GCP_CREDENTIALS_B64` disables logging to GCP

### Default configuration

The default configuration sends:

- all logs of level >= `process.env.LOG_LEVEL` (`debug` by default) to GCP,
- `warn`+ level logs to Sentry.

### Changing log levels for GCP / Sentry

```typescript
const logger = initializeLogger({
  appName: 'name-of-the-service',
  gcp: {
    logLevel: 'warn', // Only send warn+ level logs to GCP
  },
  sentry: {
    logLevel: 'error', // Only send error level logs to Sentry
  },
});
```

### Using custom `type` filter for GCP logs

```typescript
const logger = initializeLogger({
  appName: 'name-of-the-service',
  gcp: {
    logLevel: 'error', // Only send error level logs to GCP
    typeFilter: 'gcp', // Require `type: 'gcp'` being set in the log in order to send it to GCP
  },
  // ...
});
```