408 lines
9.9 KiB
TypeScript
408 lines
9.9 KiB
TypeScript
import { readFileSync } from 'fs';
|
|
import type {
|
|
ElasticsearchConfig,
|
|
AuthConfig,
|
|
SecretProvider,
|
|
} from './types.js';
|
|
import {
|
|
ConfigValidationError,
|
|
EnvironmentSecretProvider,
|
|
} from './types.js';
|
|
import { LogLevel } from '../observability/logger.js';
|
|
|
|
/**
|
|
* Configuration builder for fluent Elasticsearch configuration
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const config = new ConfigurationBuilder()
|
|
* .nodes(['http://localhost:9200', 'http://localhost:9201'])
|
|
* .auth({ type: 'basic', username: 'elastic', password: 'changeme' })
|
|
* .timeout(30000)
|
|
* .retries(3)
|
|
* .build();
|
|
* ```
|
|
*/
|
|
export class ConfigurationBuilder {
|
|
private config: Partial<ElasticsearchConfig> = {};
|
|
private secretProvider?: SecretProvider;
|
|
|
|
/**
|
|
* Set Elasticsearch node(s)
|
|
*/
|
|
nodes(nodes: string | string[]): this {
|
|
this.config.nodes = nodes;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set authentication configuration
|
|
*/
|
|
auth(auth: AuthConfig): this {
|
|
this.config.auth = auth;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set basic authentication
|
|
*/
|
|
basicAuth(username: string, password: string): this {
|
|
this.config.auth = { type: 'basic', username, password };
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set API key authentication
|
|
*/
|
|
apiKeyAuth(apiKey: string): this {
|
|
this.config.auth = { type: 'apiKey', apiKey };
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set bearer token authentication
|
|
*/
|
|
bearerAuth(token: string): this {
|
|
this.config.auth = { type: 'bearer', token };
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set cloud ID authentication
|
|
*/
|
|
cloudAuth(id: string, options?: { username?: string; password?: string; apiKey?: string }): this {
|
|
this.config.auth = {
|
|
type: 'cloud',
|
|
id,
|
|
...options,
|
|
};
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set request timeout
|
|
*/
|
|
timeout(timeoutMs: number): this {
|
|
if (!this.config.request) {
|
|
this.config.request = {};
|
|
}
|
|
this.config.request.timeout = timeoutMs;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set maximum retries
|
|
*/
|
|
retries(maxRetries: number): this {
|
|
if (!this.config.request) {
|
|
this.config.request = {};
|
|
}
|
|
this.config.request.maxRetries = maxRetries;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enable compression
|
|
*/
|
|
compression(enabled: boolean = true): this {
|
|
if (!this.config.request) {
|
|
this.config.request = {};
|
|
}
|
|
this.config.request.compression = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set connection pool size
|
|
*/
|
|
poolSize(max: number, min?: number): this {
|
|
if (!this.config.pool) {
|
|
this.config.pool = {};
|
|
}
|
|
this.config.pool.maxConnections = max;
|
|
if (min !== undefined) {
|
|
this.config.pool.minIdleConnections = min;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enable node discovery/sniffing
|
|
*/
|
|
discovery(enabled: boolean = true, options?: { interval?: number }): this {
|
|
this.config.discovery = {
|
|
enabled,
|
|
...options,
|
|
};
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set log level
|
|
*/
|
|
logLevel(level: LogLevel): this {
|
|
if (!this.config.logging) {
|
|
this.config.logging = {};
|
|
}
|
|
this.config.logging.level = level;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enable request/response logging
|
|
*/
|
|
enableRequestLogging(enabled: boolean = true): this {
|
|
if (!this.config.logging) {
|
|
this.config.logging = {};
|
|
}
|
|
this.config.logging.enableRequestLogging = enabled;
|
|
this.config.logging.enableResponseLogging = enabled;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enable metrics collection
|
|
*/
|
|
enableMetrics(enabled: boolean = true, prefix?: string): this {
|
|
this.config.metrics = {
|
|
enabled,
|
|
...(prefix && { prefix }),
|
|
};
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Enable tracing
|
|
*/
|
|
enableTracing(enabled: boolean = true, options?: { serviceName?: string; serviceVersion?: string }): this {
|
|
this.config.tracing = {
|
|
enabled,
|
|
...options,
|
|
};
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set proxy URL
|
|
*/
|
|
proxy(proxyUrl: string): this {
|
|
this.config.proxy = proxyUrl;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Load configuration from environment variables
|
|
*
|
|
* Supported environment variables:
|
|
* - ELASTICSEARCH_URL or ELASTICSEARCH_NODES (comma-separated)
|
|
* - ELASTICSEARCH_USERNAME
|
|
* - ELASTICSEARCH_PASSWORD
|
|
* - ELASTICSEARCH_API_KEY
|
|
* - ELASTICSEARCH_CLOUD_ID
|
|
* - ELASTICSEARCH_TIMEOUT
|
|
* - ELASTICSEARCH_MAX_RETRIES
|
|
* - ELASTICSEARCH_LOG_LEVEL
|
|
* - ELASTICSEARCH_PROXY
|
|
*/
|
|
fromEnv(): this {
|
|
// Nodes
|
|
const url = process.env.ELASTICSEARCH_URL;
|
|
const nodes = process.env.ELASTICSEARCH_NODES;
|
|
if (url) {
|
|
this.config.nodes = url;
|
|
} else if (nodes) {
|
|
this.config.nodes = nodes.split(',').map((n) => n.trim());
|
|
}
|
|
|
|
// Authentication
|
|
const apiKey = process.env.ELASTICSEARCH_API_KEY;
|
|
const username = process.env.ELASTICSEARCH_USERNAME;
|
|
const password = process.env.ELASTICSEARCH_PASSWORD;
|
|
const cloudId = process.env.ELASTICSEARCH_CLOUD_ID;
|
|
|
|
if (apiKey) {
|
|
this.apiKeyAuth(apiKey);
|
|
} else if (cloudId) {
|
|
this.cloudAuth(cloudId, { username, password, apiKey });
|
|
} else if (username && password) {
|
|
this.basicAuth(username, password);
|
|
}
|
|
|
|
// Request settings
|
|
const timeout = process.env.ELASTICSEARCH_TIMEOUT;
|
|
if (timeout) {
|
|
this.timeout(parseInt(timeout, 10));
|
|
}
|
|
|
|
const maxRetries = process.env.ELASTICSEARCH_MAX_RETRIES;
|
|
if (maxRetries) {
|
|
this.retries(parseInt(maxRetries, 10));
|
|
}
|
|
|
|
// Logging
|
|
const logLevel = process.env.ELASTICSEARCH_LOG_LEVEL as LogLevel;
|
|
if (logLevel && Object.values(LogLevel).includes(logLevel)) {
|
|
this.logLevel(logLevel);
|
|
}
|
|
|
|
// Proxy
|
|
const proxy = process.env.ELASTICSEARCH_PROXY;
|
|
if (proxy) {
|
|
this.proxy(proxy);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Load configuration from JSON file
|
|
*/
|
|
fromFile(filePath: string): this {
|
|
try {
|
|
const fileContent = readFileSync(filePath, 'utf-8');
|
|
const fileConfig = JSON.parse(fileContent) as Partial<ElasticsearchConfig>;
|
|
this.fromObject(fileConfig);
|
|
} catch (error) {
|
|
throw new ConfigValidationError(
|
|
'file',
|
|
`Failed to load configuration from file: ${(error as Error).message}`
|
|
);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Load configuration from object
|
|
*/
|
|
fromObject(configObject: Partial<ElasticsearchConfig>): this {
|
|
// Merge the object into current config
|
|
this.config = {
|
|
...this.config,
|
|
...configObject,
|
|
};
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set secret provider for fetching credentials
|
|
*/
|
|
withSecrets(provider: SecretProvider): this {
|
|
this.secretProvider = provider;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Resolve secrets using the configured secret provider
|
|
*/
|
|
private async resolveSecrets(): Promise<void> {
|
|
if (!this.secretProvider) {
|
|
this.secretProvider = new EnvironmentSecretProvider();
|
|
}
|
|
|
|
// Resolve authentication secrets
|
|
if (this.config.auth) {
|
|
switch (this.config.auth.type) {
|
|
case 'basic': {
|
|
const usernameSecret = await this.secretProvider.getSecret('ELASTICSEARCH_USERNAME');
|
|
const passwordSecret = await this.secretProvider.getSecret('ELASTICSEARCH_PASSWORD');
|
|
if (usernameSecret) this.config.auth.username = usernameSecret;
|
|
if (passwordSecret) this.config.auth.password = passwordSecret;
|
|
break;
|
|
}
|
|
case 'apiKey': {
|
|
const apiKeySecret = await this.secretProvider.getSecret('ELASTICSEARCH_API_KEY');
|
|
if (apiKeySecret) this.config.auth.apiKey = apiKeySecret;
|
|
break;
|
|
}
|
|
case 'bearer': {
|
|
const tokenSecret = await this.secretProvider.getSecret('ELASTICSEARCH_BEARER_TOKEN');
|
|
if (tokenSecret) this.config.auth.token = tokenSecret;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate the configuration
|
|
*/
|
|
private validate(config: Partial<ElasticsearchConfig>): ElasticsearchConfig {
|
|
// Required fields
|
|
if (!config.nodes) {
|
|
throw new ConfigValidationError('nodes', 'Elasticsearch node(s) must be specified');
|
|
}
|
|
|
|
// Normalize nodes to array
|
|
const nodes = Array.isArray(config.nodes) ? config.nodes : [config.nodes];
|
|
|
|
if (nodes.length === 0) {
|
|
throw new ConfigValidationError('nodes', 'At least one Elasticsearch node must be specified');
|
|
}
|
|
|
|
// Validate node URLs
|
|
for (const node of nodes) {
|
|
try {
|
|
new URL(node);
|
|
} catch {
|
|
throw new ConfigValidationError(
|
|
'nodes',
|
|
`Invalid node URL: ${node}. Must be a valid HTTP(S) URL`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate timeout
|
|
if (config.request?.timeout !== undefined && config.request.timeout <= 0) {
|
|
throw new ConfigValidationError('request.timeout', 'Timeout must be a positive number');
|
|
}
|
|
|
|
// Validate retries
|
|
if (config.request?.maxRetries !== undefined && config.request.maxRetries < 0) {
|
|
throw new ConfigValidationError('request.maxRetries', 'Max retries cannot be negative');
|
|
}
|
|
|
|
// Validate pool size
|
|
if (config.pool?.maxConnections !== undefined && config.pool.maxConnections <= 0) {
|
|
throw new ConfigValidationError('pool.maxConnections', 'Max connections must be positive');
|
|
}
|
|
|
|
if (
|
|
config.pool?.minIdleConnections !== undefined &&
|
|
config.pool?.maxConnections !== undefined &&
|
|
config.pool.minIdleConnections > config.pool.maxConnections
|
|
) {
|
|
throw new ConfigValidationError(
|
|
'pool.minIdleConnections',
|
|
'Min idle connections cannot exceed max connections'
|
|
);
|
|
}
|
|
|
|
return {
|
|
nodes,
|
|
...config,
|
|
} as ElasticsearchConfig;
|
|
}
|
|
|
|
/**
|
|
* Build and validate the configuration
|
|
*/
|
|
async buildAsync(): Promise<ElasticsearchConfig> {
|
|
await this.resolveSecrets();
|
|
return this.validate(this.config);
|
|
}
|
|
|
|
/**
|
|
* Build and validate the configuration (synchronous)
|
|
*/
|
|
build(): ElasticsearchConfig {
|
|
return this.validate(this.config);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new configuration builder
|
|
*/
|
|
export function createConfig(): ConfigurationBuilder {
|
|
return new ConfigurationBuilder();
|
|
}
|