autotel
Version:
Write Once, Observe Anywhere
229 lines (227 loc) • 7.71 kB
JavaScript
import { AdaptiveSampler, AlwaysSampler, NeverSampler, RandomSampler } from "./sampling.js";
import { t as requireModule } from "./node-require-vROmTeJ8.js";
import * as nodeFs from "node:fs";
import path from "node:path";
//#region src/yaml-config.ts
/**
* YAML configuration loader for autotel
*
* Supports:
* - Auto-discovery of autotel.yaml in cwd
* - AUTOTEL_CONFIG_FILE env var override
* - Environment variable substitution: ${env:VAR} and ${env:VAR:-default}
*
* @example Auto-discovery
* ```yaml
* # autotel.yaml in project root
* service:
* name: my-service
* exporter:
* endpoint: ${env:OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}
* ```
*
* @example Explicit path
* ```bash
* AUTOTEL_CONFIG_FILE=./config/otel.yaml tsx --import autotel/auto src/index.ts
* ```
*/
/**
* Lazy-load yaml parser (optional peer dependency)
* Only loads when a YAML config file is actually found
*/
function loadYamlParser() {
try {
return requireModule("yaml").parse;
} catch {
throw new Error("YAML parser not found. Install with: pnpm add yaml");
}
}
/**
* Environment variable substitution regex
* Matches ${env:VAR_NAME} and ${env:VAR_NAME:-default}
*/
const ENV_VAR_PATTERN = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
/**
* Substitute ${env:VAR} and ${env:VAR:-default} in a string
*
* @param value - String potentially containing env var references
* @returns String with env vars substituted
*
* @example
* substituteEnvVars('${env:NODE_ENV:-development}')
* // Returns 'production' if NODE_ENV=production, else 'development'
*/
function substituteEnvVars(value) {
return value.replaceAll(ENV_VAR_PATTERN, (_match, varName, defaultValue) => {
const envValue = process.env[varName];
if (envValue !== void 0) return envValue;
if (defaultValue !== void 0) return defaultValue;
console.warn(`[autotel] Environment variable ${varName} not set and no default provided`);
return "";
});
}
/**
* Recursively substitute env vars in an object
*
* @param obj - Object to process
* @returns Object with all string values having env vars substituted
*/
function substituteEnvVarsDeep(obj) {
if (typeof obj === "string") return substituteEnvVars(obj);
if (Array.isArray(obj)) return obj.map((item) => substituteEnvVarsDeep(item));
if (obj && typeof obj === "object") {
const result = {};
for (const [key, value] of Object.entries(obj)) result[key] = substituteEnvVarsDeep(value);
return result;
}
return obj;
}
/**
* Find YAML config file path
*
* Priority:
* 1. AUTOTEL_CONFIG_FILE env var (explicit path)
* 2. autotel.yaml in cwd (convention)
* 3. autotel.yml in cwd (alternative extension)
*
* @returns File path if found, null otherwise
*/
function findConfigFile() {
const envPath = process.env.AUTOTEL_CONFIG_FILE;
if (envPath) {
const resolved = path.resolve(envPath);
if (nodeFs.existsSync(resolved)) return resolved;
console.warn(`[autotel] Config file not found: ${envPath}`);
return null;
}
const conventionPath = path.resolve(process.cwd(), "autotel.yaml");
if (nodeFs.existsSync(conventionPath)) return conventionPath;
const altPath = path.resolve(process.cwd(), "autotel.yml");
if (nodeFs.existsSync(altPath)) return altPath;
return null;
}
/**
* Convert YAML config structure to AutotelConfig
*
* @param yaml - Parsed and env-substituted YAML config
* @returns Partial AutotelConfig ready for merging
*/
function yamlToAutotelConfig(yaml) {
const config = {};
if (yaml.service?.name) config.service = yaml.service.name;
if (yaml.service?.version) config.version = yaml.service.version;
if (yaml.service?.environment) config.environment = yaml.service.environment;
if (yaml.exporter?.endpoint) config.endpoint = yaml.exporter.endpoint;
if (yaml.exporter?.protocol) config.protocol = yaml.exporter.protocol;
if (yaml.exporter?.headers) config.headers = yaml.exporter.headers;
if (yaml.exporter?.destinations) config.destinations = yaml.exporter.destinations;
if (yaml.resource) config.resourceAttributes = yaml.resource;
if (yaml.autoInstrumentations) config.autoInstrumentations = yaml.autoInstrumentations;
if (yaml.debug !== void 0) config.debug = yaml.debug;
if (yaml.sampling?.preset) {
warnOnIgnoredPresetOverrides(yaml.sampling);
config.sampling = yaml.sampling.preset;
} else {
const sampler = createSamplerFromYaml(yaml.sampling);
if (sampler) config.sampler = sampler;
}
return config;
}
function createSamplerFromYaml(sampling) {
if (!sampling) return void 0;
if (sampling.preset) return void 0;
const type = sampling.type ?? "adaptive";
try {
switch (type) {
case "adaptive": return new AdaptiveSampler({
baselineSampleRate: sampling.baseline_rate,
alwaysSampleErrors: sampling.always_sample_errors,
alwaysSampleSlow: sampling.always_sample_slow,
slowThresholdMs: sampling.slow_threshold_ms
});
case "always_on": return new AlwaysSampler();
case "always_off": return new NeverSampler();
case "ratio":
if (sampling.ratio === void 0) {
console.warn("[autotel] sampling.ratio missing in YAML sampling config. Falling back to adaptive sampler.");
return new AdaptiveSampler();
}
return new RandomSampler(sampling.ratio);
default:
console.warn(`[autotel] Unknown sampling type "${type}" in YAML config. Falling back to defaults.`);
return;
}
} catch (error) {
console.warn(`[autotel] Failed to configure sampling from YAML: ${error instanceof Error ? error.message : String(error)}`);
return;
}
}
function warnOnIgnoredPresetOverrides(sampling) {
const ignoredFields = [
"type",
"ratio",
"baseline_rate",
"always_sample_errors",
"always_sample_slow",
"slow_threshold_ms"
].filter((field) => sampling[field] !== void 0);
if (ignoredFields.length === 0) return;
console.warn(`[autotel] sampling.preset="${sampling.preset}" ignores these YAML fields: ${ignoredFields.join(", ")}. Use the programmatic API with sampler or samplingPresets.*(...) for tuned presets.`);
}
/**
* Load and parse YAML config file (auto-discovery)
*
* Automatically finds and loads autotel.yaml or uses AUTOTEL_CONFIG_FILE.
* Returns null if no config file found (not an error - YAML config is optional).
*
* @returns Partial AutotelConfig or null if no config file found
*
* @example
* const yamlConfig = loadYamlConfig();
* if (yamlConfig) {
* init({ ...yamlConfig, debug: true });
* }
*/
function loadYamlConfig() {
const filePath = findConfigFile();
if (!filePath) return null;
try {
const content = nodeFs.readFileSync(filePath, "utf8");
return yamlToAutotelConfig(substituteEnvVarsDeep(loadYamlParser()(content)));
} catch (error) {
console.error(`[autotel] Failed to load YAML config from ${filePath}:`, error);
return null;
}
}
/**
* Load YAML config from a specific file path
*
* Unlike loadYamlConfig(), this throws if the file cannot be read.
*
* @param filePath - Path to YAML config file
* @returns Partial AutotelConfig
* @throws Error if file cannot be read or parsed
*
* @example
* import { loadYamlConfigFromFile } from 'autotel/yaml';
* import { init } from 'autotel';
*
* const config = loadYamlConfigFromFile('./config/otel.yaml');
* init({ ...config, debug: true });
*/
function loadYamlConfigFromFile(filePath) {
const resolved = path.resolve(filePath);
const content = nodeFs.readFileSync(resolved, "utf8");
return yamlToAutotelConfig(substituteEnvVarsDeep(loadYamlParser()(content)));
}
/**
* Check if a YAML config file exists (without loading it)
*
* @returns true if a config file would be found by loadYamlConfig()
*/
function hasYamlConfig() {
return findConfigFile() !== null;
}
//#endregion
export { hasYamlConfig, loadYamlConfig, loadYamlConfigFromFile };
//# sourceMappingURL=yaml-config.js.map