import { AsyncLocalStorage } from "async_hooks";
import ddTrace from "dd-trace";
import { safeJsonStringify } from "../../util/log-sanitizer.js";
import { getErrorMeta, getLogger } from "../../util/logger.js";
/**
 * Lazily initialize dd-trace with LLMObs if not already configured.
 * This is needed when running via turbo (start:all:obs) which bypasses
 * the CLI's configureTelemetry() that normally initializes LLMObs.
 *
 * Called from `get llmobs()` to avoid module-level side effects that
 * create test hazards (any transitive import would trigger dd-trace init).
 */
let _llmobsInitAttempted = false;
function ensureLlmobsInitialized() {
    if (_llmobsInitAttempted)
        return;
    _llmobsInitAttempted = true;
    const shouldEnableLlmobs = (process.env.DD_LLMOBS_ENABLED === "1" ||
        process.env.SUPERBLOCKS_LLMOBS_ENABLED === "true") &&
        process.env.DD_LLMOBS_ML_APP;
    if (shouldEnableLlmobs && !ddTrace.llmobs.enabled) {
        // dd-trace was auto-initialized without LLMObs. Re-init with LLMObs config.
        // Note: dd-trace.init() is idempotent for most settings but will enable LLMObs.
        ddTrace.init({
            // Don't override existing service config if already set
            service: ddTrace._tracer?._config?.service || "superblocks-ai-code-gen",
            // Enable LLMObs with env var config
            llmobs: {
                mlApp: process.env.DD_LLMOBS_ML_APP || "superblocks-ai-code-gen",
                agentlessEnabled: process.env.DD_LLMOBS_AGENTLESS_ENABLED === "1",
            },
        });
    }
}
import { DD_INTERNAL_TAGS } from "./constants.js";
import { llmobsOtelExporter } from "./otel-exporter.js";
import { getCanonicalModel } from "./utils.js";
/** Stack implementation that allows random access removals for span bookkeeping. */
class IndexedStack {
    items = [];
    /** Push a new item onto the stack. */
    push(item) {
        this.items.push(item);
    }
    /** Peek at the item on the top of the stack without removing it. */
    top() {
        return this.items[this.items.length - 1];
    }
    /** Remove a specific item from the stack if present. */
    remove(item) {
        const index = this.items.indexOf(item);
        if (index !== -1) {
            this.items.splice(index, 1);
            return true;
        }
        return false;
    }
    /** The number of items currently tracked by the stack. */
    length() {
        return this.items.length;
    }
}
class State extends IndexedStack {
}
/**
 * Core tracer responsible for LLM observability within the AI service.
 *
 * Provides utilities for executing code within spans, managing span lifecycles,
 * and decorating functions or classes with consistent instrumentation.
 */
export class LLMObsTracer {
    globalState = new State();
    localStorage = new AsyncLocalStorage();
    globalTags = {};
    defaultRootPolicy;
    static _instance = new LLMObsTracer();
    rootSpan;
    constructor({ defaultRootPolicy = "allow", } = {}) {
        this.defaultRootPolicy = defaultRootPolicy;
        // Initialize the async local storage with an empty state.
        this.localStorage.enterWith(new State());
    }
    static get instance() {
        return LLMObsTracer._instance;
    }
    /**
     * Set the default root policy for spans created without an explicit policy.
     * The default is 'allow'.
     * @see LLMObsRootPolicy
     */
    setDefaultRootPolicy(policy) {
        this.defaultRootPolicy = policy;
    }
    /**
     * Execute a function within a traced span context and finish the span
     * automatically once the function resolves.
     */
    trace(options, fn) {
        return this._trace({ finish: "auto", ...options }, fn);
    }
    /**
     * Wrap a function so each invocation runs inside an automatically managed span.
     * When the function lacks a name, the span falls back to the provided kind.
     */
    wrap(options, fn) {
        const name = options.name || fn.name || options.kind;
        const trace = this.trace.bind(this);
        return function (...args) {
            return trace({ ...options, name }, (_span) => fn.apply(this, args));
        };
    }
    startActiveSpan(options) {
        const { activation, ...rest } = options;
        return this._trace({ finish: "manual", activation, ...rest }, (span) => span);
    }
    /**
     * Flush any buffered spans.
     */
    flush() {
        try {
            this.llmobs.flush();
        }
        catch {
            /* swallow error */
        }
    }
    /**
     * Export the supplied span (or the currently active span) into the
     * DataDog-specific wire format for manual submission.
     */
    exportSpan(span) {
        if (span && isNoopSpan(span)) {
            return {
                traceId: "noop",
                spanId: "noop",
            };
        }
        if (!this.llmobs.enabled) {
            return {
                traceId: "noop",
                spanId: "noop",
            };
        }
        return this.llmobs.exportSpan(span || this.active());
    }
    annotate(spanOrAnnotations, maybeAnnotations) {
        if (!this.llmobs.enabled) {
            return;
        }
        const annotations = maybeAnnotations === undefined
            ? spanOrAnnotations
            : maybeAnnotations;
        const targetSpan = maybeAnnotations === undefined
            ? this.active()
            : spanOrAnnotations;
        const { tags, metadata, model, baseAnnotations } = LLMObsTracer.normalizeAnnotationPayload(targetSpan, annotations);
        if (model && targetSpan) {
            this.setModel(targetSpan, model.name, model.provider);
        }
        const payload = Object.keys(metadata).length
            ? { tags, metadata, ...baseAnnotations }
            : { tags, ...baseAnnotations };
        try {
            return this.llmobs.annotate(targetSpan, payload);
        }
        catch {
            /* swallow error */
        }
    }
    /** Submit feedback or evaluation metrics for a completed span. */
    submitEvaluation(spanContext, option) {
        return this.llmobs.submitEvaluation(spanContext, option);
    }
    /**
     * Create a decorator factory that wraps functions inside traced spans using
     * the provided options as defaults.
     */
    decorate(options) {
        return (target) => {
            return this.wrap(options, target);
        };
    }
    /** Retrieve the currently active span, falling back to global state. */
    active() {
        return this.localState?.top() || this.globalState.top();
    }
    /**
     * Merge globally-applied tags into those sent with every span that this tracer
     * creates. Later tags override earlier ones when keys overlap.
     */
    setGlobalTags(tags) {
        this.globalTags = { ...this.globalTags, ...tags };
    }
    /**
     * Register the span with local/global state, override its finish behaviour,
     * and apply global tags so nested spans maintain proper hierarchy.
     */
    register(span, finish, activation) {
        this.annotate(span, { tags: this.globalTags });
        const originalFinish = span.finish.bind(span);
        if (finish === "manual") {
            span.__isManualSpan = true;
        }
        span.finish = (finishTime) => {
            this.localState?.remove(span);
            this.globalState.remove(span);
            try {
                return originalFinish(finishTime);
            }
            catch {
                /* swallow error */
            }
        };
        const localState = this.localState;
        if (localState) {
            localState.push(span);
        }
        if (!localState || activation === "global") {
            this.globalState.push(span);
        }
    }
    /**
     * Shared tracing implementation used by both auto-finishing and manual spans.
     * Handles stack setup, parent alignment, and execution of the caller callback.
     */
    _trace(options, fn) {
        if (!this.llmobs.enabled) {
            // Tracing is disabled; execute the function without span management.
            getLogger().debug("llmobs tracing is disabled");
            const noopSpan = LLMObsTracer.createNoopSpan();
            return fn(noopSpan);
        }
        const { tags, finish, activation = "local", rootPolicy = this.defaultRootPolicy, ...rest } = options;
        const parentContext = this.active();
        if (!parentContext && rootPolicy === "drop-if-no-parent") {
            const noopSpan = LLMObsTracer.createNoopSpan();
            if (finish === "auto") {
                return this.localStorage.run(new State(), () => fn(noopSpan));
            }
            return fn(noopSpan);
        }
        const setup = (span, parent) => {
            this.register(span, finish, activation);
            this.annotate(span, { tags });
            if (!parent || parent === span) {
                return;
            }
            if (LLMObsTracer.isManualSpan(span) ||
                LLMObsTracer.isManualSpan(parent)) {
                LLMObsTracer.alignManualChildTrace(span, parent);
            }
        };
        return this.activate(parentContext, () => {
            if (finish === "auto") {
                return this.llmobs.trace(rest, (span) => {
                    return this.localStorage.run(new State(), () => {
                        setup(span, parentContext);
                        return fn(span);
                    });
                });
            }
            else {
                return this.llmobs.trace(rest, (span, _done) => {
                    setup(span, parentContext);
                    return fn(span);
                });
            }
        });
    }
    static createNoopSpan() {
        const context = () => ({
            toTraceId: () => "",
            toSpanId: () => "",
            toTraceparent: () => "",
        });
        const span = {
            finish: () => {
                /* noop */
            },
            context,
        };
        span.__isNoopSpan = true;
        return span;
    }
    /** Activate a span as the current parent context for downstream traces. */
    activate(span, fn) {
        if (!this.llmobs.enabled) {
            // No-op when tracing is disabled as _activate is undefined.
            return fn();
        }
        const _activate = this.llmobs._activate;
        if (_activate === undefined) {
            // fallback
            return fn();
        }
        return this.llmobs._activate(span, null, fn);
    }
    get localState() {
        return this.localStorage.getStore();
    }
    static _llmobs = null;
    static _otelExporterInitialized = false;
    static _isCloudPremCached = null;
    get llmobs() {
        if (!LLMObsTracer._llmobs?.enabled) {
            // Ensure dd-trace is initialized with LLMObs config if needed
            ensureLlmobsInitialized();
            // Lazily access ddTrace.llmobs to avoid triggering dd-trace initialization
            // before ddTrace.init() is called
            LLMObsTracer._llmobs = ddTrace.llmobs;
            // Sync global tags from dd-trace config if available.
            this.setGlobalTags(ddTrace._tracer?._config?.tags);
        }
        // Initialize OTEL exporter for CLOUD_PREM deployments when LLMObs becomes enabled.
        // This patches LLMObsSpanWriter.flush to convert spans to OTEL format instead of
        // sending them directly to Datadog LLMObs API.
        if (LLMObsTracer._llmobs?.enabled &&
            !LLMObsTracer._otelExporterInitialized &&
            LLMObsTracer.isCloudPrem()) {
            // Mark as initialized before async call to prevent duplicate init attempts
            LLMObsTracer._otelExporterInitialized = true;
            llmobsOtelExporter.init().catch((err) => {
                LLMObsTracer._otelExporterInitialized = false;
                getLogger().error("Failed to initialize OTEL exporter for LLMObs", getErrorMeta(err));
            });
        }
        return LLMObsTracer._llmobs;
    }
    /**
     * Check if current deployment is CLOUD_PREM.
     * In CLOUD_PREM, LLMObs spans are converted to OTEL spans instead of
     * being sent directly to Datadog LLMObs API.
     * Result is cached since deployment type is constant for process lifetime.
     */
    static isCloudPrem() {
        if (LLMObsTracer._isCloudPremCached !== null) {
            return LLMObsTracer._isCloudPremCached;
        }
        // First, try to import from SDK (if available as a dependency)
        try {
            // eslint-disable-next-line @typescript-eslint/no-require-imports
            const { isCloudPrem } = require("@superblocksteam/sdk");
            LLMObsTracer._isCloudPremCached = isCloudPrem;
            return isCloudPrem;
        }
        catch (error) {
            // Log non-MODULE_NOT_FOUND errors for debugging
            if (error?.code !== "MODULE_NOT_FOUND") {
                getLogger().debug("Error checking isCloudPrem:", { error });
            }
            // Fallback: check env var directly (for when SDK is not available)
            const deploymentType = process.env.SUPERBLOCKS_DEPLOYMENT_TYPE;
            const isCloudPrem = deploymentType === "cloud-prem";
            LLMObsTracer._isCloudPremCached = isCloudPrem;
            getLogger().debug(`isCloudPrem fallback to env var: SUPERBLOCKS_DEPLOYMENT_TYPE=${deploymentType}, isCloudPrem=${isCloudPrem}`);
            return isCloudPrem;
        }
    }
    setModel(span, modelName, modelProvider) {
        const { provider, modelId } = getCanonicalModel({
            provider: modelProvider,
            modelId: modelName,
        });
        this.setTag(span, DD_INTERNAL_TAGS.MODEL_NAME, modelId);
        this.setTag(span, DD_INTERNAL_TAGS.MODEL_PROVIDER, provider);
    }
    setTag(span, key, value) {
        try {
            this.llmobs._tagger?._setTag(span, key, value);
        }
        catch {
            /* swallow error */
        }
    }
    /**
     * Realign a child span created within a manual parent so it retains the
     * correct trace/parent IDs; mirrors dd-trace's internal behaviour.
     */
    static alignManualChildTrace(child, parent) {
        const childContext = child.context();
        const parentContext = parent.context();
        childContext._parentId = parentContext._spanId;
        const parentTrace = parentContext._trace;
        const childTrace = childContext._trace;
        if (!parentTrace || childTrace === parentTrace) {
            return;
        }
        if (childTrace?.started) {
            const index = childTrace.started.indexOf(child);
            if (index !== -1) {
                childTrace.started.splice(index, 1);
            }
        }
        childContext._trace = parentTrace;
        childContext._traceId = parentContext._traceId;
        if (Array.isArray(parentTrace.started) &&
            parentTrace.started.indexOf(child) === -1) {
            parentTrace.started.push(child);
        }
    }
    /** True when a span was created via {@link startActiveSpan}. */
    static isManualSpan(span) {
        return Boolean(span.__isManualSpan);
    }
    /** Normalize annotation payloads so tags and metadata are consistently shaped. */
    static normalizeAnnotationPayload(span, raw) {
        const { error, tags: rawTags = {}, metadata: rawMetadata = {}, model, ...baseAnnotations } = raw;
        const tags = { ...rawTags };
        const internalContext = span?.context();
        // Merge any existing metadata from span context
        const metadata = {
            ...internalContext?._metadata,
            ...rawMetadata,
        };
        if (internalContext) {
            internalContext._metadata = metadata;
        }
        if (error !== undefined) {
            tags.error = true;
            if (error instanceof Error) {
                Object.assign(tags, {
                    "error.msg": error.message,
                    "error.stack": error.stack,
                    "error.type": error.name,
                });
            }
            else {
                metadata.error = safeJsonStringify(error);
            }
        }
        return { tags, metadata, model, baseAnnotations };
    }
}
function isNoopSpan(span) {
    return Boolean(span.__isNoopSpan);
}
export const tracer = LLMObsTracer.instance;
export default tracer;
//# sourceMappingURL=tracer.js.map