npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@herrromich/az-functions

v1.1.5

Published

Extensions for NodeJS Azure Functions

Downloads

920

Readme

@herrromich/az-functions

az-functions extension for Azure Functions in Node.js

npm version npm downloads License: MIT node

Overview

az-functions extends the Azure Functions programming model for Node.js (v4) by introducing a powerful structure for building scalable and maintainable serverless applications. It integrates several key concepts:

  • Inversion of Control (IoC) Container: Using InversifyJS to manage dependencies and ensure proper decoupling of components.
  • TypeScript Decorators: Declaratively define HTTP controllers, Event Hub handlers, and their arguments using decorators.
  • Input Validation: Integrating the Zod validation library to ensure that all inputs (e.g., HTTP request bodies, Event Hub messages) are properly validated before processing.
  • OpenAPI Code-First Generation: Automatically generating OpenAPI (Swagger) definitions based on the controller code, following a code-first approach.
  • Structured Logging: Built-in logging via Winston with optional OpenTelemetry / Application Insights export.

Installation

pnpm add @herrromich/az-functions @azure/functions inversify reflect-metadata zod @asteasolutions/zod-to-openapi
npm install @herrromich/az-functions @azure/functions inversify reflect-metadata zod @asteasolutions/zod-to-openapi
yarn add @herrromich/az-functions @azure/functions inversify reflect-metadata zod @asteasolutions/zod-to-openapi

@azure/functions, inversify, reflect-metadata,zod and @asteasolutions/zod-to-openapi are peer dependencies and must be installed alongside the package.

az-functions uses TypeScript decorators. Configure your tsconfig.json accordingly:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Quick Start

Step 1. Initialize reflect-metadata and optionally Zod for OpenAPI

// src/init.ts
import 'reflect-metadata';

import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);

Step 2. Define trigger handler classes

Define your HTTP controllers and/or Event Hub handlers using decorators (see sections below).

Step 3. Start the platform

Call startPlatform with a PlatformConfiguration object. The framework creates and manages the IoC container internally — you provide your ContainerModule[] and trigger handler classes:

// src/index.ts
import './init';

import { startPlatform, OtelConfiguration } from '@herrromich/az-functions';
import { OrdersController } from './controllers/orders.controller';
import { TruckTelemetryHandler } from './handlers/truck-telemetry.handler';
import { AppConfigModule } from './modules/app-config';
import { PersistenceModule } from './modules/persistence';
import { SecurityModule } from './modules/security';
import { ORDERS_REST_APPLICATION } from './applications/orders-api.application';

const otelConfiguration: OtelConfiguration | undefined =
  process.env.APPLICATIONINSIGHTS_CONNECTION_STRING !== undefined
    ? {
        applicationInsightsConnectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
        serviceName: process.env.WEBSITE_DEPLOYMENT_ID,
        serviceVersion: '1.0.0',
        serviceInstanceId: process.env.WEBSITE_INSTANCE_ID,
      }
    : undefined;

startPlatform({
  triggerHandlerClasses: [OrdersController, TruckTelemetryHandler],
  restApplications: [ORDERS_REST_APPLICATION],
  modules: [AppConfigModule, PersistenceModule, SecurityModule],
  loggerConfiguration: {
    otelConfiguration,
  },
});

[!WARNING] If you use a bundler like webpack, make sure the init import is at the very top of your entry module.

PlatformConfiguration

| Property | Type | Description | |--------------------------|--------------------------|------------------------------------------------------------------------| | triggerHandlerClasses | TriggerHandlerClass[] | HTTP controller and Event Hub handler classes to register. | | restApplications | RestApplication[] | Optional REST application definitions (OpenAPI config, context path). | | modules | ContainerModule[] | Inversify container modules loaded into the platform container. | | loggerConfiguration | LoggerConfiguration | Optional logger and OpenTelemetry configuration. |

HTTP Controller

Define a REST Application (optional)

A RestApplication defines the OpenAPI metadata and base context path for a group of controllers. If omitted, the platform generates a default OpenAPI definition with available endpoints.

// src/applications/orders-api.application.ts
import { RestApplication } from '@herrromich/az-functions';

export const ORDERS_API = 'orders-api';

export const ORDERS_REST_APPLICATION: RestApplication = {
  name: ORDERS_API,
  context: '/orders-api',
  openApiConfig: {
    openapi: '3.0.1',
    info: {
      title: 'Orders API',
      version: '1.0.0',
      description: 'API for Orders Management',
    },
    tags: [
      { name: 'Orders', description: 'CRUD operations for orders' },
    ],
    security: [{ bearerAuth: [] }],
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT',
        },
      },
    },
  },
};

Define an HTTP Controller

Use @HttpController (class decorator) and HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete, @Head) to define routes. Parameter decorators inject validated request data:

// src/controllers/orders.controller.ts
import { z } from 'zod';
import { inject } from 'inversify';
import {
  AuthContext,
  AuthCtx,
  BadRequestError,
  Body,
  Get,
  HttpController,
  HttpDirectResponseBuilder,
  LOGGER_FACTORY,
  LoggerFactory,
  NotFoundError,
  NumberSchema,
  Post,
  QueryParam,
} from '@herrromich/az-functions';
import { ORDERS_API } from './orders-api.application';

const OrderDtoSchema = z.object({
  id: z.string().uuid(),
  customerName: z.string(),
  total: z.number(),
}).openapi('OrderDto');

type OrderDto = z.infer<typeof OrderDtoSchema>;

const OrdersResponseSchema = z.object({
  items: z.array(OrderDtoSchema),
  total: z.number(),
}).openapi('OrdersResponse');

type OrdersResponse = z.infer<typeof OrdersResponseSchema>;

const OrderCreateRequestSchema = z.object({
  customerId: z.string().uuid(),
  items: z.array(z.object({ productId: z.string(), quantity: z.number() })),
}).openapi('OrderCreateRequest');

type OrderCreateRequest = z.infer<typeof OrderCreateRequestSchema>;

@HttpController({
  application: ORDERS_API,
  path: '/orders',
  tags: ['Orders'],
})
export class OrdersController {
  private readonly logger;

  constructor(
    @inject(LOGGER_FACTORY) loggerFactory: LoggerFactory,
    private readonly ordersRepository: OrdersRepository,
  ) {
    this.logger = loggerFactory();
  }

  @Get({
    description: 'Get orders with pagination',
    directResponse: {
      status: 200,
      description: 'Paginated list of orders',
      jsonContent: { schema: OrdersResponseSchema },
    },
  })
  async getOrders(
    @AuthCtx() authContext: AuthContext,
    @QueryParam({
      name: 'offset',
      schema: NumberSchema.optional().openapi({ description: 'Items to skip' }),
    })
    offset: number | undefined,
    @QueryParam({
      name: 'limit',
      schema: NumberSchema.optional().openapi({ description: 'Max items to return' }),
    })
    limit: number | undefined,
  ): Promise<OrdersResponse> {
    this.logger.info('Fetching orders.');
    // ... fetch and return orders
  }

  @Post({
    description: 'Create a new order',
    directResponse: {
      status: 201,
      description: 'Order created successfully',
      jsonContent: { schema: OrderDtoSchema },
    },
    responses: {
      '400': { description: 'Bad request error' },
    },
  })
  async createOrder(
    @AuthCtx() authContext: AuthContext,
    @Body({
      description: 'Order creation request',
      schema: OrderCreateRequestSchema,
    })
    request: OrderCreateRequest,
  ): Promise<HttpResponseInit> {
    this.logger.info('Creating order.');
    const order = await this.ordersRepository.create(request);
    return HttpDirectResponseBuilder.builder<OrderDto>()
      .header('Location', `/orders/${order.id}`)
      .jsonBody(order)
      .build();
  }
}

HTTP Decorators Reference

Class Decorator

| Decorator | Config | Description | |----------------------------|--------------------|--------------------------------------| | @HttpController(config) | ControllerConfig | Marks a class as an HTTP controller. |

ControllerConfig:

| Property | Type | Description | |---------------|--------------|--------------------------------------------------------------| | application | string? | Name of the REST application this controller belongs to. | | path | string | Base path for all operations in this controller. | | tags | string[]? | OpenAPI tags for this controller. |

Method Decorators

| Decorator | HTTP Method | |--------------------|-------------| | @Get(config?) | GET | | @Post(config?) | POST | | @Put(config?) | PUT | | @Patch(config?) | PATCH | | @Delete(config?) | DELETE | | @Head(config?) | HEAD |

Each accepts a ControllerOperationConfig (or ControllerRequestBodyOperationConfig for POST/PUT/PATCH) with properties like path, description, summary, directResponse, responses, authLevel, etc.

Parameter Decorators

| Decorator | Description | |------------------------|----------------------------------------------------------------------------------------------| | @Body(config) | Injects the validated request body. Config: { schema, description?, required?, example? } | | @QueryParam(config) | Injects a validated query parameter. Config: { name, schema? } | | @PathParam(config) | Injects a validated path parameter. Config: { name, schema? } | | @HeaderParam(config) | Injects a validated header value. Config: { name, schema? } | | @Request() | Injects the raw HttpRequest object. | | @AuthCtx() | Injects the AuthContext with the authenticated principal. |

HTTP Error Classes

Throw these from controller methods to return appropriate HTTP error responses:

| Class | Status Code | |-----------------------|-------------| | BadRequestError | 400 | | UnauthorizedError | 401 | | ForbiddenError | 403 | | NotFoundError | 404 | | InternalServerError | 500 |

Security

HTTP-level authentication/authorization is declarative: you describe who may call an operation using the same OpenAPI security/securitySchemes vocabulary you already use for documentation, and the framework resolves and enforces it at request time — no manual header parsing or auth checks in your controller code.

1. Declare a security scheme and require it

Security schemes are declared once per RestApplication (openApiConfig.components.securitySchemes) and referenced by name from either the application (applies to every operation) or an individual operation (overrides the application-level requirement for that operation only):

export const ORDERS_REST_APPLICATION: RestApplication = {
  name: ORDERS_API,
  context: '/orders-api',
  openApiConfig: {
    // ...
    security: [{ bearerAuth: [] }], // required for every operation unless overridden per-operation
    components: {
      securitySchemes: {
        bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
      },
    },
  },
};

class OrdersController {
  @Get({
    description: 'Get order by ID',
    // Overrides the application-level requirement for just this operation, and requires a scope:
    security: [{ bearerAuth: ['Orders.Read'] }],
    directResponse: { status: 200, description: 'Order', jsonContent: { schema: OrderDtoSchema } },
  })
  async getOrderById(@PathParam({ name: 'orderId', schema: IdDtoSchema }) orderId: string): Promise<OrderDto> {
    // ...
  }
}
  • security is an array of OR-ed requirement objects — any one of them satisfies the operation.
  • Each requirement object can list multiple scheme keys — all of them must succeed (AND) for that requirement to be satisfied.
  • The array of strings per scheme is the list of scopes required from the resolved principal.
  • Omitting security on both the operation and the application leaves the operation unauthenticated — the framework injects a default AuthContext (principal: null, principals: [], scopes: []).

2. Implement an AuthenticationService per scheme

Each scheme name used in securitySchemes must resolve to an AuthenticationService bound under AUTHENTICATION_SERVICE, scoped to that scheme name via .whenNamed(...):

import { AuthenticationError, AuthenticationService, AUTHENTICATION_SERVICE } from '@herrromich/az-functions';
import { HttpRequest } from '@azure/functions';
import { ContainerModule, injectable } from 'inversify';

@injectable()
export class BearerAuthenticationService implements AuthenticationService {
  async authenticate(request: HttpRequest): Promise<Principal> {
    const authHeader = request.headers.get('authorization');
    if (typeof authHeader !== 'string' || !authHeader.startsWith('Bearer ')) {
      throw new AuthenticationError('Invalid or missing Authorization Bearer token');
    }
    const token = authHeader.split(' ')[1] ?? '';
    // ...verify the token (e.g. via jwks-rsa/jsonwebtoken)...
    const scopes: string[] = []; // resolved scopes/permissions
    return { subject: 'user-id', type: 'user', scheme: 'bearer', scopes };
  }
}

export const SecurityModule = new ContainerModule(({ bind }) => {
  bind(AUTHENTICATION_SERVICE).to(BearerAuthenticationService).whenNamed('bearerAuth');
});
  • Throw AuthenticationError (not a generic Error) when the request simply isn't authenticated — the framework turns this into a 401 Unauthorized response. Any other error propagates and is treated as an unexpected failure (500).
  • Register the module in startPlatform({ modules: [...] }) like any other ContainerModule.
  • The same scheme name can be bound differently per RestApplication by additionally tagging the binding, if different applications need different authentication logic under the same scheme name — not needed for the common case of one implementation per scheme name across the whole app.

3. Principal and AuthContext

An AuthenticationService.authenticate(...) call resolves a Principal:

interface Principal {
  subject: string; // unique identifier of the principal (e.g. user id)
  type: string; // e.g. "user", "service"
  scheme: string; // auth scheme used (e.g. "bearer")
  scopes: string[]; // scopes/permissions granted to the principal
  meta?: Record<string, unknown>;
}

When an operation's requirement object has multiple schemes (AND), the resulting Principals are merged into a single AuthContext:

interface AuthContext {
  principal: Principal | null; // the (first) resolved principal, or null when unauthenticated
  principals: Principal[]; // every principal resolved for the operation (AND-composed schemes)
  scopes: string[]; // union of scopes across all resolved principals
}

By default, merging is strict: all AND-composed principals must share the same subject/type, or an AuthenticationError is thrown (surfaced as 401). Bind a custom PrincipalMergeService under PRINCIPAL_MERGE_SERVICE to override this behavior (e.g. to allow combining a user principal with a service principal under different rules).

Required scopes (from security: [{ scheme: [...scopes] }]) are checked against the resolved principal's scopes after authentication succeeds — a missing scope results in a 403 Forbidden response rather than 401, since the caller is authenticated but not authorized for that operation.

4. Access the AuthContext in a controller

Inject it into an operation method with @AuthCtx():

class OrdersController {
  async getOrderById(
    @AuthCtx() authContext: AuthContext,
    @PathParam({ name: 'orderId', schema: IdDtoSchema }) orderId: string,
  ): Promise<OrderDto> {
    const subject = authContext.principal?.subject;
    // ...
  }
}

Outside a controller method — e.g. in a mapper or a domain service that still needs the current caller's identity — inject SecurityContext instead and call getAuthentication(), which reads the same AuthContext from the active PlatformContextManager scope:

import { SecurityContext } from '@herrromich/az-functions';
import { inject, injectable } from 'inversify';

@injectable()
export class OrdersMapper {
  constructor(@inject(SecurityContext) private readonly securityContext: SecurityContext) {}

  toDto(order: Order) {
    const authContext = this.securityContext.getAuthentication();
    // ...
  }
}

5. Error responses

| Condition | Response | |--------------------------------------------------------------------------|----------| | No AuthenticationService for a requirement's scheme(s) succeeds | 401 Unauthorized | | At least one scheme succeeds, but the resolved principal is missing a required scope | 403 Forbidden |

These are enforced by the framework itself before your operation method runs — you don't need to (and shouldn't) throw UnauthorizedError/a forbidden response for missing/insufficient authentication yourself; reserve throwing UnauthorizedError/BadRequestError/NotFoundError in your own code for business-logic-level authorization/validation failures instead (see the "HTTP Error Classes" section above).

HttpDirectResponseBuilder

Use when you need full control over the HTTP response (custom status, headers):

return HttpDirectResponseBuilder.builder<OrderDto>()
  .header('Location', `/orders/${order.id}`)
  .jsonBody(orderDto)
  .build();

OpenAPI Generation

Once the application is built, you can generate the OpenAPI specification in a special mode:

PLATFORM_MODE=print-open-api node dist/index.js

This outputs the OpenAPI JSON and YAML files for each registered REST application without starting the Azure Functions host.

| Environment Variable | Default | Description | |-----------------------|-----------------------------|--------------------------------------------------------------------------------------------------------| | PLATFORM_MODE | start | Set to print-open-api to generate OpenAPI definitions instead of starting the host. | | OPEN_API_PRINT_PATH | dist/open-api-definitions | Directory where generated .json and .yaml files are written. Resolved relative to process.cwd(). |

Example with a custom output path:

PLATFORM_MODE=print-open-api OPEN_API_PRINT_PATH=./docs/api node dist/index.js

Event Hub Handler

Define an Event Hub Handler

Use @EventHubHandler (class decorator) to specify the Event Hub connection and name. Use @OnEventHubTrigger (method decorator) to define the trigger method:

// src/handlers/truck-telemetry.handler.ts
import { z } from 'zod';
import { inject } from 'inversify';
import {
  EventHubHandler,
  EventHubMessageWrapper,
  LOGGER_FACTORY,
  LoggerFactory,
  Messages,
  OnEventHubTrigger,
} from '@herrromich/az-functions';

const TelemetryPayloadSchema = z.object({
  deviceId: z.string(),
  temperature: z.number(),
  timestamp: z.string(),
});

type TelemetryPayload = z.infer<typeof TelemetryPayloadSchema>;

@EventHubHandler({
  connection: 'EventHubConnection',
  eventHubName: 'telemetry',
})
export class TruckTelemetryHandler {
  private readonly logger;

  constructor(@inject(LOGGER_FACTORY) loggerFactory: LoggerFactory) {
    this.logger = loggerFactory();
  }

  @OnEventHubTrigger({ cardinality: 'many' })
  async handleTelemetry(
    @Messages({
      withPayload: TelemetryPayloadSchema,
      withEventData: true,
    })
    messages: EventHubMessageWrapper<TelemetryPayload, undefined, undefined, true>[],
  ): Promise<void> {
    this.logger.info(`Received ${messages.length} telemetry messages`, {
      messages: messages.map(msg => ({
        payload: msg.payload,
        enqueuedTimeUtc: msg.eventData.enqueuedTimeUtc,
      })),
    });
  }
}

Event Hub Decorators Reference

Class Decorator

| Decorator | Config | Description | |-----------------------------|-------------------------|-----------------------------------------| | @EventHubHandler(config) | EventHubHandlerConfig | Marks a class as an Event Hub handler. |

EventHubHandlerConfig:

| Property | Type | Description | |----------------|----------|----------------------------------------------------------------| | connection | string | App setting name containing the Event Hub connection string. | | eventHubName | string | Name of the Event Hub to listen to. |

Method Decorator

| Decorator | Config | Description | |--------------------------------|---------------------------|----------------------------------------------------------------| | @OnEventHubTrigger(config?) | OnEventHubTriggerConfig | Marks the method that handles incoming Event Hub messages. |

OnEventHubTriggerConfig:

| Property | Type | Default | Description | |----------------|---------------------|------------|--------------------------------------------------| | triggerId | string? | — | Unique identifier for the trigger. | | consumerGroup| string? | $Default | Consumer group to use. | | cardinality | 'one' \| 'many'? | 'many' | Whether to receive a single message or a batch. | | extraInputs | FunctionInput[]? | — | Additional Azure Functions inputs. | | extraOutputs | FunctionOutput[]? | — | Additional Azure Functions outputs. |

Parameter Decorators

| Decorator | Cardinality | Description | |----------------------|-------------|--------------------------------------------------| | @Message(config?) | one | Injects a single validated message wrapper. | | @Messages(config?) | many | Injects an array of validated message wrappers. | | @RawMessage() | one | Injects the raw unvalidated message. | | @RawMessages() | many | Injects the raw unvalidated message array. |

EventHubTriggerMessageArgConfig (for @Message / @Messages):

| Property | Type | Description | |-------------------------|------------|------------------------------------------------------------------------------| | withPayload | ZodType? | Schema to validate the message payload. | | withProperties | ZodType? | Schema to validate custom properties. | | withSystemProperties | ZodType? | Schema to validate system properties. | | withEventData | true? | Include Event Hub event data (offset, sequence number, enqueued time, etc.). |

EventHubMessageWrapper<PAYLOAD, PROPERTIES, SYSTEM_PROPERTIES, EVENT_DATA>

The wrapper type provides typed access to message components:

message.payload           // PAYLOAD — validated message body
message.properties        // PROPERTIES — custom properties (if schema provided)
message.systemProperties  // SYSTEM_PROPERTIES — system properties (if schema provided)
message.eventData         // EventHubEventData — event metadata (if withEventData: true)
message.valid             // boolean — whether validation succeeded

Startup Service

Register a startup hook that runs when the Azure Functions host starts:

import { IStartupService, STARTUP_SERVICE } from '@herrromich/az-functions';
import { injectable } from 'inversify';

@injectable()
export class MyStartupService implements IStartupService {
  async startup(): Promise<void> {
    // Run migrations, warm caches, etc.
  }
}

Register it in a container module:

import { ContainerModule } from 'inversify';
import { STARTUP_SERVICE } from '@herrromich/az-functions';
import { MyStartupService } from './startup.service';

export const StartupModule = new ContainerModule(({ bind }) => {
  bind(STARTUP_SERVICE).to(MyStartupService);
});

Logging

The framework provides a structured logging system built on Winston. It supports automatic logger naming, hierarchical log level configuration, per-invocation context metadata, and optional OpenTelemetry export.

Basic Usage

Inject LOGGER_FACTORY and call it to create a logger scoped to your class:

import { inject } from 'inversify';
import { LOGGER_FACTORY, LoggerFactory } from '@herrromich/az-functions';

@injectable()
export class OrdersService {
  private readonly logger;

  constructor(@inject(LOGGER_FACTORY) loggerFactory: LoggerFactory) {
    this.logger = loggerFactory(); // auto-detects logger name from the call stack
  }

  doWork() {
    this.logger.info('Processing started.');
    this.logger.debug('Details:', { someContext: 'value' });
  }
}

You can also provide an explicit logger name:

this.logger = loggerFactory('my-app.orders.OrdersService');

Log Levels

The following log levels are available, ordered from highest to lowest severity:

| Level | Priority | Description | |-----------|----------|------------------------| | error | 0 | Error conditions | | warn | 1 | Warning conditions | | info | 2 | Informational messages | | http | 3 | HTTP request logging | | verbose | 4 | Verbose output | | debug | 5 | Debug information | | silly | 6 | Trace-level detail |

Logger Interface

The Logger type exposes a method for each log level. Each method accepts a message and optional metadata:

logger.error('Operation failed', { orderId, error });
logger.warn('Retrying request', { attempt: 3 });
logger.info('Order created', { orderId });
logger.verbose('Cache hit', { key });
logger.debug('Query executed', { sql, params });
logger.silly('Entering method', { args });

Default Log Level

Set the default log level for all loggers via LoggerConfiguration:

startPlatform({
  // ...
  loggerConfiguration: {
    defaultLogLevel: 'debug', // default is 'info' if omitted
  },
});

The default log level is set once at startup and applies to all loggers that don't have a specific level configured via LogLevelProvider. It can be read from the container using the DEFAULT_LOG_LEVEL service identifier (e.g., for use in a LogLevelProvider constructor).

Per-Logger Log Level Configuration

For fine-grained control, implement a LogLevelProvider and bind it to LOG_LEVEL_PROVIDER. The provider receives the logger name and returns the applicable log level (or undefined to fall back to the default):

import { LogLevel, LogLevelProvider, LOG_LEVEL_PROVIDER } from '@herrromich/az-functions';
import { injectable } from 'inversify';

@injectable()
class MyLogLevelProvider implements LogLevelProvider {
  getLogLevel(loggerName?: string): LogLevel | undefined {
    if (loggerName?.startsWith('my-app.persistence')) {
      return 'error'; // suppress noisy persistence logs
    }
    if (loggerName?.startsWith('my-app.orders')) {
      return 'debug'; // verbose logging for orders module
    }
    return undefined; // use default log level
  }
}

export const LoggerModule = new ContainerModule(({ bind }) => {
  bind(LOG_LEVEL_PROVIDER).to(MyLogLevelProvider);
});

Hierarchical Log Levels with TrieSearchService

For applications with many loggers, use the built-in TrieSearchService to configure log levels in a hierarchical, prefix-based manner. Logger names separated by . are matched using a trie — the longest matching prefix wins:

import {
  DEFAULT_LOG_LEVEL,
  LogLevel,
  LogLevelProvider,
  LOG_LEVEL_PROVIDER,
  SYSTEM_LOGGER_NAME_PREFIX,
  TrieSearchService,
} from '@herrromich/az-functions';
import { inject, optional } from 'inversify';

export class TrieSearchLogLevelProvider extends TrieSearchService<LogLevel> implements LogLevelProvider {
  constructor(@inject(DEFAULT_LOG_LEVEL) @optional() defaultLogLevel: LogLevel) {
    super('.', defaultLogLevel); // '.' is the separator for hierarchical names
    this.set(SYSTEM_LOGGER_NAME_PREFIX, 'warn');       // platform internals: warn and above
    this.set('my-app.persistence.kysely', 'error');     // Kysely SQL logs: errors only
    this.set('my-app.orders', 'debug');                 // orders module: debug and above
  }

  getLogLevel(loggerName: string | undefined): LogLevel | undefined {
    return this.find(loggerName);
  }
}

export const LoggerModule = new ContainerModule(({ bind }) => {
  bind(TrieSearchLogLevelProvider).toSelf();
  bind(LOG_LEVEL_PROVIDER).toService(TrieSearchLogLevelProvider);
});

With this configuration:

  • my-app.orders.OrdersService → debug (matches my-app.orders prefix)
  • my-app.orders.OrdersMapper → debug (matches my-app.orders prefix)
  • my-app.persistence.kysely → error (exact match)
  • my-app.persistence.repository → default level (no specific prefix match)
  • #az-functions.http-controller → warn (matches #az-functions prefix)

Since TrieSearchService has set(), get(), and getAll() methods, you can also adjust log levels at runtime. For example, by exposing an HTTP endpoint (see the example project for a full LogLevelsController implementation).

Automatic Logger Name Resolution

By default, loggerFactory() (called without arguments) uses LOGGER_NAME_PROVIDER to derive the logger name from the call stack. Bind a custom LOGGER_NAME_PROVIDER to control how names are derived:

import { LOGGER_NAME_PROVIDER } from '@herrromich/az-functions';

export const LoggerModule = new ContainerModule(({ bind }) => {
  bind(LOGGER_NAME_PROVIDER).toFactory(() => {
    const regexp =
      /^\s*at\s+(?:new\s+)?([A-Za-z0-9_$]+)\s+\(.*src[\\/](.+)\/[^\\/]+:\d+:\d+\)$/;
    return (stackEntry?: string) => {
      const stackLines = stackEntry?.split('\n') ?? [];
      for (const line of stackLines) {
        const match = regexp.exec(line.trim());
        if (match) {
          return `my-app.${match[2]?.replace(/\//g, '.')}.${match[1]}`;
        }
      }
    };
  });
});

This produces logger names like my-app.shared.orders.OrdersService based on the file path and class name of the caller.

Context Logger Metadata

Use adjustContextLoggerMetadata to attach metadata to all log messages within the current invocation context. Metadata is configured per log level, so you can include detailed data only at lower severity levels to avoid noise:

import {
  adjustContextLoggerMetadata,
  PLATFORM_CONTEXT_MANAGER,
} from '@herrromich/az-functions';

// Attach metadata scoped to the current invocation context
adjustContextLoggerMetadata(contextManager, {
  error: {
    // included in error-level messages — full diagnostic data
    requestId,
    userId,
    requestBody,
  },
  warn: {
    // included in warn-level messages — moderate detail
    requestId,
    userId,
  },
  silly: {
    // included in silly-level messages — full trace data
    requestId,
    userId,
    requestBody,
    headers,
  },
});

The metadata is automatically merged with any existing context metadata. It is included in all subsequent log messages at the corresponding level within the same invocation.

SYSTEM_LOGGER_NAME_PREFIX

The constant SYSTEM_LOGGER_NAME_PREFIX ('#az-functions') identifies loggers used internally by the platform. Use it when configuring log levels to control the verbosity of framework-internal logging separately from your application:

this.set(SYSTEM_LOGGER_NAME_PREFIX, 'warn'); // only warnings and errors from the framework

When an internal (platform) class calls loggerFactory() without an explicit name, the name is auto-derived from the call stack as ${SYSTEM_LOGGER_NAME_PREFIX}.<module-path>.<ClassName>, where <module-path> mirrors the source folder structure inside the package (with / replaced by .) and <ClassName> is the constructor, class, or function that requested the logger.

Because logger names follow this hierarchical, dot-separated structure, you can use TrieSearchService (see above) to target a whole subsystem with a single prefix, or a specific class with a fully qualified name.

The following internal logger names are currently produced by the platform:

| Logger Name | Module / Component | |--------------------------------------------------------------------------|--------------------------------------------------------| | #az-functions.platform.AzurePlatform | Core platform bootstrap / trigger wiring | | #az-functions.http-controller.HttpOperationsRegistrationService | HTTP operation registration with the Functions host | | #az-functions.http-controller.HttpHandlerFactory | HTTP trigger handler creation | | #az-functions.http-controller.OpenApiRegistrationService | OpenAPI route/document registration | | #az-functions.http-controller.OpenApiDefinitionService | OpenAPI definition generation | | #az-functions.http-controller.OpenApiPrintService | OpenAPI print (PLATFORM_MODE=print-open-api) mode | | #az-functions.http-controller.security.AuthenticatorProvider | HTTP authenticator resolution | | #az-functions.http-controller.security.OperationAuthenticationResolver | Per-operation authentication resolution | | #az-functions.event-hub-handler.EventHubTriggersRegistrationService | Event Hub trigger registration with the Functions host | | #az-functions.event-hub-handler.EventHubHandlerFactory | Event Hub trigger handler creation |

[!NOTE] This list reflects the classes that currently request a logger via loggerFactory() without an explicit name. It may grow as the framework evolves — inspect the resolved logger name via your own LogLevelProvider or log output if you need to confirm the exact name for your installed version.

Log Sanitization

Before log metadata is emitted, the platform sanitizes it to avoid leaking sensitive data and to keep log entries bounded in size. Sanitization is applied automatically to every logged metadata object (e.g. the second argument passed to logger.info(message, metadata), error cause/details, and HttpRequest / HttpResponse instances) — you don't need to call anything yourself for the default behavior.

What sanitization does:

  • Redacts sensitive headers. authorization, cookie and any header starting with x-ms are replaced with [REDACTED] when an HttpRequest/HttpResponse is logged.
  • Omits request/response bodies, replacing them with a placeholder like [RequestBody<123Byte>] so payloads are never logged inline.
  • Truncates long strings to a maximum length (appending ...).
  • Truncates large arrays/sets/maps to a maximum number of elements (appending a ... more N items marker).
  • Truncates objects with many keys to a maximum number of keys (adding a __meta__ entry describing how many keys were omitted).
  • Limits recursion depth for deeply nested objects, replacing anything beyond the limit with a short placeholder (e.g. [Object], [Array<5>]).
  • Truncates error stack traces to a maximum number of lines, while still recursing into cause and (for AzFunctionsError) details.
  • Detects circular references, replacing repeated object references with [Circular].

Default Limits

Limits are configured per log level via SanitizerOptions (maxDepth, maxTraceLength, maxArrayLength, maxKeysCount, maxStringLength). Lower-severity/high-volume levels use tighter limits, while error and silly are effectively unbounded so no diagnostic information is lost when you need it most:

| Log Level | maxDepth | maxTraceLength | maxArrayLength | maxKeysCount | maxStringLength | |-----------|-----------:|-----------------:|------------------:|---------------:|-------------------:| | error | unbounded | 10 (default) | unbounded | unbounded | unbounded | | warn | 10 | 10 (default) | 20 (default) | 20 (default) | 250 (default) | | info | 5 (default)| 10 (default) | 20 (default) | 20 (default) | 250 (default) | | http | 5 | 10 (default) | 10 | 20 (default) | 250 (default) | | verbose | 10 | 10 (default) | 20 (default) | 20 (default) | 250 (default) | | debug | 20 | 10 (default) | 25 | 25 | 1000 | | silly | unbounded | 10 (default) | unbounded | unbounded | unbounded |

[!NOTE] Cells marked "(default)" fall back to the base defaults used by sanitizeMetadata (maxDepth: 5, maxTraceLength: 10, maxArrayLength: 20, maxKeysCount: 20, maxStringLength: 250) because that log level has no explicit override for that property.

Reconfiguring Sanitization

Override the defaults (per log level) via sanitizerOptions in LoggerConfiguration. Only the levels/properties you want to change need to be specified — everything else keeps its default:

startPlatform({
  // ...
  loggerConfiguration: {
    sanitizerOptions: {
      // allow larger payloads to be logged at info level
      info: { maxStringLength: 2000, maxArrayLength: 50 },
      // reduce noise from warn-level logs even further
      warn: { maxDepth: 3, maxKeysCount: 10 },
    },
  },
});

OpenTelemetry / Application Insights

Pass otelConfiguration in loggerConfiguration to export logs and traces to Application Insights:

startPlatform({
  // ...
  loggerConfiguration: {
    otelConfiguration: {
      applicationInsightsConnectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
      serviceName: 'my-service',
      serviceVersion: '1.0.0',
    },
  },
});

If no otelConfiguration is provided, the framework falls back to the built-in Azure Functions InvocationContext logger.

Registering Unsupported Azure Functions Triggers

For trigger types not yet supported by decorators (e.g., Cosmos DB, Timer), use the Azure Functions SDK directly. To benefit from the platform's structured logging and context propagation, retrieve the PlatformContextManager and PlatformContextProvider from the platform container and wrap your handler execution:

import { app, InvocationContext } from '@azure/functions';
import {
  Logger,
  LOGGER_FACTORY,
  LoggerFactory,
  PLATFORM_CONTEXT_MANAGER,
  PLATFORM_CONTEXT_PROVIDER,
  startPlatform,
} from '@herrromich/az-functions';

// startPlatform returns the platform container
const platformContainer = startPlatform({ /* ... */ });

const contextManager = platformContainer.get(PLATFORM_CONTEXT_MANAGER);
const contextProvider = platformContainer.get(PLATFORM_CONTEXT_PROVIDER);
const loggerFactory = platformContainer.get(LOGGER_FACTORY);
const logger: Logger = loggerFactory('cosmosdb-handler');

app.cosmosDB('cosmosDbTrigger', {
  connection: 'CosmosDBConnection',
  containerName: 'my-container',
  databaseName: 'my-database',
  handler: async (documents: unknown[], context: InvocationContext) => {
    // Wrap execution in the platform context for structured logging
    return contextManager.runWith(
      contextProvider.providePlatformContext(context),
      async () => {
        logger.info(`Received ${documents.length} documents from Cosmos DB`);
        logger.debug('Processing documents', { documents });
        // ... handle documents
      },
    );
  },
});

Key services available from the platform container:

| Service Identifier | Type | Description | |-----------------------------|---------------------------|---------------------------------------------------------------------------------------------------------| | PLATFORM_CONTEXT_MANAGER | PlatformContextManager | Manages the async context. Use runWith() to scope logging and context values to a handler invocation. | | PLATFORM_CONTEXT_PROVIDER | PlatformContextProvider | Creates a PlatformContext from an InvocationContext. | | LOGGER_FACTORY | LoggerFactory | Creates scoped Logger instances with structured logging support. |

Without wrapping in contextManager.runWith(), the logger will still work but won't have access to the invocation context (e.g., invocation ID, trigger metadata) for log correlation.

License

MIT