"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SocketIoInstrumentation = void 0;
/*
 * Copyright The OpenTelemetry Authors
 *
 * 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.
 */
const api_1 = require("@opentelemetry/api");
const instrumentation_1 = require("@opentelemetry/instrumentation");
const semconv_1 = require("./semconv");
const semconv_obsolete_1 = require("./semconv-obsolete");
const AttributeNames_1 = require("./AttributeNames");
/** @knipignore */
const version_1 = require("./version");
const utils_1 = require("./utils");
const reservedEvents = [
    'connect',
    'connect_error',
    'disconnect',
    'disconnecting',
    'newListener',
    'removeListener',
];
class SocketIoInstrumentation extends instrumentation_1.InstrumentationBase {
    constructor(config = {}) {
        super(version_1.PACKAGE_NAME, version_1.PACKAGE_VERSION, (0, utils_1.normalizeConfig)(config));
    }
    init() {
        const socketInstrumentation = new instrumentation_1.InstrumentationNodeModuleFile('socket.io/dist/socket.js', ['>=3 <5'], (moduleExports, moduleVersion) => {
            if (moduleExports === undefined || moduleExports === null) {
                return moduleExports;
            }
            if (moduleVersion === undefined) {
                return moduleExports;
            }
            if ((0, instrumentation_1.isWrapped)(moduleExports?.Socket?.prototype?.on)) {
                this._unwrap(moduleExports.Socket.prototype, 'on');
            }
            this._wrap(moduleExports.Socket.prototype, 'on', this._patchOn(moduleVersion));
            if ((0, instrumentation_1.isWrapped)(moduleExports?.Socket?.prototype?.emit)) {
                this._unwrap(moduleExports.Socket.prototype, 'emit');
            }
            this._wrap(moduleExports.Socket.prototype, 'emit', this._patchEmit(moduleVersion));
            return moduleExports;
        }, moduleExports => {
            if ((0, instrumentation_1.isWrapped)(moduleExports?.Socket?.prototype?.on)) {
                this._unwrap(moduleExports.Socket.prototype, 'on');
            }
            if ((0, instrumentation_1.isWrapped)(moduleExports?.Socket?.prototype?.emit)) {
                this._unwrap(moduleExports.Socket.prototype, 'emit');
            }
            return moduleExports;
        });
        const broadcastOperatorInstrumentation = new instrumentation_1.InstrumentationNodeModuleFile('socket.io/dist/broadcast-operator.js', ['>=4 <5'], (moduleExports, moduleVersion) => {
            if (moduleExports === undefined || moduleExports === null) {
                return moduleExports;
            }
            if (moduleVersion === undefined) {
                return moduleExports;
            }
            if ((0, instrumentation_1.isWrapped)(moduleExports?.BroadcastOperator?.prototype?.emit)) {
                this._unwrap(moduleExports.BroadcastOperator.prototype, 'emit');
            }
            this._wrap(moduleExports.BroadcastOperator.prototype, 'emit', this._patchEmit(moduleVersion));
            return moduleExports;
        }, moduleExports => {
            if ((0, instrumentation_1.isWrapped)(moduleExports?.BroadcastOperator?.prototype?.emit)) {
                this._unwrap(moduleExports.BroadcastOperator.prototype, 'emit');
            }
            return moduleExports;
        });
        const namespaceInstrumentation = new instrumentation_1.InstrumentationNodeModuleFile('socket.io/dist/namespace.js', ['<4'], (moduleExports, moduleVersion) => {
            if (moduleExports === undefined || moduleExports === null) {
                return moduleExports;
            }
            if (moduleVersion === undefined) {
                return moduleExports;
            }
            if ((0, instrumentation_1.isWrapped)(moduleExports?.Namespace?.prototype?.emit)) {
                this._unwrap(moduleExports.Namespace.prototype, 'emit');
            }
            this._wrap(moduleExports.Namespace.prototype, 'emit', this._patchEmit(moduleVersion));
            return moduleExports;
        }, moduleExports => {
            if ((0, instrumentation_1.isWrapped)(moduleExports?.Namespace?.prototype?.emit)) {
                this._unwrap(moduleExports.Namespace.prototype, 'emit');
            }
        });
        const socketInstrumentationLegacy = new instrumentation_1.InstrumentationNodeModuleFile('socket.io/lib/socket.js', ['2'], (moduleExports, moduleVersion) => {
            if (moduleExports === undefined || moduleExports === null) {
                return moduleExports;
            }
            if (moduleVersion === undefined) {
                return moduleExports;
            }
            if ((0, instrumentation_1.isWrapped)(moduleExports.prototype?.on)) {
                this._unwrap(moduleExports.prototype, 'on');
            }
            this._wrap(moduleExports.prototype, 'on', this._patchOn(moduleVersion));
            if ((0, instrumentation_1.isWrapped)(moduleExports.prototype?.emit)) {
                this._unwrap(moduleExports.prototype, 'emit');
            }
            this._wrap(moduleExports.prototype, 'emit', this._patchEmit(moduleVersion));
            return moduleExports;
        }, moduleExports => {
            if ((0, instrumentation_1.isWrapped)(moduleExports.prototype?.on)) {
                this._unwrap(moduleExports.prototype, 'on');
            }
            if ((0, instrumentation_1.isWrapped)(moduleExports.prototype?.emit)) {
                this._unwrap(moduleExports.prototype, 'emit');
            }
            return moduleExports;
        });
        const namespaceInstrumentationLegacy = new instrumentation_1.InstrumentationNodeModuleFile('socket.io/lib/namespace.js', ['2'], (moduleExports, moduleVersion) => {
            if (moduleExports === undefined || moduleExports === null) {
                return moduleExports;
            }
            if (moduleVersion === undefined) {
                return moduleExports;
            }
            if ((0, instrumentation_1.isWrapped)(moduleExports?.prototype?.emit)) {
                this._unwrap(moduleExports.prototype, 'emit');
            }
            this._wrap(moduleExports.prototype, 'emit', this._patchEmit(moduleVersion));
            return moduleExports;
        }, moduleExports => {
            if ((0, instrumentation_1.isWrapped)(moduleExports?.prototype?.emit)) {
                this._unwrap(moduleExports.prototype, 'emit');
            }
        });
        return [
            new instrumentation_1.InstrumentationNodeModuleDefinition('socket.io', ['>=3 <5'], (moduleExports, moduleVersion) => {
                if (moduleExports === undefined || moduleExports === null) {
                    return moduleExports;
                }
                if (moduleVersion === undefined) {
                    return moduleExports;
                }
                if ((0, instrumentation_1.isWrapped)(moduleExports?.Server?.prototype?.on)) {
                    this._unwrap(moduleExports.Server.prototype, 'on');
                }
                this._wrap(moduleExports.Server.prototype, 'on', this._patchOn(moduleVersion));
                return moduleExports;
            }, moduleExports => {
                if ((0, instrumentation_1.isWrapped)(moduleExports?.Server?.prototype?.on)) {
                    this._unwrap(moduleExports.Server.prototype, 'on');
                }
                return moduleExports;
            }, [
                broadcastOperatorInstrumentation,
                namespaceInstrumentation,
                socketInstrumentation,
            ]),
            new instrumentation_1.InstrumentationNodeModuleDefinition('socket.io', ['2'], (moduleExports, moduleVersion) => {
                if (moduleExports === undefined || moduleExports === null) {
                    return moduleExports;
                }
                if (moduleVersion === undefined) {
                    return moduleExports;
                }
                if ((0, instrumentation_1.isWrapped)(moduleExports?.prototype?.on)) {
                    this._unwrap(moduleExports.prototype, 'on');
                }
                this._wrap(moduleExports.prototype, 'on', this._patchOn(moduleVersion));
                return moduleExports;
            }, (moduleExports, moduleVersion) => {
                if ((0, instrumentation_1.isWrapped)(moduleExports?.prototype?.on)) {
                    this._unwrap(moduleExports.prototype, 'on');
                }
                return moduleExports;
            }, [namespaceInstrumentationLegacy, socketInstrumentationLegacy]),
        ];
    }
    setConfig(config = {}) {
        return super.setConfig((0, utils_1.normalizeConfig)(config));
    }
    _patchOn(moduleVersion) {
        const self = this;
        return (original) => {
            return function (ev, originalListener) {
                if (!self.getConfig().traceReserved && reservedEvents.includes(ev)) {
                    return original.apply(this, arguments);
                }
                if (self.getConfig().onIgnoreEventList?.includes(ev)) {
                    return original.apply(this, arguments);
                }
                const wrappedListener = function (...args) {
                    const eventName = ev;
                    const namespace = this.name || this.adapter?.nsp?.name;
                    const span = self.tracer.startSpan(`${semconv_obsolete_1.MESSAGING_OPERATION_VALUE_RECEIVE} ${namespace}`, {
                        kind: api_1.SpanKind.CONSUMER,
                        attributes: {
                            [semconv_1.ATTR_MESSAGING_SYSTEM]: 'socket.io',
                            [semconv_obsolete_1.ATTR_MESSAGING_DESTINATION]: namespace,
                            [semconv_1.ATTR_MESSAGING_OPERATION]: semconv_obsolete_1.MESSAGING_OPERATION_VALUE_RECEIVE,
                            [AttributeNames_1.SocketIoInstrumentationAttributes.SOCKET_IO_EVENT_NAME]: eventName,
                        },
                    });
                    const { onHook } = self.getConfig();
                    if (onHook) {
                        (0, instrumentation_1.safeExecuteInTheMiddle)(() => onHook(span, { moduleVersion, payload: args }), e => {
                            if (e)
                                self._diag.error('onHook error', e);
                        }, true);
                    }
                    return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), () => self.endSpan(() => originalListener.apply(this, arguments), span));
                };
                return original.apply(this, [ev, wrappedListener]);
            };
        };
    }
    endSpan(traced, span) {
        try {
            const result = traced();
            if ((0, utils_1.isPromise)(result)) {
                return result.then(value => {
                    span.end();
                    return value;
                }, err => {
                    span.recordException(err);
                    span.setStatus({
                        code: api_1.SpanStatusCode.ERROR,
                        message: err?.message,
                    });
                    span.end();
                    throw err;
                });
            }
            else {
                span.end();
                return result;
            }
        }
        catch (error) {
            span.recordException(error);
            span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message });
            span.end();
            throw error;
        }
    }
    _patchEmit(moduleVersion) {
        const self = this;
        return (original) => {
            return function (ev, ...args) {
                if (!self.getConfig().traceReserved && reservedEvents.includes(ev)) {
                    return original.apply(this, arguments);
                }
                if (self.getConfig().emitIgnoreEventList?.includes(ev)) {
                    return original.apply(this, arguments);
                }
                const messagingSystem = 'socket.io';
                const eventName = ev;
                const attributes = {
                    [semconv_1.ATTR_MESSAGING_SYSTEM]: messagingSystem,
                    [semconv_obsolete_1.ATTR_MESSAGING_DESTINATION_KIND]: semconv_obsolete_1.MESSAGING_DESTINATION_KIND_VALUE_TOPIC,
                    [AttributeNames_1.SocketIoInstrumentationAttributes.SOCKET_IO_EVENT_NAME]: eventName,
                };
                const rooms = (0, utils_1.extractRoomsAttributeValue)(this);
                if (rooms.length) {
                    attributes[AttributeNames_1.SocketIoInstrumentationAttributes.SOCKET_IO_ROOMS] = rooms;
                }
                const namespace = this.name || this.adapter?.nsp?.name || this.sockets?.name;
                if (namespace) {
                    attributes[AttributeNames_1.SocketIoInstrumentationAttributes.SOCKET_IO_NAMESPACE] =
                        namespace;
                    attributes[semconv_obsolete_1.ATTR_MESSAGING_DESTINATION] = namespace;
                }
                const span = self.tracer.startSpan(`send ${namespace}`, {
                    kind: api_1.SpanKind.PRODUCER,
                    attributes,
                });
                const { emitHook } = self.getConfig();
                if (emitHook) {
                    (0, instrumentation_1.safeExecuteInTheMiddle)(() => emitHook(span, { moduleVersion, payload: args }), e => {
                        if (e)
                            self._diag.error('emitHook error', e);
                    }, true);
                }
                try {
                    return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), () => original.apply(this, arguments));
                }
                catch (error) {
                    span.setStatus({
                        code: api_1.SpanStatusCode.ERROR,
                        message: error.message,
                    });
                    throw error;
                }
                finally {
                    span.end();
                }
            };
        };
    }
}
exports.SocketIoInstrumentation = SocketIoInstrumentation;
//# sourceMappingURL=socket.io.js.map