"use strict";
/*
 * Copyright The OpenTelemetry Authors
 * Copyright (c) 2025, Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getOracleTelemetryTraceHandlerClass = void 0;
const instrumentation_1 = require("@opentelemetry/instrumentation");
const api_1 = require("@opentelemetry/api");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const semconv_1 = require("./semconv");
const OUT_BIND = 3003; // bindinfo direction value.
const constants_1 = require("./constants");
// It dynamically retrieves the TraceHandlerBase class from the oracledb module
// (if available) while avoiding direct imports that could cause issues if
// the module is missing.
function getTraceHandlerBaseClass(obj) {
    try {
        return obj.traceHandler.TraceHandlerBase;
    }
    catch (err) {
        api_1.diag.error('Failed to load oracledb module.', err);
        return null;
    }
}
function getOracleTelemetryTraceHandlerClass(obj) {
    const traceHandlerBase = getTraceHandlerBaseClass(obj);
    if (!traceHandlerBase) {
        return undefined;
    }
    /**
     * OracleTelemetryTraceHandler extends TraceHandlerBase from oracledb module
     * It implements the abstract methods; `onEnterFn`, `onExitFn`,
     * `onBeginRoundTrip` and `onEndRoundTrip` of TraceHandlerBase class.
     * Inside these overridden methods, the input traceContext data is used
     * to generate attributes for span.
     */
    class OracleTelemetryTraceHandler extends traceHandlerBase {
        _getTracer;
        _instrumentConfig;
        constructor(getTracer, config) {
            super();
            this._getTracer = getTracer;
            this._instrumentConfig = config;
        }
        _shouldSkipInstrumentation() {
            return (this._instrumentConfig.requireParentSpan === true &&
                api_1.trace.getSpan(api_1.context.active()) === undefined);
        }
        // It returns db.namespace as mentioned in semantic conventions
        // Ex: ORCL1|PDB1|db_high.adb.oraclecloud.com
        _getDBNameSpace(instanceName, pdbName, serviceName) {
            if (instanceName == null && pdbName == null && serviceName == null) {
                return undefined;
            }
            return `${instanceName ?? ''}|${pdbName ?? ''}|${serviceName ?? ''}`;
        }
        // Returns the connection related Attributes for
        // semantic standards and module custom keys.
        _getConnectionSpanAttributes(config) {
            return {
                [semantic_conventions_1.ATTR_DB_SYSTEM_NAME]: semconv_1.DB_SYSTEM_NAME_VALUE_ORACLE_DB,
                [semantic_conventions_1.ATTR_NETWORK_TRANSPORT]: config.protocol,
                [semconv_1.ATTR_DB_USER]: config.user,
                [semantic_conventions_1.ATTR_DB_NAMESPACE]: this._getDBNameSpace(config.instanceName, config.pdbName, config.serviceName),
                [semantic_conventions_1.ATTR_SERVER_ADDRESS]: config.hostName,
                [semantic_conventions_1.ATTR_SERVER_PORT]: config.port,
            };
        }
        // It returns true if object is of type oracledb.Lob.
        _isLobInstance(obj) {
            return (typeof obj === 'object' &&
                obj !== null &&
                Reflect.getPrototypeOf(obj)?.constructor?.name === 'Lob');
        }
        // Transforms the bind values array or bindinfo into an object
        // 'db.operation.parameter'.
        // Ex:
        //   db.operation.parameter.0 = "someval" // for bind by position
        //   db.operation.parameter.name = "someval" // for bind by name
        // It is only called if config 'enhancedDatabaseReporting' is true.
        _getValues(values) {
            if (!values)
                return undefined;
            const convertedValues = {};
            try {
                if (Array.isArray(values)) {
                    // Handle indexed (positional) parameters
                    values.forEach((value, index) => {
                        const extractedValue = this._extractValue(value);
                        if (extractedValue !== undefined) {
                            convertedValues[(0, semconv_1.ATTR_DB_OPERATION_PARAMETER)(`${index}`)] =
                                extractedValue;
                        }
                    });
                }
                else if (values && typeof values === 'object') {
                    // Handle named parameters
                    for (const [paramName, value] of Object.entries(values)) {
                        const key = (0, semconv_1.ATTR_DB_OPERATION_PARAMETER)(paramName);
                        let inVal = value;
                        if (inVal && typeof inVal === 'object') {
                            // Check bind info if present.
                            if (inVal.dir === OUT_BIND) {
                                // outbinds
                                convertedValues[key] = '';
                                continue;
                            }
                            if ('val' in inVal) {
                                inVal = inVal.val;
                            }
                        }
                        const extractedValue = this._extractValue(inVal);
                        if (extractedValue !== undefined) {
                            convertedValues[key] = extractedValue;
                        }
                    }
                }
            }
            catch (e) {
                api_1.diag.error('failed to stringify bind values:', values, e);
                return undefined;
            }
            return convertedValues;
        }
        _extractValue(value) {
            if (value == null) {
                return 'null';
            }
            if (value instanceof Buffer || this._isLobInstance(value)) {
                return value.toString();
            }
            if (typeof value === 'object') {
                return JSON.stringify(value);
            }
            return value.toString();
        }
        // Updates the call level attributes in span.
        // roundTrip flag will skip dumping bind values for
        // internal roundtrip spans generated for oracledb exported functions.
        _setCallLevelAttributes(span, callConfig, roundTrip = false) {
            if (!callConfig)
                return;
            if (callConfig.statement) {
                span.setAttribute(semantic_conventions_1.ATTR_DB_OPERATION_NAME, 
                // retrieve just the first word
                callConfig.statement.split(' ')[0].toUpperCase());
                if (this._instrumentConfig.dbStatementDump ||
                    this._instrumentConfig.enhancedDatabaseReporting) {
                    span.setAttribute(semantic_conventions_1.ATTR_DB_QUERY_TEXT, callConfig.statement);
                    if (this._instrumentConfig.enhancedDatabaseReporting && !roundTrip) {
                        const values = this._getValues(callConfig.values);
                        if (values) {
                            span.setAttributes(values);
                        }
                    }
                }
            }
        }
        _handleExecuteCustomRequest(span, traceContext) {
            if (typeof this._instrumentConfig.requestHook === 'function') {
                (0, instrumentation_1.safeExecuteInTheMiddle)(() => {
                    this._instrumentConfig.requestHook?.(span, {
                        connection: traceContext.connectLevelConfig,
                        inputArgs: traceContext.additionalConfig.args,
                    });
                }, err => {
                    if (err) {
                        api_1.diag.error('Error running request hook', err);
                    }
                }, true);
            }
        }
        _handleExecuteCustomResult(span, traceContext) {
            if (typeof this._instrumentConfig.responseHook === 'function') {
                (0, instrumentation_1.safeExecuteInTheMiddle)(() => {
                    this._instrumentConfig.responseHook?.(span, {
                        data: traceContext.additionalConfig.result,
                    });
                }, err => {
                    if (err) {
                        api_1.diag.error('Error running query hook', err);
                    }
                }, true);
            }
        }
        // Updates the spanName following the format
        // {FunctionName:[sqlCommand] db.namespace}
        // Ex: 'oracledb.Pool.getConnection:[SELECT] ORCL1|PDB1|db_high.adb.oraclecloud.com'
        // This function is called when connectLevelConfig has required parameters populated.
        _updateSpanName(traceContext) {
            const { connectLevelConfig, callLevelConfig, userContext, operation } = traceContext;
            if (![
                constants_1.SpanNames.EXECUTE,
                constants_1.SpanNames.EXECUTE_MANY,
                constants_1.SpanNames.EXECUTE_MSG,
            ].includes(operation)) {
                // Ignore for connection establishment functions.
                return;
            }
            const { instanceName, pdbName, serviceName } = connectLevelConfig;
            const dbName = this._getDBNameSpace(instanceName, pdbName, serviceName);
            const sqlCommand = callLevelConfig?.statement?.split(' ')[0].toUpperCase() || '';
            userContext.span.updateName(`${operation}:${sqlCommand}${dbName && ` ${dbName}`}`);
        }
        // Updates the span with final traceContext attributes
        // which are updated after the exported function call.
        // roundTrip flag will skip dumping bind values for
        // internal roundtrip spans generated for exported functions.
        _updateFinalSpanAttributes(traceContext, roundTrip = false) {
            const span = traceContext.userContext.span;
            // Set if additional connection and call parameters
            // are available
            if (traceContext.connectLevelConfig) {
                span.setAttributes(this._getConnectionSpanAttributes(traceContext.connectLevelConfig));
            }
            if (traceContext.callLevelConfig) {
                this._setCallLevelAttributes(span, traceContext.callLevelConfig, roundTrip);
            }
            if (traceContext.error) {
                span.recordException(traceContext.error);
                span.setStatus({
                    code: api_1.SpanStatusCode.ERROR,
                    message: traceContext.error.message,
                });
            }
        }
        setInstrumentConfig(config = {}) {
            this._instrumentConfig = config;
        }
        // This method is invoked before calling an exported function
        // from oracledb module.
        onEnterFn(traceContext) {
            if (this._shouldSkipInstrumentation()) {
                return;
            }
            const spanName = traceContext.operation;
            const spanAttributes = traceContext.connectLevelConfig
                ? this._getConnectionSpanAttributes(traceContext.connectLevelConfig)
                : {};
            traceContext.userContext = {
                span: this._getTracer().startSpan(spanName, {
                    kind: api_1.SpanKind.CLIENT,
                    attributes: spanAttributes,
                }),
            };
            if (traceContext.fn) {
                // wrap the active span context to the exported function.
                traceContext.fn = api_1.context.bind(api_1.trace.setSpan(api_1.context.active(), traceContext.userContext.span), traceContext.fn);
            }
            if (traceContext.operation === constants_1.SpanNames.EXECUTE) {
                this._handleExecuteCustomRequest(traceContext.userContext.span, traceContext);
            }
        }
        // This method is invoked after exported function from oracledb module
        // completes.
        onExitFn(traceContext) {
            if (!traceContext.userContext?.span) {
                return;
            }
            this._updateFinalSpanAttributes(traceContext);
            switch (traceContext.operation) {
                case constants_1.SpanNames.EXECUTE:
                    this._handleExecuteCustomResult(traceContext.userContext.span, traceContext);
                    break;
                default:
                    break;
            }
            this._updateSpanName(traceContext);
            traceContext.userContext.span.end();
        }
        // This method is invoked before a round trip call to DB is done
        // from the oracledb module as part of sql execution.
        onBeginRoundTrip(traceContext) {
            if (this._shouldSkipInstrumentation()) {
                return;
            }
            const spanName = traceContext.operation;
            const spanAttrs = {};
            traceContext.userContext = {
                span: this._getTracer().startSpan(spanName, {
                    kind: api_1.SpanKind.CLIENT,
                    attributes: spanAttrs,
                }),
            };
        }
        // This method is invoked after a round trip call to DB is done
        // from the oracledb module as part of sql execution.
        onEndRoundTrip(traceContext) {
            if (!traceContext.userContext?.span) {
                return;
            }
            // Set if additional connection and call parameters
            // are available
            this._updateFinalSpanAttributes(traceContext, true);
            this._updateSpanName(traceContext);
            traceContext.userContext.span.end();
        }
    }
    return OracleTelemetryTraceHandler;
}
exports.getOracleTelemetryTraceHandlerClass = getOracleTelemetryTraceHandlerClass;
//# sourceMappingURL=OracleTelemetryTraceHandler.js.map