A structured logger with Slack integration and AWS Lambda support.
See CHANGELOG.md for a detailed list of changes.
- Structured logging with Bunyan
- Slack integration
- AWS Lambda context support
- TypeScript support
- Log level management
- Async context tracking
- Context management with AsyncLocalStorage
npm install @heyatlas/loggerimport { Logger } from "@heyatlas/logger";
// Only name is required at root level
const logger = new Logger({
name: "my-app",
});
// Environment is determined by process.env.NODE_ENV
// Defaults to "local" if not set
logger.info("Hello world", { userId: "123" });import { AsyncLocalStorage } from "async_hooks";
import { Logger, withLoggerLambda } from "@heyatlas/logger";
// Create execution context
const executionContext = new AsyncLocalStorage();
// Create logger configuration
const loggerConfig = {
name: "my-lambda-service",
slackApiToken: process.env.SLACK_TOKEN,
production: {
streams: [{ level: "info", type: "stdout" }],
slack: {
defaultChannel: "#prod-alerts",
level: "error",
},
},
};
// Create logger instance
const logger = new Logger(loggerConfig);
// Lambda handler
export const handler = withLoggerLambda(
executionContext,
logger, // Pass the logger instance
async (event, context) => {
// The logger is automatically available in the execution context
const contextLogger = executionContext.getStore()?.logger;
contextLogger?.info("Processing event", { event });
try {
// Your handler logic here
const result = await processEvent(event);
contextLogger?.info("Event processed successfully", { result });
return result;
} catch (error) {
contextLogger?.error("Failed to process event", { error });
throw error;
}
}
);
// Alternative: Pass config directly to withLoggerLambda
export const alternativeHandler = withLoggerLambda(
executionContext,
loggerConfig, // Pass the config directly
async (event, context) => {
// Logger is created internally and available in context
const contextLogger = executionContext.getStore()?.logger;
contextLogger?.info("Processing with config-based logger");
// ... handler logic
}
);import express from "express";
import { AsyncLocalStorage } from "async_hooks";
import { createLogger } from "@heyatlas/logger";
const app = express();
const executionContext = new AsyncLocalStorage();
// Create logger factory
const { logger, getLogger } = createLogger(executionContext, {
name: "my-api-service",
slackApiToken: process.env.SLACK_TOKEN,
production: {
streams: [{ level: "info", type: "stdout" }],
slack: {
defaultChannel: "#prod-alerts",
level: "error",
},
},
});
// Export getLogger for use in other modules
export { getLogger };
// Middleware to create context for each request
app.use((req, res, next) => {
executionContext.run({ logger }, () => next());
});
// Example route
app.get("/users/:id", async (req, res) => {
const logger = getLogger();
try {
logger.info("Fetching user", { userId: req.params.id });
const user = await userService.getUser(req.params.id);
logger.info("User fetched successfully", { user });
res.json(user);
} catch (error) {
logger.error("Failed to fetch user", { error });
res.status(500).json({ error: "Internal server error" });
}
});import { ApolloServer } from "apollo-server";
import { AsyncLocalStorage } from "async_hooks";
import { createLogger } from "@heyatlas/logger";
const executionContext = new AsyncLocalStorage();
// Create logger factory
const { logger, getLogger } = createLogger(executionContext, {
name: "my-graphql-service",
slackApiToken: process.env.SLACK_TOKEN,
production: {
streams: [{ level: "info", type: "stdout" }],
slack: {
defaultChannel: "#prod-alerts",
level: "error",
},
},
});
// Export getLogger for use in other modules
export { getLogger };
// Apollo Server context
const context = async ({ req }) => {
return executionContext.run({ logger }, () => ({ req }));
};
// Example resolver
const resolvers = {
Query: {
user: async (_, { id }, context) => {
const logger = getLogger();
try {
logger.info("Fetching user", { userId: id });
const user = await userService.getUser(id);
logger.info("User fetched successfully", { user });
return user;
} catch (error) {
logger.error("Failed to fetch user", { error });
throw error;
}
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
context,
});const logger = new Logger({
name: "my-app",
slackApiToken: "your-slack-token",
staging: {
streams: [{ type: "stdout", level: "info" }],
slack: {
defaultChannel: "#staging-logs",
level: "warn",
},
},
production: {
streams: [{ type: "stdout", level: "info" }],
slack: {
defaultChannel: "#production-logs",
level: "error",
},
},
});
// Regular logging with level validation
logger.error("Something went wrong", {
slack: { channel: "#alerts" },
error: new Error("Details here"),
});
// Direct Slack messaging without level validation
// Useful for sending notifications that don't need to go through the logger
logger.slackLogger?.sendDirect("#notifications", "Important notification");The sendDirect method allows you to send messages directly to Slack without any formatting or level validation. This is useful for:
- Sending simple notifications that don't need to be logged
- Reusing the SlackLogger instance in other parts of your application
- Sending plain text messages to specific channels
The logger uses NODE_ENV to determine which environment configuration to use:
import { Logger } from "@heyatlas/logger";
const logger = new Logger({
name: "my-service",
slackApiToken: process.env.SLACK_TOKEN, // Optional: enable Slack integration
// Environment-specific configurations
local: {
streams: [{ level: "debug", type: "stdout" }],
},
test: {
streams: [{ level: "fatal", type: "stdout" }],
},
staging: {
streams: [{ level: "info", type: "stdout" }],
slack: {
defaultChannel: "#staging-logs",
level: "warn",
},
},
production: {
streams: [{ level: "info", type: "stdout" }],
slack: {
defaultChannel: "#prod-alerts",
level: "error",
},
},
});The logger uses two levels of configuration:
-
Root Configuration:
name(required): Logger nameslackApiToken(optional): Slack API token for integration
-
Environment Configuration:
streams(required): Array of stream configurationsslack(optional): Slack settings per environmentdefaultChannel: Default Slack channellevel: Minimum level for Slack notifications
The logger supports creating child loggers that inherit context from their parent while allowing for additional bound fields. This is particularly useful for tracking request-specific information across different parts of your application.
// Create a parent logger
const logger = new Logger(config);
// Create a child logger with request-specific fields
const requestLogger = logger.child({ reqId: "123" });
// The child logger inherits the parent's context
logger.setContext("userId", "456");
requestLogger.info("Processing request"); // Includes both reqId and userId
// Child loggers can have their own context
requestLogger.setContext("component", "auth");
requestLogger.info("Authenticating user"); // Includes reqId, userId, and component
// Child loggers maintain their context across async boundaries
const executionContext = new AsyncLocalStorage<LoggerContext>();
executionContext.run({ logger: requestLogger }, async () => {
// The logger maintains its context here
requestLogger.info("Inside async context");
});Child loggers inherit their parent's context and can override it with their own bound fields. The inheritance works as follows:
- Parent context is copied to the child
- Bound fields are merged with the parent's context
- Child-specific context can be added using
setContext - Context is maintained across async boundaries
The logger supports the following log levels in order of priority:
fatal: System is unusableerror: Error conditionswarn: Warning conditionsinfo: Informational messagesdebug: Debug-level messagestrace: Trace-level messages
MIT