metrics/Trace.js


const lib = require("../lib");
const logger = require("../logger");

module.exports = class Trace {

    static #sock;

    /**
    * AWS X-Ray trace support
    *
    * Only supports local daemon UDP port 2000, to test locally
    *
    *    socat -U -v PIPE  udp-recv:2000
    *
    * @example
    *
    * var trace = new metrics.Trace({ _host: "127.0.0.1", annotations: { tag: app.onstance.tag, role: app.role } });
    * var sub1 = trace.start("subsegment1");
    * sub1.stop();
    * var sub2 = trace.start("subsegment2");
    * trace.stop(req);
    * trace.send();
    * trace.destroy();
    * @param {object} [options]
    * @param {Trqce} [parent]
    * @class Trace
    */

    constructor(options, parent)
    {
        if (parent instanceof Trace) {
            this._parent = parent;
        } else {
            this.trace_id = `1-${Math.round(new Date().getTime() / 1000).toString(16)}-${lib.randomBytes(12)}`;
        }
        this.id = lib.randomBytes(8);

        this._start = Date.now();
        this.start_time = this._start / 1000;

        if (typeof options == "string") {
            this.name = options;
        } else {
            for (const p in options) {
                if (this[p] === undefined) this[p] = options[p];
            }
        }
        if (!this.name) this.name = process.title.split(/[^a-z0-9_-]/i)[0];
    }

    /**
     * Closes a segment or subsegment, for segments it sends it right away
     * @param {IncomingRequest} [req]
     * @memberOf Trace
     * @method stop
     */
    stop(req)
    {
        if (!this.end_time) {
            this._end = Date.now();
            this.end_time = this._end / 1000;
        }

        if (req?.res?.statusCode) {
            this.http = {
                request: {
                    method: req.method || "GET",
                    url: `http${req.options.secure}://${req.options.host}${req.url}`,
                },
                response: {
                    status: req.res.statusCode
                }
            }
        }
        for (const i in this.subsegments) this.subsegments[i].stop();
    }

    /**
     * destroy all traces and subsegments
     * @method destroy
     * @memberOf Trace
     */
    destroy()
    {
        for (const i in this.subsegments) {
            this.subsegments[i].destroy();
        }
        for (const p in this) {
            if (typeof this[p] == "object") delete this[p];
        }
    }

    /**
     * @method toString
     * @memberOf Trace
     */
    toString(msg)
    {
        return lib.stringify(msg || this, (key, val) => (key[0] == "_" ? undefined : val))
    }


    /**
     * Sends a segment to local daemon
     * @memberOf Trace
     * @method send
     */
    send(msg)
    {
        if (!Trace.#sock) {
            Trace.#sock = require("node:dgram").createSocket('udp4').unref();
        }

        var json = this.toString(msg);

        Trace.#sock.send(`{"format":"json","version":1}\n${json}`, this._port || 2000, this._host, (err) => {
            logger.logger(err ? "error": "debug", "trace", "send:", err, json);
        });
    }

    /**
     * Starts a new subsegment
     * @param {object} [options]
     * @memberOf Trace
     * @method start
     */
    start(options)
    {
        var sub = new Trace(options, this);
        if (!this.subsegments) this.subsegments = [];
        this.subsegments.push(sub);
        return sub;
    }

}