File

src/plugins/plugin-pg.ts

Description

Partial shape of objects returned by Client#query. Only contains methods that are significant to the query lifecycle.

Index

Properties

Properties

_result
_result: pg_7.QueryResult
Type : pg_7.QueryResult
Optional
handleError
handleError: function
Type : function
handleReadyForQuery
handleReadyForQuery: function
Type : function
import {EventEmitter} from 'events';
import * as shimmer from 'shimmer';

import {Patch, Plugin, Span, Tracer} from '../plugin-types';

import {pg_6, pg_7} from './types';

// TS: Client#query also accepts a callback as a last argument, but TS cannot
// detect this as it's a dependent type. So we don't specify it here.
type ClientQueryArguments =
  | [Submittable & pg_7.QueryConfig]
  | [string]
  | [string, {}];
type PG7QueryReturnValue =
  | ((pg_7.QueryConfig & ({submit: Function} & EventEmitter)) | pg_7.Query)
  | Promise<pg_7.QueryResult>;
type Callback<T> = (err: Error | null, res?: T) => void;

const noOp = () => {};

function populateLabelsFromInputs(span: Span, args: ClientQueryArguments) {
  const queryObj = args[0];
  if (typeof queryObj === 'object') {
    if (queryObj.text) {
      span.addLabel('query', queryObj.text);
    }
    if (queryObj.values) {
      span.addLabel('values', queryObj.values);
    }
  } else if (typeof queryObj === 'string') {
    span.addLabel('query', queryObj);
    if (args.length >= 2 && typeof args[1] !== 'function') {
      span.addLabel('values', args[1]);
    }
  }
}

function populateLabelsFromOutputs(
  span: Span,
  err: Error | null,
  res?: pg_7.QueryResult
) {
  if (err) {
    span.addLabel('error', err);
  }
  if (res) {
    span.addLabel('row_count', res.rowCount);
    span.addLabel('oid', res.oid);
    span.addLabel('rows', res.rows);
    // Starting in pg@7.16.0, res.fields could be undefined.
    span.addLabel('fields', res.fields || []);
  }
}

/**
 * Partial shape of objects returned by Client#query. Only contains methods that
 * are significant to the query lifecycle.
 */
interface Submittable {
  // Called when the query is completed.
  handleReadyForQuery: () => void;
  // Called when an error occurs.
  handleError: () => void;
  // A field that is populated when the Submittable is a Query object.
  _result?: pg_7.QueryResult;
}

/**
 * Utility class to help organize patching logic.
 */
class PostgresPatchUtility {
  readonly maybePopulateLabelsFromInputs: typeof populateLabelsFromInputs;
  readonly maybePopulateLabelsFromOutputs: typeof populateLabelsFromOutputs;

  constructor(private readonly tracer: Tracer) {
    this.maybePopulateLabelsFromInputs =
      tracer.enhancedDatabaseReportingEnabled()
        ? populateLabelsFromInputs
        : noOp;
    this.maybePopulateLabelsFromOutputs =
      tracer.enhancedDatabaseReportingEnabled()
        ? populateLabelsFromOutputs
        : noOp;
  }

  patchSubmittable(pgQuery: Submittable, span: Span): Submittable {
    let spanEnded = false;
    const {maybePopulateLabelsFromOutputs} = this;
    if (pgQuery.handleError!) {
      shimmer.wrap(pgQuery, 'handleError', origCallback => {
        // Elements of args are not individually accessed.
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return this.tracer.wrap(function (
          this: Submittable,
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          ...args: any[]
        ): void {
          if (!spanEnded) {
            const err: Error = args[0];
            maybePopulateLabelsFromOutputs(span, err);
            span.endSpan();
            spanEnded = true;
          }
          if (origCallback) {
            origCallback.apply(this, args);
          }
        });
      });
    }
    if (pgQuery.handleReadyForQuery!) {
      shimmer.wrap(pgQuery, 'handleReadyForQuery', origCallback => {
        // Elements of args are not individually accessed.
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return this.tracer.wrap(function (
          this: Submittable,
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          ...args: any[]
        ): void {
          if (!spanEnded) {
            maybePopulateLabelsFromOutputs(span, null, this._result);
            span.endSpan();
            spanEnded = true;
          }
          if (origCallback) {
            origCallback.apply(this, args);
          }
        });
      });
    }
    return pgQuery;
  }

  patchCallback(
    callback: Callback<pg_7.QueryResult>,
    span: Span
  ): Callback<pg_7.QueryResult> {
    return this.tracer.wrap((err: Error | null, res?: pg_7.QueryResult) => {
      this.maybePopulateLabelsFromOutputs(span, err, res);
      span.endSpan();
      callback(err, res);
    });
  }

  patchPromise(
    promise: Promise<pg_7.QueryResult>,
    span: Span
  ): Promise<pg_7.QueryResult> {
    return (promise = promise.then(
      res => {
        this.maybePopulateLabelsFromOutputs(span, null, res);
        span.endSpan();
        return res;
      },
      err => {
        this.maybePopulateLabelsFromOutputs(span, err);
        span.endSpan();
        throw err;
      }
    ));
  }
}

const plugin: Plugin = [
  {
    file: 'lib/client.js',
    versions: '^6.x',
    // TS: Client is a class name.
    patch: (Client, api) => {
      const pgPatch = new PostgresPatchUtility(api);

      shimmer.wrap(Client.prototype, 'query', query => {
        // Every call to Client#query will have a Submittable object associated
        // with it. We need to patch two handlers (handleReadyForQuery and
        // handleError) to end a span.
        // There are a few things to note here:
        // * query accepts a Submittable or a string. A Query is a Submittable.
        //   So if we can get a Submittable from the input we patch it
        //   proactively, otherwise (in the case of a string) we patch the
        //   output Query instead.
        // * If query is passed a callback, the callback will be invoked from
        //   either handleReadyForQuery or handleError. So we don't need to
        //   separately patch the callback.
        return function query_trace(
          this: pg_6.Client,
          ...args: ClientQueryArguments
        ) {
          if (args.length >= 1) {
            const span = api.createChildSpan({name: 'pg-query'});
            if (!api.isRealSpan(span)) {
              return query.apply(this, args);
            }
            // Extract query text and values, if needed.
            pgPatch.maybePopulateLabelsFromInputs(span, args);
            if (typeof args[0] === 'object') {
              pgPatch.patchSubmittable(args[0], span);
              return query.apply(this, args);
            } else {
              return pgPatch.patchSubmittable(
                query.apply(this, args) as Submittable,
                span
              );
            }
          } else {
            // query was called with no arguments.
            // This doesn't make sense, but don't do anything that might cause
            // an error to get thrown here, or a span to be started.
            return query.apply(this, args);
          }
        };
      });
    },
    // TS: Client is a class name.
    unpatch(Client) {
      shimmer.unwrap(Client.prototype, 'query');
    },
  } as Patch<typeof pg_6.Client>,
  {
    file: 'lib/client.js',
    versions: '^7.x',
    // TS: Client is a class name.
    patch: (Client, api) => {
      const pgPatch = new PostgresPatchUtility(api);
      shimmer.wrap(Client.prototype, 'query', query => {
        return function query_trace(this: pg_7.Client) {
          const span = api.createChildSpan({name: 'pg-query'});
          if (!api.isRealSpan(span)) {
            // eslint-disable-next-line prefer-rest-params
            return query.apply(this, arguments);
          }

          let pgQuery: PG7QueryReturnValue;
          // In 7.x, the value of pgQuery depends on how the query() was called.
          // It can be one of:
          // - (query: pg.Submittable) => EventEmitter
          //   - Note: return value is the same as the argument.
          // - ([*], callback: (err, res: pg.Result) => void) => void
          // - ([*]) => Promise<pg.Result>
          // where [*] is one of:
          // - ...[query: { text: string, values?: Array<any> }]
          // - ...[text: string, values?: Array<any>]
          // See: https://node-postgres.com/guides/upgrading
          const argLength = arguments.length;
          if (argLength >= 1) {
            const args: ClientQueryArguments = Array.prototype.slice.call(
              // eslint-disable-next-line prefer-rest-params
              arguments,
              0
            );

            // Extract query text and values, if needed.
            pgPatch.maybePopulateLabelsFromInputs(span, args);

            // If we received a callback, bind it to the current context,
            // optionally adding labels as well.
            const callback = args[args.length - 1];
            if (typeof callback === 'function') {
              args[args.length - 1] = pgPatch.patchCallback(
                callback as Callback<pg_7.QueryResult>,
                span
              );
            } else if (typeof args[0] === 'object') {
              pgPatch.patchSubmittable(args[0] as Submittable, span);
            }
            pgQuery = query.apply(this, args);
          } else {
            // eslint-disable-next-line prefer-rest-params
            pgQuery = query.apply(this, arguments);
          }

          if (pgQuery) {
            if (pgQuery instanceof EventEmitter) {
              api.wrapEmitter(pgQuery);
            } else if (typeof pgQuery.then === 'function') {
              // Unlike in pg 6, the returned value can't be both a Promise and
              // a Submittable. So we don't run the risk of double-patching
              // here.
              pgPatch.patchPromise(pgQuery, span);
            }
          }
          return pgQuery;
        };
      });
    },
    // TS: Client is a class name.
    unpatch(Client) {
      shimmer.unwrap(Client.prototype, 'query');
    },
  } as Patch<typeof pg_7.Client>,
];

export = plugin;

results matching ""

    No results matching ""