feat(logging): Add structured Logger and integrate into Smarts3Server; pass full config to server

This commit is contained in:
2025-11-23 22:37:32 +00:00
parent a7bc902dd0
commit 7f3de92961
5 changed files with 547 additions and 29 deletions

View File

@@ -4,9 +4,11 @@ import { MiddlewareStack } from './middleware-stack.js';
import { S3Context } from './context.js';
import { FilesystemStore } from './filesystem-store.js';
import { S3Error } from './s3-error.js';
import { Logger } from './logger.js';
import { ServiceController } from '../controllers/service.controller.js';
import { BucketController } from '../controllers/bucket.controller.js';
import { ObjectController } from '../controllers/object.controller.js';
import type { ISmarts3Config } from '../index.js';
export interface ISmarts3ServerOptions {
port?: number;
@@ -14,6 +16,7 @@ export interface ISmarts3ServerOptions {
directory?: string;
cleanSlate?: boolean;
silent?: boolean;
config?: Required<ISmarts3Config>;
}
/**
@@ -25,18 +28,57 @@ export class Smarts3Server {
private router: S3Router;
private middlewares: MiddlewareStack;
public store: FilesystemStore; // Made public for direct access from Smarts3 class
private options: Required<ISmarts3ServerOptions>;
private options: Required<Omit<ISmarts3ServerOptions, 'config'>>;
private config: Required<ISmarts3Config>;
private logger: Logger;
constructor(options: ISmarts3ServerOptions = {}) {
this.options = {
port: 3000,
address: '0.0.0.0',
directory: plugins.path.join(process.cwd(), '.nogit/bucketsDir'),
cleanSlate: false,
silent: false,
...options,
port: options.port ?? 3000,
address: options.address ?? '0.0.0.0',
directory: options.directory ?? plugins.path.join(process.cwd(), '.nogit/bucketsDir'),
cleanSlate: options.cleanSlate ?? false,
silent: options.silent ?? false,
};
// Store config for middleware and feature configuration
// If no config provided, create minimal default (for backward compatibility)
this.config = options.config ?? {
server: {
port: this.options.port,
address: this.options.address,
silent: this.options.silent,
},
storage: {
directory: this.options.directory,
cleanSlate: this.options.cleanSlate,
},
auth: {
enabled: false,
credentials: [{ accessKeyId: 'S3RVER', secretAccessKey: 'S3RVER' }],
},
cors: {
enabled: false,
allowedOrigins: ['*'],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'],
allowedHeaders: ['*'],
exposedHeaders: ['ETag', 'x-amz-request-id', 'x-amz-version-id'],
maxAge: 86400,
allowCredentials: false,
},
logging: {
level: 'info',
format: 'text',
enabled: true,
},
limits: {
maxObjectSize: 5 * 1024 * 1024 * 1024,
maxMetadataSize: 2048,
requestTimeout: 300000,
},
};
this.logger = new Logger(this.config.logging);
this.store = new FilesystemStore(this.options.directory);
this.router = new S3Router();
this.middlewares = new MiddlewareStack();
@@ -49,20 +91,118 @@ export class Smarts3Server {
* Setup middleware stack
*/
private setupMiddlewares(): void {
// Logger middleware
if (!this.options.silent) {
// CORS middleware (must be first to handle preflight requests)
if (this.config.cors.enabled) {
this.middlewares.use(async (req, res, ctx, next) => {
const start = Date.now();
console.log(`${req.method} ${req.url}`);
console.log(` Headers:`, JSON.stringify(req.headers, null, 2).slice(0, 200));
const origin = req.headers.origin || req.headers.referer;
// Check if origin is allowed
const allowedOrigins = this.config.cors.allowedOrigins || ['*'];
const isOriginAllowed =
allowedOrigins.includes('*') ||
(origin && allowedOrigins.includes(origin));
if (isOriginAllowed) {
// Set CORS headers
res.setHeader(
'Access-Control-Allow-Origin',
allowedOrigins.includes('*') ? '*' : origin || '*'
);
if (this.config.cors.allowCredentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
// Handle preflight OPTIONS request
if (req.method === 'OPTIONS') {
res.setHeader(
'Access-Control-Allow-Methods',
(this.config.cors.allowedMethods || []).join(', ')
);
res.setHeader(
'Access-Control-Allow-Headers',
(this.config.cors.allowedHeaders || []).join(', ')
);
if (this.config.cors.maxAge) {
res.setHeader(
'Access-Control-Max-Age',
String(this.config.cors.maxAge)
);
}
res.writeHead(204);
res.end();
return; // Don't call next() for OPTIONS
}
// Set exposed headers for actual requests
if (this.config.cors.exposedHeaders && this.config.cors.exposedHeaders.length > 0) {
res.setHeader(
'Access-Control-Expose-Headers',
this.config.cors.exposedHeaders.join(', ')
);
}
}
await next();
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
});
}
// TODO: Add authentication middleware
// TODO: Add CORS middleware
// Authentication middleware (simple static credentials)
if (this.config.auth.enabled) {
this.middlewares.use(async (req, res, ctx, next) => {
const authHeader = req.headers.authorization;
// Extract access key from Authorization header
let accessKeyId: string | undefined;
if (authHeader) {
// Support multiple auth formats:
// 1. AWS accessKeyId:signature
// 2. AWS4-HMAC-SHA256 Credential=accessKeyId/date/region/service/aws4_request, ...
if (authHeader.startsWith('AWS ')) {
accessKeyId = authHeader.substring(4).split(':')[0];
} else if (authHeader.startsWith('AWS4-HMAC-SHA256')) {
const credentialMatch = authHeader.match(/Credential=([^/]+)\//);
accessKeyId = credentialMatch ? credentialMatch[1] : undefined;
}
}
// Check if access key is valid
const isValid = this.config.auth.credentials.some(
(cred) => cred.accessKeyId === accessKeyId
);
if (!isValid) {
ctx.throw('AccessDenied', 'Access Denied');
return;
}
await next();
});
}
// Logger middleware
if (!this.options.silent && this.config.logging.enabled) {
this.middlewares.use(async (req, res, ctx, next) => {
const start = Date.now();
// Log request
this.logger.request(req.method || 'UNKNOWN', req.url || '/', {
headers: req.headers,
});
await next();
// Log response
const duration = Date.now() - start;
this.logger.response(
req.method || 'UNKNOWN',
req.url || '/',
res.statusCode || 500,
duration
);
});
}
}
/**
@@ -122,11 +262,14 @@ export class Smarts3Server {
): Promise<void> {
const s3Error = err instanceof S3Error ? err : S3Error.fromError(err);
if (!this.options.silent) {
console.error(`[S3Error] ${s3Error.code}: ${s3Error.message}`);
if (s3Error.status >= 500) {
console.error(err.stack || err);
}
// Log the error
this.logger.s3Error(s3Error.code, s3Error.message, s3Error.status);
// Log stack trace for server errors
if (s3Error.status >= 500) {
this.logger.debug('Error stack trace', {
stack: err.stack || err.toString(),
});
}
// Send error response
@@ -155,7 +298,10 @@ export class Smarts3Server {
// Create HTTP server
this.httpServer = plugins.http.createServer((req, res) => {
this.handleRequest(req, res).catch((err) => {
console.error('Fatal error in request handler:', err);
this.logger.error('Fatal error in request handler', {
error: err.message,
stack: err.stack,
});
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal Server Error');
@@ -169,9 +315,7 @@ export class Smarts3Server {
if (err) {
reject(err);
} else {
if (!this.options.silent) {
console.log(`S3 server listening on ${this.options.address}:${this.options.port}`);
}
this.logger.info(`S3 server listening on ${this.options.address}:${this.options.port}`);
resolve();
}
});
@@ -191,9 +335,7 @@ export class Smarts3Server {
if (err) {
reject(err);
} else {
if (!this.options.silent) {
console.log('S3 server stopped');
}
this.logger.info('S3 server stopped');
resolve();
}
});