0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

feat: replace bunyan by pino.js (#2148)

* feat: replace bunyan by pino.js

* chore: refactor logger

* chore: fix e2e

* chore: better catch
This commit is contained in:
Juan Picado 2021-03-30 20:32:46 +02:00 committed by GitHub
parent 274d483de4
commit ee97dcb46c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 396 additions and 491 deletions

View file

@ -24,4 +24,6 @@ jobs:
- name: Build
run: yarn code:build
- name: Test CLI
run: yarn run test:e2e:cli
run: yarn test:e2e:cli
env:
NODE_ENV: production

View file

@ -5,5 +5,6 @@
"**/.nyc_output": true,
"**/build": true,
"**/coverage": true
}
}
},
"typescript.tsdk": "node_modules/typescript/lib"
}

View file

@ -73,9 +73,8 @@ middlewares:
enabled: true
# log settings
logs:
- { type: stdout, format: pretty, level: http }
#- {type: file, path: verdaccio.log, level: info}
logs: { type: stdout, format: pretty, level: http }
#experiments:
# # support for npm token command
# token: false

View file

@ -79,9 +79,8 @@ middlewares:
enabled: true
# log settings
logs:
- { type: stdout, format: pretty, level: http }
#- {type: file, path: verdaccio.log, level: info}
logs: { type: stdout, format: pretty, level: http }
#experiments:
# # support for npm token command
# token: false

View file

@ -26,7 +26,6 @@
"JSONStream": "1.3.5",
"async": "3.2.0",
"body-parser": "1.19.0",
"bunyan": "1.8.15",
"commander": "7.2.0",
"compression": "1.7.4",
"cookies": "0.8.0",
@ -35,6 +34,7 @@
"debug": "^4.3.1",
"envinfo": "7.7.4",
"express": "4.17.1",
"fast-safe-stringify": "^2.0.7",
"handlebars": "4.7.7",
"http-errors": "1.8.0",
"js-yaml": "4.0.0",
@ -47,7 +47,10 @@
"minimatch": "3.0.4",
"mkdirp": "1.0.4",
"mv": "2.1.1",
"pino": "6.11.2",
"pkginfo": "0.4.1",
"prettier-bytes": "^1.0.3",
"pretty-ms": "^5.0.0",
"request": "2.88.0",
"semver": "7.3.4",
"validator": "13.5.2",
@ -88,6 +91,7 @@
"@types/mime": "2.0.1",
"@types/minimatch": "3.0.3",
"@types/node": "12.12.21",
"@types/pino": "6.3.6",
"@types/request": "2.48.3",
"@types/semver": "6.2.0",
"@typescript-eslint/eslint-plugin": "4.13.0",
@ -99,7 +103,6 @@
"babel-jest": "26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-dynamic-import-node": "2.3.3",
"babel-plugin-emotion": "10.0.33",
"codecov": "3.8.1",
"cross-env": "7.0.3",
"detect-secrets": "1.0.6",
@ -133,7 +136,6 @@
"standard-version": "9.1.1",
"supertest": "6.1.1",
"typescript": "3.9.9",
"verdaccio": "^4.5.1",
"verdaccio-auth-memory": "10.0.0",
"verdaccio-memory": "10.0.0"
},
@ -181,7 +183,7 @@
"preferGlobal": true,
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-commit": "lint-staged",
"commit-msg": "commitlint -e $GIT_PARAMS"
}
},

View file

@ -41,4 +41,4 @@ middlewares:
enabled: true
logs:
- { type: stdout, format: pretty, level: warn }
- { type: stdout, format: json, level: warn }

View file

@ -31,7 +31,7 @@ import {
import { aesDecrypt, verifyPayload } from './crypto-utils';
export function validatePassword(
password: string,
password: string, // pragma: allowlist secret
minLength: number = DEFAULT_MIN_LIMIT_PASSWORD
): boolean {
return typeof password === 'string' && password.length >= minLength;
@ -125,21 +125,21 @@ export function handleSpecialUnpublish(): any {
};
}
export function getDefaultPlugins(): IPluginAuth<Config> {
export function getDefaultPlugins(logger: any): IPluginAuth<Config> {
return {
authenticate(user: string, password: string, cb: Callback): void {
authenticate(_user: string, _password: string, cb: Callback): void { // pragma: allowlist secret
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
},
add_user(user: string, password: string, cb: Callback): void {
add_user(_user: string, _password: string, cb: Callback): void { // pragma: allowlist secret
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
},
// FIXME: allow_action and allow_publish should be in the @verdaccio/types
// @ts-ignore
allow_access: allow_action('access'),
allow_access: allow_action('access', logger),
// @ts-ignore
allow_publish: allow_action('publish'),
allow_publish: allow_action('publish', logger),
allow_unpublish: handleSpecialUnpublish()
};
}

View file

@ -39,7 +39,7 @@ const LoggerApi = require('./logger');
class Auth implements IAuth {
public config: Config;
public logger: Logger;
public secret: string;
public secret: string; // pragma: allowlist secret
public plugins: IPluginAuth<Config>[];
public constructor(config: Config) {
@ -70,13 +70,13 @@ class Auth implements IAuth {
}
private _applyDefaultPlugins(): void {
this.plugins.push(getDefaultPlugins());
this.plugins.push(getDefaultPlugins(this.logger));
}
public changePassword(
username: string,
password: string,
newPassword: string,
password: string, // pragma: allowlist secret
newPassword: string, // pragma: allowlist secret
cb: Callback
): void {
const validPlugins = _.filter(this.plugins, (plugin) => _.isFunction(plugin.changePassword));

View file

@ -1,168 +0,0 @@
/* eslint-disable */
import { prettyTimestamped } from './logger/format/pretty-timestamped';
import { pretty } from './logger/format/pretty';
import { jsonFormat } from './logger/format/json';
const cluster = require('cluster');
const Logger = require('bunyan');
const Error = require('http-errors');
const Stream = require('stream');
const pkgJSON = require('../../package.json');
const _ = require('lodash');
const dayjs = require('dayjs');
/**
* A RotatingFileStream that modifies the message first
*/
class VerdaccioRotatingFileStream extends Logger.RotatingFileStream {
// We depend on mv so that this is there
write(obj) {
super.write(jsonFormat(obj, false));
}
rotate(): void {
super.rotate();
this.emit('rotated');
}
}
let logger;
export interface LoggerTarget {
type?: string;
format?: string;
level?: string;
options?: any;
path?: string;
}
const DEFAULT_LOGGER_CONF = [{ type: 'stdout', format: 'pretty', level: 'http' }];
/**
* Setup the Buyan logger
* @param {*} logs list of log configuration
*/
function setup(logs, { logStart } = { logStart: true }) {
const streams: any = [];
if (logs == null) {
logs = DEFAULT_LOGGER_CONF;
}
logs.forEach(function (target: LoggerTarget) {
let level = target.level || 35;
if (level === 'http') {
level = 35;
}
// create a stream for each log configuration
if (target.type === 'rotating-file') {
if (target.format !== 'json') {
throw new Error('Rotating file streams only work with JSON!');
}
if (cluster.isWorker) {
// https://github.com/trentm/node-bunyan#stream-type-rotating-file
throw new Error('Cluster mode is not supported for rotating-file!');
}
const stream = new VerdaccioRotatingFileStream(
// @ts-ignore
_.merge(
{},
// Defaults can be found here: https://github.com/trentm/node-bunyan#stream-type-rotating-file
target.options || {},
{ path: target.path, level }
)
);
const rotateStream = {
type: 'raw',
level,
stream
};
if (logStart) {
stream.on('rotated', () => logger.warn('Start of logfile'));
}
streams.push(rotateStream);
} else {
const stream = new Stream();
stream.writable = true;
let destination;
let destinationIsTTY = false;
if (target.type === 'file') {
// destination stream
destination = require('fs').createWriteStream(target.path, {
flags: 'a',
encoding: 'utf8'
});
destination.on('error', function (err) {
stream.emit('error', err);
});
} else if (target.type === 'stdout' || target.type === 'stderr') {
destination = target.type === 'stdout' ? process.stdout : process.stderr;
destinationIsTTY = destination.isTTY;
} else {
throw Error('wrong target type for a log');
}
if (target.format === 'pretty') {
// making fake stream for pretty printing
stream.write = (obj) => {
destination.write(pretty(obj, destinationIsTTY));
};
} else if (target.format === 'pretty-timestamped') {
// making fake stream for pretty printing
stream.write = (obj) => {
destination.write(prettyTimestamped(obj, destinationIsTTY));
};
} else {
stream.write = (obj) => {
destination.write(jsonFormat(obj, destinationIsTTY));
};
}
streams.push({
// @ts-ignore
type: 'raw',
// @ts-ignore
level,
// @ts-ignore
stream: stream
});
}
});
// buyan default configuration
logger = new Logger({
name: pkgJSON.name,
streams: streams,
serializers: {
err: Logger.stdSerializers.err,
req: Logger.stdSerializers.req,
res: Logger.stdSerializers.res
}
});
// In case of an empty log file, we ensure there is always something logged. This also helps see if the server
// was restarted in any cases
if (logStart) {
logger.warn('Verdaccio started');
}
process.on('SIGUSR2', function () {
// https://github.com/trentm/node-bunyan#stream-type-rotating-file
if (logger) {
/**
* Note on log rotation: Often you may be using external log rotation utilities like logrotate on Linux or logadm
* on SmartOS/Illumos. In those cases, unless your are ensuring "copy and truncate" semantics
* (via copytruncate with logrotate or -c with logadm) then the fd for your 'file' stream will change.
*/
logger.reopenFileStreams();
}
});
}
export { setup, logger };

View file

@ -1,9 +0,0 @@
import { fillInMsgTemplate } from '../formatter';
const Logger = require('bunyan');
export function jsonFormat(obj, hasColors): string {
const msg = fillInMsgTemplate(obj.msg, obj, hasColors);
return `${JSON.stringify({ ...obj, msg }, Logger.safeCycles())}\n`;
}

View file

@ -1,6 +0,0 @@
import { formatLoggingDate } from '../utils';
import { printMessage } from '../formatter';
export function prettyTimestamped(obj, hasColors): string {
return `[${formatLoggingDate(obj.time)}] ${printMessage(obj.level, obj.msg, obj, hasColors)}\n`;
}

View file

@ -1,5 +0,0 @@
import { printMessage } from '../formatter';
export function pretty(obj, hasColors): string {
return `${printMessage(obj.level, obj.msg, obj, hasColors)}\n`;
}

View file

@ -1,67 +0,0 @@
import { inspect } from 'util';
import { red, green } from 'kleur';
import { white } from 'kleur';
import { isObject, pad } from '../utils';
import { calculateLevel, levels, subsystems } from './levels';
let LEVEL_VALUE_MAX = 0;
for (const l in levels) {
if (Object.prototype.hasOwnProperty.call(levels, l)) {
LEVEL_VALUE_MAX = Math.max(LEVEL_VALUE_MAX, l.length);
}
}
/**
* Apply colors to a string based on level parameters.
* @param {*} type
* @param {*} msg
* @param {*} templateObjects
* @param {*} hasColors
* @return {String}
*/
export function printMessage(type, msg, templateObjects, hasColors) {
if (typeof type === 'number') {
type = calculateLevel(type);
}
const finalMessage = fillInMsgTemplate(msg, templateObjects, hasColors);
const sub = subsystems[hasColors ? 0 : 1][templateObjects.sub] || subsystems[+!hasColors].default;
if (hasColors) {
return ` ${levels[type](pad(type, LEVEL_VALUE_MAX))}${white(`${sub} ${finalMessage}`)}`;
}
return ` ${pad(type, LEVEL_VALUE_MAX)}${sub} ${finalMessage}`;
}
export function fillInMsgTemplate(msg, obj: unknown, colors): string {
return msg.replace(/@{(!?[$A-Za-z_][$0-9A-Za-z\._]*)}/g, (_, name): string => {
let str = obj;
let is_error;
if (name[0] === '!') {
name = name.substr(1);
is_error = true;
}
const _ref = name.split('.');
for (let _i = 0; _i < _ref.length; _i++) {
const id = _ref[_i];
if (isObject(str)) {
// @ts-ignore
str = str[id];
} else {
str = undefined;
}
}
if (typeof str === 'string') {
if (!colors || (str as string).includes('\n')) {
return str;
} else if (is_error) {
return red(str);
}
return green(str);
}
return inspect(str, undefined, null, colors);
});
}

View file

@ -0,0 +1,17 @@
import { printMessage, PrettyOptionsExtended } from './prettifier';
export type PrettyFactory = (param) => string;
/*
options eg:
{ messageKey: 'msg', levelFirst: true, prettyStamp: false }
*/
module.exports = function prettyFactory(options: PrettyOptionsExtended): PrettyFactory {
// the break line must happens in the prettify component
const breakLike = '\n';
return (inputData): string => {
// FIXME: review colors by default is true
return printMessage(inputData, options, true) + breakLike;
};
};

View file

@ -0,0 +1,100 @@
import { inspect } from 'util';
import { white, red, green } from 'kleur';
import _ from 'lodash';
import dayjs from 'dayjs';
import {PrettyOptions} from "pino";
import {calculateLevel, LevelCode, levelsColors, subSystemLevels} from "../levels";
import { padLeft, padRight } from '../utils';
export const CUSTOM_PAD_LENGTH = 1;
export const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
export function isObject(obj: unknown): boolean {
return _.isObject(obj) && _.isNull(obj) === false && _.isArray(obj) === false;
}
export function formatLoggingDate(time: number, message): string {
const timeFormatted = dayjs(time).format(FORMAT_DATE);
return `[${timeFormatted}]${message}`;
}
export interface PrettyOptionsExtended extends PrettyOptions {
prettyStamp: boolean;
}
let LEVEL_VALUE_MAX = 0;
// eslint-disable-next-line guard-for-in
for (const l in levelsColors) {
LEVEL_VALUE_MAX = Math.max(LEVEL_VALUE_MAX, l.length);
}
const ERROR_FLAG = '!';
export interface ObjectTemplate {
level: LevelCode;
msg: string;
sub?: string;
[key: string]: string | number | object | null | void;
}
export function fillInMsgTemplate(msg, templateOptions: ObjectTemplate, colors): string {
const templateRegex = /@{(!?[$A-Za-z_][$0-9A-Za-z\._]*)}/g;
return msg.replace(templateRegex, (_, name): string => {
let str = templateOptions;
let isError;
if (name[0] === ERROR_FLAG) {
name = name.substr(1);
isError = true;
}
// object can be @{foo.bar.}
const listAccessors = name.split('.');
for (let property = 0; property < listAccessors.length; property++) {
const id = listAccessors[property];
if (isObject(str)) {
str = (str as object)[id];
}
}
if (typeof str === 'string') {
if (colors === false || (str as string).includes('\n')) {
return str;
} else if (isError) {
return red(str);
}
return green(str);
}
// object, showHidden, depth, colors
return inspect(str, undefined, null, colors);
});
}
function getMessage(debugLevel, msg, sub, templateObjects, hasColors) {
const finalMessage = fillInMsgTemplate(msg, templateObjects, hasColors);
const subSystemType = subSystemLevels.color[sub ?? 'default'];
if (hasColors) {
const logString = `${levelsColors[debugLevel](padRight(debugLevel, LEVEL_VALUE_MAX))}${white(`${subSystemType} ${finalMessage}`)}`;
return padLeft(logString);
}
const logString = `${padRight(debugLevel, LEVEL_VALUE_MAX)}${subSystemType} ${finalMessage}`;
return padRight(logString);
}
export function printMessage(
templateObjects: ObjectTemplate,
options: PrettyOptionsExtended,
hasColors = true): string {
const { prettyStamp } = options;
const { level, msg, sub } = templateObjects;
const debugLevel = calculateLevel(level);
const logMessage = getMessage(debugLevel, msg, sub, templateObjects, hasColors);
return prettyStamp ? formatLoggingDate(templateObjects.time as number, logMessage) : logMessage;
}

1
src/lib/logger/index.ts Normal file
View file

@ -0,0 +1 @@
export { setup, createLogger, logger } from './logger';

View file

@ -1,51 +1,56 @@
import { yellow, green, black, blue, red, magenta, cyan, white } from 'kleur';
import { yellow, green, red, magenta, black, blue, cyan, white } from 'kleur';
// level to color
export const levels = {
fatal: red,
error: red,
warn: yellow,
http: magenta,
info: cyan,
debug: green,
trace: white
};
export type LogLevel = 'trace' | 'debug' | 'info' | 'http' | 'warn' | 'error' | 'fatal';
/**
* Match the level based on buyan severity scale
* @param {*} x severity level
* @return {String} security level
*/
export function calculateLevel(x) {
switch (true) {
case x < 15:
return 'trace';
case x < 25:
return 'debug';
case x < 35:
return 'info';
case x == 35:
return 'http';
case x < 45:
return 'warn';
case x < 55:
return 'error';
default:
return 'fatal';
}
export type LevelCode = number;
export function calculateLevel(levelCode: LevelCode): LogLevel {
switch (true) {
case levelCode < 15:
return 'trace';
case levelCode < 25:
return 'debug';
case levelCode < 35:
return 'info';
case levelCode == 35:
return 'http';
case levelCode < 45:
return 'warn';
case levelCode < 55:
return 'error';
default:
return 'fatal';
}
}
export const subsystems = [
{
in: green('<--'),
out: yellow('-->'),
fs: black('-=-'),
default: blue('---')
export const levelsColors = {
fatal: red,
error: red,
warn: yellow,
http: magenta,
info: cyan,
debug: green,
trace: white,
};
enum ARROWS {
LEFT = '<--',
RIGHT = '-->',
EQUAL = '-=-',
NEUTRAL = '---'
}
export const subSystemLevels = {
color: {
in: green(ARROWS.LEFT),
out: yellow(ARROWS.RIGHT),
fs: black(ARROWS.EQUAL),
default: blue(ARROWS.NEUTRAL),
},
{
in: '<--',
out: '-->',
fs: '-=-',
default: '---'
}
];
white: {
in: ARROWS.LEFT,
out: ARROWS.RIGHT,
fs: ARROWS.EQUAL,
default: ARROWS.NEUTRAL,
},
};

146
src/lib/logger/logger.ts Normal file
View file

@ -0,0 +1,146 @@
import pino from 'pino';
import _ from 'lodash';
import buildDebug from 'debug';
import { yellow } from 'kleur';
import { padLeft } from './utils';
function isProd() {
return process.env.NODE_ENV === 'production';
}
export let logger;
const debug = buildDebug('verdaccio:logger');
const DEFAULT_LOG_FORMAT = isProd() ? 'json' : 'pretty';
export type LogPlugin = {
dest: string;
options?: any[];
};
export type LogType = 'file' | 'stdout';
export type LogFormat = 'json' | 'pretty-timestamped' | 'pretty';
export function createLogger(
options = {},
destination = pino.destination(1),
format: LogFormat = DEFAULT_LOG_FORMAT,
prettyPrintOptions = {
// we hide warning since the prettifier should not be used in production
// https://getpino.io/#/docs/pretty?id=prettifier-api
suppressFlushSyncWarning: true,
}
) {
if (_.isNil(format)) {
format = DEFAULT_LOG_FORMAT;
}
let pinoConfig = {
...options,
customLevels: {
http: 35,
},
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
},
};
debug('has prettifier? %o', !isProd());
// pretty logs are not allowed in production for performance reasons
if ((format === DEFAULT_LOG_FORMAT || format !== 'json') && isProd() === false) {
pinoConfig = Object.assign({}, pinoConfig, {
// more info
// https://github.com/pinojs/pino-pretty/issues/37
prettyPrint: {
levelFirst: true,
prettyStamp: format === 'pretty-timestamped',
...prettyPrintOptions,
},
prettifier: require('./formatter'),
});
}
return pino(pinoConfig, destination);
}
export function getLogger() {
if (_.isNil(logger)) {
console.warn('logger is not defined');
return;
}
return logger;
}
const DEFAULT_LOGGER_CONF: LoggerConfigItem = {
type: 'stdout',
format: 'pretty',
level: 'http',
};
export type LoggerConfigItem = {
type?: LogType;
plugin?: LogPlugin;
format?: LogFormat;
path?: string;
level?: string;
};
export type LoggerConfig = LoggerConfigItem[];
export function setup(options: LoggerConfig | LoggerConfigItem = [DEFAULT_LOGGER_CONF]) {
debug('setup logger');
const isLegacyConf = Array.isArray(options);
if (isLegacyConf) {
const deprecateMessage = 'deprecate: multiple logger configuration is deprecated, please check the migration guide.';
// eslint-disable-next-line no-console
console.log(yellow(padLeft(deprecateMessage)));
}
// verdaccio 5 does not allow multiple logger configuration
// backward compatible, pick only the first option
// next major will thrown an error
let loggerConfig = isLegacyConf ? options[0] : options;
if (!loggerConfig?.level) {
loggerConfig = Object.assign({}, loggerConfig, {
level: 'http',
});
}
const pinoConfig = { level: loggerConfig.level };
if (loggerConfig.type === 'file') {
debug('logging file enabled');
logger = createLogger(pinoConfig, pino.destination(loggerConfig.path), loggerConfig.format);
} else if (loggerConfig.type === 'rotating-file') {
// eslint-disable-next-line no-console
console.log(yellow(padLeft('rotating-file type is not longer supported, consider use [logrotate] instead')));
// eslint-disable-next-line no-console
console.log(yellow(padLeft('fallback to stdout configuration triggered')));
debug('logging stdout enabled');
logger = createLogger(pinoConfig, pino.destination(1), loggerConfig.format);
} else {
debug('logging stdout enabled');
logger = createLogger(pinoConfig, pino.destination(1), loggerConfig.format);
}
if (isProd()) {
// why only on prod? https://github.com/pinojs/pino/issues/920#issuecomment-710807667
const finalHandler = pino.final(logger, (err, finalLogger, event) => {
finalLogger.info(`${event} caught`);
if (err) {
finalLogger.error(err, 'error caused exit');
}
process.exit(err ? 1 : 0);
});
process.on('uncaughtException', (err) => finalHandler(err, 'uncaughtException'));
process.on('unhandledRejection', (err) => finalHandler(err as Error, 'unhandledRejection'));
process.on('beforeExit', () => finalHandler(null, 'beforeExit'));
process.on('exit', () => finalHandler(null, 'exit'));
process.on('uncaughtException', (err) => finalHandler(err, 'uncaughtException'));
process.on('SIGINT', () => finalHandler(null, 'SIGINT'));
process.on('SIGQUIT', () => finalHandler(null, 'SIGQUIT'));
process.on('SIGTERM', () => finalHandler(null, 'SIGTERM'));
}
}

View file

@ -1,7 +1,16 @@
import dayjs from 'dayjs';
export const FORMAT_DATE = 'YYYY-MM-DD HH:mm:ss';
export const CUSTOM_PAD_LENGTH = 1;
export function formatLoggingDate(time: string): string {
return dayjs(time).format(FORMAT_DATE);
}
export function padLeft(message: string) {
return message.padStart(message.length + CUSTOM_PAD_LENGTH, ' ');
}
export function padRight(message: string, max = message.length + CUSTOM_PAD_LENGTH ) {
return message.padEnd(max, ' ');
}

View file

@ -55,7 +55,7 @@ class Storage implements IStorageHandler {
public constructor(config: Config) {
this.config = config;
this.uplinks = setupUpLinks(config);
this.logger = logger.child();
this.logger = logger.child({module: 'storage'});
this.filters = [];
// @ts-ignore
this.localStorage = null;

View file

@ -1,24 +1,19 @@
storage: ./storage
#store:
# memory:
# limit: 1000
auth:
htpasswd:
file: ./htpasswd
max_users: -1
web:
enable: true
enable: false
title: verdaccio-e2e-pkg
uplinks:
npmjs:
url: https://registry.verdaccio.org/
logs:
- { type: stdout, format: pretty, level: warn }
logs: { type: stdout, format: json, level: trace }
packages:
'@*/*':
@ -34,4 +29,5 @@ packages:
publish: $anonymous
unpublish: $authenticated
proxy: npmjs
_debug: true

View file

@ -17,8 +17,7 @@ uplinks:
local:
url: http://localhost:4873
logs:
- { type: stdout, format: pretty, level: warn }
logs: { type: stdout, format: json, level: warn }
packages:
'@*/*':

View file

@ -1,30 +1,44 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import { yellow } from 'kleur';
import { green } from 'kleur';
import { spawn } from 'child_process';
import { npm } from '../utils/process';
import * as __global from '../utils/global.js';
module.exports = async () => {
const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'verdaccio-cli-e2e-'));
const tempConfigFile = path.join(tempRoot, 'verdaccio.yaml');
__global.addItem('dir-root', tempRoot);
console.log(yellow(`Add temp root folder: ${tempRoot}`));
fs.copyFileSync(
path.join(__dirname, '../config/_bootstrap_verdaccio.yaml'),
path.join(tempRoot, 'verdaccio.yaml')
);
console.log(green(`Global temp root folder: ${tempRoot}`));
fs.copyFileSync(path.join(__dirname, '../config/_bootstrap_verdaccio.yaml'), tempConfigFile);
console.log(green(`global temp root conf: ${tempConfigFile}`));
// @ts-ignore
global.__namespace = __global;
console.log(`current directory: ${process.cwd()}`);
const rootVerdaccio = path.resolve('./bin/verdaccio');
console.log(green(`verdaccio root path: ${rootVerdaccio}`));
// @ts-ignore
global.registryProcess = spawn(
'node',
[path.resolve('./bin/verdaccio'), '-c', './verdaccio.yaml'],
// @ts-ignore
{ cwd: tempRoot, silence: false }
);
global.registryProcess = spawn('node', [path.resolve('./bin/verdaccio'), '-c', './verdaccio.yaml'], {
cwd: tempRoot,
// stdio: 'pipe'
});
// @ts-ignore
global.registryProcess.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
// @ts-ignore
global.registryProcess.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
// @ts-ignore
global.registryProcess.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
// publish current build version on local registry
await npm('publish', '--registry', 'http://localhost:4873');
await npm('publish', '--registry', 'http://localhost:4873', '--verbose');
};

View file

@ -8,6 +8,7 @@ export function installVerdaccio(verdaccioInstall) {
'verdaccio',
'--registry',
'http://localhost:4873',
'--no-package-lock'
'--no-package-lock',
'--verbose'
);
}

View file

@ -26,7 +26,7 @@ describe('npm install', () => {
registryProcess = await spawnRegistry(pathVerdaccioModule, ['-c', configPath, '-l', port], {
cwd: verdaccioInstall,
silent: true
silent: false
});
const body = await callRegistry(`http://localhost:${port}/verdaccio`);

View file

@ -1,7 +1,7 @@
// we need this for notifications
import { setup } from '../../src/lib/logger';
setup([]);
setup({});
import { IServerBridge } from '../types';

View file

@ -29,8 +29,7 @@ uplinks:
baduplink:
url: http://localhost:55666/
logs:
- { type: stdout, format: pretty, level: trace }
logs: { type: stdout, format: pretty, level: trace }
packages:
'@test/*':

View file

@ -30,8 +30,7 @@ auth:
name: authtest
password: blahblah-password
logs:
- { type: stdout, format: pretty, level: trace }
logs: { type: stdout, format: pretty, level: trace }
packages:
'@test/*':

View file

@ -18,8 +18,7 @@ auth:
name: test
password: test
logs:
- { type: stdout, format: pretty, level: trace }
logs: { type: stdout, format: pretty, level: trace }
packages:
'pkg-gh131':

View file

@ -22,7 +22,7 @@ class ExampleAuthPlugin implements IPluginAuth<{}> {
this.logger = options.logger;
}
adduser(user: string, password: string, cb: Callback): void {
adduser(user: string, password: string, cb: Callback): void { // pragma: allowlist secret
cb();
}
@ -30,7 +30,7 @@ class ExampleAuthPlugin implements IPluginAuth<{}> {
cb();
}
authenticate(user: string, password: string, cb: Callback): void {
authenticate(user: string, password: string, cb: Callback): void { // pragma: allowlist secret
cb();
}
@ -84,7 +84,7 @@ const config1: AppConfig = new Config({
const options: PluginOptions<{}> = {
config: config1,
logger: logger.child()
logger: logger.child({sub: 'out'})
};
const auth = new ExampleAuthPlugin(config1, options);

View file

@ -52,10 +52,9 @@ const checkDefaultConfPackages = (config) => {
expect(config.middlewares.audit).toBeDefined();
expect(config.middlewares.audit.enabled).toBeTruthy();
// logs
expect(config.logs).toBeDefined();
expect(config.logs[0].type).toEqual('stdout');
expect(config.logs[0].format).toEqual('pretty');
expect(config.logs[0].level).toEqual('http');
expect(config.logs.type).toEqual('stdout');
expect(config.logs.format).toEqual('pretty');
expect(config.logs.level).toEqual('http');
// must not be enabled by default
expect(config.notify).toBeUndefined();
expect(config.store).toBeUndefined();

View file

@ -1,127 +0,0 @@
import { fillInMsgTemplate } from '../../../../src/lib/logger/formatter';
import { LOG_VERDACCIO_ERROR, LOG_VERDACCIO_BYTES } from '../../../../src/api/middleware';
import { HTTP_STATUS } from '@verdaccio/commons-api';
// the following mocks avoid use colors, thus the strings can be matched
jest.mock('kleur', () => {
// we emulate colors with this pattern color[msg]
return {
green: (r) => `g[${r}]`,
yellow: (r) => `y[${r}]`,
black: (r) => `b[${r}]`,
blue: (r) => `bu[${r}]`,
red: (r) => `r[${r}]`,
cyan: (r) => `c[${r}]`,
magenta: (r) => `m[${r}]`,
white: (r) => `w[${r}]`
};
});
jest.mock('util', () => {
// we need to override only one method, but still we need others
const originalModule = jest.requireActual('util');
return {
...originalModule,
inspect: (r) => r
};
});
describe('Logger Parser', () => {
describe('basic messages', () => {
test('number object property', () => {
expect(fillInMsgTemplate('foo:@{foo}', { foo: 1 }, false)).toEqual('foo:1');
});
test('string object property', () => {
expect(fillInMsgTemplate('foo:@{foo}', { foo: 'bar' }, false)).toEqual('foo:bar');
});
test('empty message no object property', () => {
expect(fillInMsgTemplate('foo', undefined, false)).toEqual('foo');
});
test('string no object property', () => {
expect(fillInMsgTemplate('foo', null, false)).toEqual('foo');
});
test('string no object property with break line', () => {
expect(fillInMsgTemplate('foo \n bar', null, false)).toEqual('foo \n bar');
});
test('string no object property with colors', () => {
expect(fillInMsgTemplate('foo', null, true)).toEqual('foo');
});
test('string object property with colors', () => {
expect(fillInMsgTemplate('foo:@{foo}', { foo: 'bar' }, true)).toEqual(`foo:${'g[bar]'}`);
});
});
describe('middleware log messages', () => {
describe('test errors log', () => {
const middlewareObject = {
name: 'verdaccio',
request: {
method: 'POST',
url: '/-/npm/v1/user'
},
user: 'userTest2001',
remoteIP: '::ffff:127.0.0.1',
status: HTTP_STATUS.UNAUTHORIZED,
error: 'some error',
msg:
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}', error: @{!error}"
};
test('should display error log', () => {
const expectedErrorMessage = `401, user: userTest2001(::ffff:127.0.0.1), req: 'POST /-/npm/v1/user', error: some error`;
expect(fillInMsgTemplate(LOG_VERDACCIO_ERROR, middlewareObject, false)).toEqual(
expectedErrorMessage
);
});
test('should display error log with colors', () => {
const expectedErrorMessage = `401, user: g[userTest2001](g[::ffff:127.0.0.1]), req: 'g[POST] g[/-/npm/v1/user]', error: r[some error]`;
expect(fillInMsgTemplate(LOG_VERDACCIO_ERROR, middlewareObject, true)).toEqual(
expectedErrorMessage
);
});
});
describe('test bytes log', () => {
const middlewareObject = {
name: 'verdaccio',
hostname: 'macbook-touch',
pid: 85621,
sub: 'in',
level: 35,
request: {
method: 'PUT',
url: '/-/user/org.couchdb.user:userTest2002'
},
user: 'userTest2002',
remoteIP: '::ffff:127.0.0.1',
status: 201,
error: undefined,
bytes: { in: 50, out: 405 },
msg:
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}', bytes: @{bytes.in}/@{bytes.out}",
time: '2019-07-20T11:31:49.939Z',
v: 0
};
test('should display log with bytes', () => {
expect(fillInMsgTemplate(LOG_VERDACCIO_BYTES, middlewareObject, false)).toEqual(
`201, user: userTest2002(::ffff:127.0.0.1), req: 'PUT /-/user/org.couchdb.user:userTest2002', bytes: 50/405`
);
});
test('should display log with bytes with colors', () => {
expect(fillInMsgTemplate(LOG_VERDACCIO_BYTES, middlewareObject, true)).toEqual(
`201, user: g[userTest2002](g[::ffff:127.0.0.1]), req: 'g[PUT] g[/-/user/org.couchdb.user:userTest2002]', bytes: 50/405`
);
});
});
});
});

BIN
yarn.lock

Binary file not shown.