<commit_before>const mqtt = require('mqtt');
const fs = require('fs-extra');
const winston = require('winston');
require('winston-daily-rotate-file');
const path = require('path');
const dgram = require('dgram');
const si = require('systeminformation');
const os = require('os');
const crypto = require('crypto');

const { Command, Option } = require('commander');

const Influx = require('influx');
const { Pool } = require('pg');

function checkFileExistsSync(filepath) {
    let flag = true;
    try {
        fs.accessSync(filepath, fs.constants.F_OK);
    } catch (e) {
        flag = false;
    }
    return flag;
}

// Get app version from package.json file
const appVersion = require('../package.json').version;

// Command line parameters
const program = new Command();
program
    .version(appVersion)
    .name('butler-sos')
    .description(
        'Butler SenseOps Stats ("Butler-SOS") is a microservice publishing operational Qlik Sense metrics to InfluxDB, Prometheus and New Relic.\nUser events and log events can be forwarded from Sense to Butler SOS and then acted upon there. Events can be stored in InfluxDB and sent to New Relic.\nAdd Grafana for great looking dashboards and you get real-time monitoring of what happens inside a Qlik Sense environment.'
    )
    .option('-c, --configfile <file>', 'path to config file')
    .addOption(
        new Option('-l, --loglevel <level>', 'log level').choices([
            'error',
            'warn',
            'info',
            'verbose',
            'debug',
            'silly',
        ])
    )
    .option(
        '--new-relic-account-name  <name...>',
        'New Relic account name. Used within Butler SOS to differentiate between different target New Relic accounts'
    )
    .option('--new-relic-api-key <key...>', 'insert API key to use with New Relic')
    .option('--new-relic-account-id <id...>', 'New Relic account ID');

// Parse command line params
program.parse(process.argv);
const options = program.opts();

// Is there a config file specified on the command line?
let configFileOption;
let configFileExpanded;
let configFilePath;
let configFileBasename;
let configFileExtension;
if (options.configfile && options.configfile.length > 0) {
    configFileOption = options.configfile;
    configFileExpanded = path.resolve(options.configfile);
    configFilePath = path.dirname(configFileExpanded);
    configFileExtension = path.extname(configFileExpanded);
    configFileBasename = path.basename(configFileExpanded, configFileExtension);

    if (configFileExtension.toLowerCase() !== '.yaml') {
        // eslint-disable-next-line no-console
        console.log('Error: Config file extension must be yaml');
        process.exit(1);
    }

    if (checkFileExistsSync(options.configfile)) {
        process.env.NODE_CONFIG_DIR = configFilePath;
        process.env.NODE_ENV = configFileBasename;
    } else {
        // eslint-disable-next-line no-console
        console.log('Error: Specified config file does not exist');
        process.exit(1);
    }
}

// Are we running as standalone app or not?
const isPkg = typeof process.pkg !== 'undefined';
if (isPkg && configFileOption === undefined) {
    // Show help if running as standalone app and mandatory options (e.g. config file) are not specified
    program.help({ error: true });
}

// eslint-disable-next-line import/order
const config = require('config');

// Is there a log level file specified on the command line?
if (options.loglevel && options.loglevel.length > 0) {
    config['Butler-SOS'].logLevel = options.loglevel;
}

// Set up array for storing app ids and names
const appNames = [];

// Set up logger with timestamps and colors, and optional logging to disk file
const logTransports = [];

logTransports.push(
    new winston.transports.Console({
        name: 'console',
        level: config.get('Butler-SOS.logLevel'),
        format: winston.format.combine(
            winston.format.errors({ stack: true }),
            winston.format.timestamp(),
            winston.format.colorize(),
            winston.format.simple(),
            winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`)
        ),
    })
);

if (config.get('Butler-SOS.fileLogging')) {
    logTransports.push(
        new winston.transports.DailyRotateFile({
            dirname: path.join(process.cwd(), config.get('Butler-SOS.logDirectory')),
            filename: 'butler-sos.%DATE%.log',
            level: config.get('Butler-SOS.logLevel'),
            datePattern: 'YYYY-MM-DD',
            maxFiles: '30d',
        })
    );
}

const logger = winston.createLogger({
    transports: logTransports,
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`)
    ),
});

// Function to get current logging level
const getLoggingLevel = () => logTransports.find((transport) => transport.name === 'console').level;

// Are there New Relic account name(s), API key(s) and account ID(s) specified on the command line?
// There must be the same number of each specified!
// If so, replace any info from the config file with data from command line options
if (
    options?.newRelicAccountName?.length > 0 &&
    options?.newRelicApiKey?.length > 0 &&
    options?.newRelicAccountId?.length > 0 &&
    options?.newRelicAccountName?.length === options?.newRelicApiKey?.length &&
    options?.newRelicApiKey?.length === options?.newRelicAccountId?.length
) {
    config['Butler-SOS'].thirdPartyToolsCredentials.newRelic = [];

    for (let index = 0; index < options.newRelicApiKey.length; index++) {
        const accountName = options.newRelicAccountName[index];
        const accountId = options.newRelicAccountId[index];
        const insertApiKey = options.newRelicApiKey[index];

        config['Butler-SOS'].thirdPartyToolsCredentials.newRelic.push({
            accountName,
            accountId,
            insertApiKey,
        });
    }
} else if (
    options?.newRelicAccountName?.length > 0 ||
    options?.newRelicApiKey?.length > 0 ||
    options?.newRelicAccountId?.length > 0
) {
    logger.error(
        'Incorrect command line parameters: Number of New Relic account names/IDs/API keys must match.'
    );
    process.exit(1);
}

// ------------------------------------
// User activity UDP server
const udpServerUserActivity = {};

try {
    udpServerUserActivity.host = config.has('Butler-SOS.userEvents.udpServerConfig.serverHost')
        ? config.get('Butler-SOS.userEvents.udpServerConfig.serverHost')
        : '';<commit_msg>feat: Specify zero or more New Relic credentials via command line option Implements #429<commit_after>const mqtt = require('mqtt');
const fs = require('fs-extra');
const winston = require('winston');
require('winston-daily-rotate-file');
const path = require('path');
const dgram = require('dgram');
const si = require('systeminformation');
const os = require('os');
const crypto = require('crypto');

const { Command, Option } = require('commander');

const Influx = require('influx');
const { Pool } = require('pg');

function checkFileExistsSync(filepath) {
    let flag = true;
    try {
        fs.accessSync(filepath, fs.constants.F_OK);
    } catch (e) {
        flag = false;
    }
    return flag;
}

// Get app version from package.json file
const appVersion = require('../package.json').version;

// Command line parameters
const program = new Command();
program
    .version(appVersion)
    .name('butler-sos')
    .description(
        'Butler SenseOps Stats ("Butler-SOS") is a microservice publishing operational Qlik Sense metrics to InfluxDB, Prometheus and New Relic.\nUser events and log events can be forwarded from Sense to Butler SOS and then acted upon there. Events can be stored in InfluxDB and sent to New Relic.\nAdd Grafana for great looking dashboards and you get real-time monitoring of what happens inside a Qlik Sense environment.'
    )
    .option('-c, --configfile <file>', 'path to config file')
    .addOption(
        new Option('-l, --loglevel <level>', 'log level').choices([
            'error',
            'warn',
            'info',
            'verbose',
            'debug',
            'silly',
        ])
    )
    .option(
        '--new-relic-account-name  <name...>',
        'New Relic account name. Used within Butler SOS to differentiate between different target New Relic accounts'
    )
    .option('--new-relic-api-key <key...>', 'insert API key to use with New Relic')
    .option('--new-relic-account-id <id...>', 'New Relic account ID');

// Parse command line params
program.parse(process.argv);
const options = program.opts();

// Is there a config file specified on the command line?
let configFileOption;
let configFileExpanded;
let configFilePath;
let configFileBasename;
let configFileExtension;
if (options.configfile && options.configfile.length > 0) {
    configFileOption = options.configfile;
    configFileExpanded = path.resolve(options.configfile);
    configFilePath = path.dirname(configFileExpanded);
    configFileExtension = path.extname(configFileExpanded);
    configFileBasename = path.basename(configFileExpanded, configFileExtension);

    if (configFileExtension.toLowerCase() !== '.yaml') {
        // eslint-disable-next-line no-console
        console.log('Error: Config file extension must be yaml');
        process.exit(1);
    }

    if (checkFileExistsSync(options.configfile)) {
        process.env.NODE_CONFIG_DIR = configFilePath;
        process.env.NODE_ENV = configFileBasename;
    } else {
        // eslint-disable-next-line no-console
        console.log('Error: Specified config file does not exist');
        process.exit(1);
    }
}

// Are we running as standalone app or not?
const isPkg = typeof process.pkg !== 'undefined';
if (isPkg && configFileOption === undefined) {
    // Show help if running as standalone app and mandatory options (e.g. config file) are not specified
    program.help({ error: true });
}

// eslint-disable-next-line import/order
const config = require('config');

// Is there a log level file specified on the command line?
if (options.loglevel && options.loglevel.length > 0) {
    config['Butler-SOS'].logLevel = options.loglevel;
}

// Set up array for storing app ids and names
const appNames = [];

// Set up logger with timestamps and colors, and optional logging to disk file
const logTransports = [];

logTransports.push(
    new winston.transports.Console({
        name: 'console',
        level: config.get('Butler-SOS.logLevel'),
        format: winston.format.combine(
            winston.format.errors({ stack: true }),
            winston.format.timestamp(),
            winston.format.colorize(),
            winston.format.simple(),
            winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`)
        ),
    })
);

if (config.get('Butler-SOS.fileLogging')) {
    logTransports.push(
        new winston.transports.DailyRotateFile({
            dirname: path.join(process.cwd(), config.get('Butler-SOS.logDirectory')),
            filename: 'butler-sos.%DATE%.log',
            level: config.get('Butler-SOS.logLevel'),
            datePattern: 'YYYY-MM-DD',
            maxFiles: '30d',
        })
    );
}

const logger = winston.createLogger({
    transports: logTransports,
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`)
    ),
});

// Function to get current logging level
const getLoggingLevel = () => logTransports.find((transport) => transport.name === 'console').level;

// Are there New Relic account name(s), API key(s) and account ID(s) specified on the command line?
// There must be the same number of each specified!
// If so, replace any info from the config file with data from command line options
if (
    options?.newRelicAccountName?.length > 0 &&
    options?.newRelicApiKey?.length > 0 &&
    options?.newRelicAccountId?.length > 0 &&
    options?.newRelicAccountName?.length === options?.newRelicApiKey?.length &&
    options?.newRelicApiKey?.length === options?.newRelicAccountId?.length
) {
    config['Butler-SOS'].thirdPartyToolsCredentials.newRelic = [];

    //
    for (let index = 0; index < options.newRelicApiKey.length; index++) {
        const accountName = options.newRelicAccountName[index];
        const accountId = options.newRelicAccountId[index];
        const insertApiKey = options.newRelicApiKey[index];

        config['Butler-SOS'].thirdPartyToolsCredentials.newRelic.push({
            accountName,
            accountId,
            insertApiKey,
        });
    }
} else if (
    options?.newRelicAccountName?.length > 0 ||
    options?.newRelicApiKey?.length > 0 ||
    options?.newRelicAccountId?.length > 0
) {
    logger.error(
        'Incorrect command line parameters: Number of New Relic account names/IDs/API keys must match.'
    );
    process.exit(1);
}

// ------------------------------------
// User activity UDP server
const udpServerUserActivity = {};

try {
    udpServerUserActivity.host = config.has('Butler-SOS.userEvents.udpServerConfig.serverHost')
        ? config.get('Butler-SOS.userEvents.udpServerConfig.serverHost')
        : '';
{'fraction_non_alphanumeric': 0.13382286827513862, 'fraction_numerical': 0.002322793346320995, 'mean_word_length': 3.0605415272284757, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2795516', 'n_tokens_mistral': 3874, 'n_tokens_neox': 3825, 'n_words': 1029}