@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
494 lines (451 loc) • 13.5 kB
text/typescript
/**
* Error Logger Utilities
*
* Standardizes error logging across the KRAPI SDK with consistent formatting
* and context inclusion.
*/
import { Logger } from "../core";
import { KrapiError } from "../core/krapi-error";
import { HttpError } from "../http-clients/http-error";
/**
* Log levels for error logging
*/
export type LogLevel = "error" | "warn" | "info" | "debug";
/**
* Error log context interface
*
* Comprehensive context for error logging including:
* - Operation details (what was being performed)
* - Request data (what was sent)
* - Response data (what was received)
* - User/session context
* - System state
*/
export interface ErrorLogContext {
// Operation context
operation?: string;
service?: string;
// Resource context
userId?: string;
projectId?: string;
collectionName?: string;
documentId?: string;
// Request tracking
requestId?: string;
userAgent?: string;
ipAddress?: string;
sessionId?: string;
// HTTP request context (if applicable)
httpMethod?: string;
httpUrl?: string;
requestBody?: unknown;
requestQuery?: Record<string, unknown>;
requestHeaders?: Record<string, string>;
// HTTP response context (if applicable)
responseData?: unknown;
responseHeaders?: Record<string, string>;
httpStatus?: number;
// Input parameters (what data was received)
inputData?: Record<string, unknown>;
parameters?: Record<string, unknown>;
// Additional context
[key: string]: unknown;
}
/**
* Structured error log entry
*/
export interface ErrorLogEntry {
timestamp: string;
level: LogLevel;
message: string;
error: {
code: string;
message: string;
status?: number;
details?: Record<string, unknown>;
};
context?: ErrorLogContext;
stack?: string | undefined;
}
/**
* Log an error with standardized formatting
*
* Formats and logs errors consistently across the SDK, including error codes,
* context information, and appropriate log levels.
*
* @param logger - Logger instance to use
* @param error - The error to log
* @param context - Additional context for the error
* @param level - Log level (defaults to 'error')
*/
export function logError(
logger: Logger,
error: unknown,
context?: ErrorLogContext,
level: LogLevel = "error"
): void {
const logEntry = createErrorLogEntry(error, context);
// Use appropriate logger method based on level
switch (level) {
case "error":
logger.error(formatErrorMessage(logEntry), logEntry);
break;
case "warn":
logger.warn(formatErrorMessage(logEntry), logEntry);
break;
case "info":
logger.info(formatErrorMessage(logEntry), logEntry);
break;
case "debug":
logger.debug(formatErrorMessage(logEntry), logEntry);
break;
}
}
/**
* Create a structured error log entry with comprehensive context
*
* Extracts all available information from errors including:
* - Request data (body, query, headers)
* - Response data (body, headers, status)
* - Operation context
* - Full stack traces
* - All error details
*
* @param error - The error to log
* @param context - Additional context
* @returns Structured error log entry with full context
*/
export function createErrorLogEntry(
error: unknown,
context?: ErrorLogContext
): ErrorLogEntry {
const timestamp = new Date().toISOString();
// Handle HttpError - extract all HTTP context
if (error instanceof HttpError) {
const httpContext: ErrorLogContext = {
...(context || {}),
...(error.method ? { httpMethod: error.method } : {}),
...(error.url ? { httpUrl: error.url } : {}),
...(error.status !== undefined ? { httpStatus: error.status } : {}),
...(error.requestHeaders ? { requestHeaders: error.requestHeaders } : {}),
...(error.requestBody !== undefined ? { requestBody: error.requestBody } : {}),
...(error.requestQuery ? { requestQuery: error.requestQuery } : {}),
...(error.responseData !== undefined ? { responseData: error.responseData } : {}),
...(error.responseHeaders ? { responseHeaders: error.responseHeaders } : {}),
};
const logEntry: ErrorLogEntry = {
timestamp,
level: "error",
message: error.message,
error: {
code: error.code || "HTTP_ERROR",
message: error.message,
...(error.status !== undefined ? { status: error.status } : {}),
details: {
isApiError: error.isApiError,
isNetworkError: error.isNetworkError,
isAuthError: error.isAuthError,
isClientError: error.isClientError,
isServerError: error.isServerError,
originalError: error.originalError,
},
},
stack: error.stack,
context: httpContext,
};
return logEntry;
}
// Handle KrapiError - extract all error details
if (error instanceof KrapiError) {
// Extract HTTP context from error details if it exists
const httpErrorDetails = error.details?.httpError as {
method?: string;
url?: string;
status?: number;
requestBody?: unknown;
requestQuery?: Record<string, unknown>;
requestHeaders?: Record<string, string>;
responseData?: unknown;
responseHeaders?: Record<string, string>;
} | undefined;
const enhancedContext: ErrorLogContext = {
...(error.requestId ? { requestId: error.requestId } : {}),
...context,
// Extract HTTP context from error details
...(httpErrorDetails ? {
httpMethod: httpErrorDetails.method,
httpUrl: httpErrorDetails.url,
httpStatus: httpErrorDetails.status,
requestBody: httpErrorDetails.requestBody,
requestQuery: httpErrorDetails.requestQuery,
requestHeaders: httpErrorDetails.requestHeaders,
responseData: httpErrorDetails.responseData,
responseHeaders: httpErrorDetails.responseHeaders,
} : {}),
// Include all error details
...(error.details ? { errorDetails: error.details } : {}),
};
const logEntry: ErrorLogEntry = {
timestamp,
level: getLogLevelFromError(error),
message: error.message,
error: {
code: error.code,
message: error.message,
...(error.status !== undefined ? { status: error.status } : {}),
...(error.details ? { details: error.details } : {}),
},
stack: error.stack,
context: enhancedContext,
};
return logEntry;
}
// Handle standard Error objects
if (error instanceof Error) {
const logEntry: ErrorLogEntry = {
timestamp,
level: "error",
message: error.message,
error: {
code: "UNKNOWN_ERROR",
message: error.message,
details: {
errorName: error.name,
originalError: error,
},
},
stack: error.stack,
...(context ? { context } : {}),
};
return logEntry;
}
// Handle unknown error types
const errorString = String(error);
const logEntry: ErrorLogEntry = {
timestamp,
level: "error",
message: errorString,
error: {
code: "UNKNOWN_ERROR",
message: errorString,
details: {
errorType: typeof error,
errorValue: error,
},
},
...(context ? { context } : {}),
};
return logEntry;
}
/**
* Format error message for logging
*
* @param entry - Error log entry
* @returns Formatted error message
*/
export function formatErrorMessage(entry: ErrorLogEntry): string {
const parts = [
`[${entry.error.code}]`,
entry.message,
];
if (entry.error.status) {
parts.push(`(HTTP ${entry.error.status})`);
}
if (entry.context?.operation) {
parts.push(`[${entry.context.operation}]`);
}
if (entry.context?.requestId) {
parts.push(`[Request: ${entry.context.requestId}]`);
}
return parts.join(" ");
}
/**
* Get appropriate log level based on error type
*
* @param error - KrapiError instance
* @returns Appropriate log level
*/
export function getLogLevelFromError(error: KrapiError): LogLevel {
// Server errors should be logged as errors
if (error.status && error.status >= 500) {
return "error";
}
// Client errors (4xx) are often expected and can be logged as warnings
if (error.status && error.status >= 400 && error.status < 500) {
// But some client errors like validation errors should be warnings
if (error.code === "VALIDATION_ERROR" || error.code === "BAD_REQUEST") {
return "warn";
}
return "error";
}
// Authentication errors
if (error.code === "UNAUTHORIZED" || error.code === "FORBIDDEN") {
return "warn"; // Often expected in normal operation
}
// Not found errors
if (error.code === "NOT_FOUND") {
return "info"; // Often expected (checking if resource exists)
}
// Default to error level
return "error";
}
/**
* Log error with operation context
*
* Convenience function for logging errors with operation-specific context.
*
* @param logger - Logger instance
* @param error - Error to log
* @param operation - Operation name
* @param additionalContext - Additional context
*/
export function logOperationError(
logger: Logger,
error: unknown,
operation: string,
additionalContext?: Omit<ErrorLogContext, "operation">
): void {
logError(logger, error, {
operation,
...additionalContext,
});
}
/**
* Log service-level errors with consistent formatting
*
* @param logger - Logger instance
* @param error - Error to log
* @param service - Service name
* @param operation - Operation name
* @param context - Additional context
*/
export function logServiceError(
logger: Logger,
error: unknown,
service: string,
operation: string,
context?: ErrorLogContext
): void {
const enhancedContext = {
service,
operation,
...context,
};
logError(logger, error, enhancedContext);
}
/**
* Log HTTP client errors with comprehensive request/response context
*
* Automatically extracts all HTTP context from HttpError instances.
*
* @param logger - Logger instance
* @param error - HTTP error
* @param method - HTTP method (if not in error)
* @param url - Request URL (if not in error)
* @param context - Additional context
*/
export function logHttpError(
logger: Logger,
error: unknown,
method?: string,
url?: string,
context?: ErrorLogContext
): void {
// If error is HttpError, it already contains all HTTP context
// Just add any additional context provided
const enhancedContext: ErrorLogContext = {
...(context || {}),
...(error instanceof HttpError ? {} : {
...(method ? { httpMethod: method } : {}),
...(url ? { httpUrl: url } : {}),
}),
};
logError(logger, error, enhancedContext);
}
/**
* Log service operation errors with comprehensive context
*
* This is the main function services should use for error logging.
* It captures:
* - Operation name and service
* - Input parameters (what data was received)
* - Request data (if HTTP operation)
* - Response data (if available)
* - Full error details
* - Stack traces
*
* @param logger - Logger instance
* @param error - Error to log
* @param service - Service name (e.g., "ProjectsService", "AuthService")
* @param operation - Operation name (e.g., "createProject", "login")
* @param inputData - Input parameters/data that was received
* @param additionalContext - Additional context (userId, projectId, etc.)
*/
export function logServiceOperationError(
logger: Logger,
error: unknown,
service: string,
operation: string,
inputData?: Record<string, unknown>,
additionalContext?: Omit<ErrorLogContext, "service" | "operation" | "inputData">
): void {
const enhancedContext: ErrorLogContext = {
service,
operation,
inputData: inputData || {},
...(additionalContext || {}),
};
logError(logger, error, enhancedContext);
}
/**
* Create a child logger with error context
*
* Returns a logger function that automatically includes context in all logs.
*
* @param parentLogger - Parent logger
* @param context - Context to include in all logs
* @returns Contextualized logger
*/
export function createContextLogger(
parentLogger: Logger,
context: ErrorLogContext
): Logger {
return {
error: (message: string, ...args: unknown[]) => {
const enhancedArgs = args.map(arg => {
if (arg && typeof arg === "object" && !(arg instanceof Error)) {
return { ...context, ...arg };
}
return arg;
});
parentLogger.error(message, ...enhancedArgs);
},
warn: (message: string, ...args: unknown[]) => {
const enhancedArgs = args.map(arg => {
if (arg && typeof arg === "object" && !(arg instanceof Error)) {
return { ...context, ...arg };
}
return arg;
});
parentLogger.warn(message, ...enhancedArgs);
},
info: (message: string, ...args: unknown[]) => {
const enhancedArgs = args.map(arg => {
if (arg && typeof arg === "object" && !(arg instanceof Error)) {
return { ...context, ...arg };
}
return arg;
});
parentLogger.info(message, ...enhancedArgs);
},
debug: (message: string, ...args: unknown[]) => {
const enhancedArgs = args.map(arg => {
if (arg && typeof arg === "object" && !(arg instanceof Error)) {
return { ...context, ...arg };
}
return arg;
});
parentLogger.debug(message, ...enhancedArgs);
},
};
}