solarwinds / appoptics-apm-node

AppOptics APM Instrumentation Agent for Node.js
Apache License 2.0
11 stars 9 forks source link

Error with fs.exists wrapper. #287

Open EthanShoeDev opened 1 year ago

EthanShoeDev commented 1 year ago

I am trying to use AppOptics in a nodejs project that uses the prisma library. I am also using esm imports, so I am instantiating AppOptics like so:

node -r appoptics-apm dist/src/main

The sequence of events that leads to the error:

  1. function patchPathMethod (fs, method) within this library wraps the fs.exists() method.
  2. Prisma begins its initialization and checks what OS it is running on. That results in this method being called ref:

    export async function resolveDistro(): Promise<undefined | GetOSResult['distro']> {
    // https://github.com/retrohacker/getos/blob/master/os.json
    const osReleaseFile = '/etc/os-release'
    const alpineReleaseFile = '/etc/alpine-release'
    
    if (await exists(alpineReleaseFile)) {
    return 'musl'
    } else if (await exists(osReleaseFile)) {
    return parseDistro(await readFile(osReleaseFile, 'utf-8'))
    } else {
    return
    }
    }

    exists() is simply a promisified version of fs.exists()

    const exists = promisify(fs.exists)
  3. The second call to exsits() causes the boolean value true to be thrown instead of returned as it should be. The thrown boolean cascades into this error which crashes the application:
TypeError: Cannot create property 'clientVersion' on boolean 'true'
    at RequestHandler.handleRequestError (/home/project/node_modules/@prisma/client/runtime/index.js:31959:26)
    at RequestHandler.handleAndLogRequestError (/home/project/node_modules/@prisma/client/runtime/index.js:31913:12)
    at /home/project/node_modules/@prisma/client/runtime/index.js:32458:25
    at async PrismaService._executeRequest (/home/project/node_modules/@prisma/client/runtime/index.js:33022:22)
    at async PrismaService._request (/home/project/node_modules/@prisma/client/runtime/index.js:32994:16)

After debugging it for a while, I believe that the problem stems from the fact that patchPathMethod within node_modules/appoptics-apm/lib/probes/fs.js is not meant to override suppressedCallback within the fs library. I believe the fs library has changed in different versions of node but mine looks like this (node version v16.17.0):

/**
 * Tests whether or not the given path exists.
 * @param {string | Buffer | URL} path
 * @param {(exists?: boolean) => any} callback
 * @returns {void}
 */
function exists(path, callback) {
  maybeCallback(callback);

  function suppressedCallback(err) {
    callback(err ? false : true);
  }

  try {
    fs.access(path, F_OK, suppressedCallback);
  } catch {
    return callback(false);
  }
}
EthanShoeDev commented 1 year ago

Here are the logs as requested by support: err.log

EthanShoeDev commented 1 year ago

An extra note. I do not even need fs monitoring at all. I tried to disable it with the config file but that didn't seem to do anything: appoptics-apm-config.json

{
    "enabled": true,
    "serviceKey": "REDACTED",
    "probes": {
        "fs": {
            "enabled": false,
            "ignoreErrors": {
                "open": {
                    "ENOENT": true
                },
                "access": {
                    "ENOENT": true
                }
            }
        }
    }
}
ronilan commented 1 year ago

Try fs config:

registered: false

https://github.com/appoptics/appoptics-apm-node/blob/master/lib/probe-defaults.js#L28

EthanShoeDev commented 1 year ago

That worked! Thank you!

By the way, here is a minimal reproduction of the error case:

import * as express from 'express';
import { promisify } from 'util';
import * as fs from 'fs';

const exists = promisify(fs.exists);

async function resolveDistro() {
  const osReleaseFile = '/etc/os-release';
  const alpineReleaseFile = '/etc/alpine-release';

  if (await exists(alpineReleaseFile)) {
    return 'musl';
  } else if (await exists(osReleaseFile)) {
    return 'not-musl';
  } else {
    return;
  }
}

const initServer = async () => {
  await resolveDistro();
  const app = express();

  app.get('/', (req, res) => {
    res.send('Successful response.');
  });

  app.listen(3000, () => console.log('Example app is listening on port 3000.'));
};

initServer().catch((err) =>
  console.log('This value was just thrown as an error: ' + err),
);

Results in:

ethan@Ethan-XPS:~/project$ node dist/src/main2
Example app is listening on port 3000.
^C
ethan@Ethan-XPS:~/project$ node -r appoptics-apm dist/src/main2
This value was just thrown as an error: true
^C