Files
elasticsearch/ts/core/config/configuration-builder.ts

408 lines
9.9 KiB
TypeScript
Raw Normal View History

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();
}