How can I add line number to this following winston-based logger?
So that the line number appears at the end of the file path.
const winston = require('winston');
winston.emitErrs = true;
const logger = function (module) {
let parts, path;
parts = module.filename.split('\\');
path = parts[parts.length - 2] + '\\' + parts.pop();
return new winston.Logger({
transports: [
new winston.transports.Console({
level: 'debug',
handleExceptions: true,
json: false,
colorize: true,
label: path,
timestamp: true
})
],
exitOnError: false
});
};
module.exports = logger;
update
current output example is like:
2018-06-10T00:13:33.344Z - info: [app\main.js] Here is my log
the desired format is:
2018-06-10T00:13:33.344Z - info: [app\main.js:150] Here is my log
in which 150 is appended to the file path.
You can use this code in a new file winston.js. and the Use it by requiring it anywhere.
var winston = require('winston')
var path = require('path')
var PROJECT_ROOT = path.join(__dirname, '..')
var appRoot = require('app-root-path');
const options = {
file: {
level: 'info',
filename: `${appRoot}/logs/app.log`,
handleExceptions: true,
json: true,
maxsize: 5242880, // 5MB
maxFiles: 5,
colorize: false,
timestamp: true
},
console: {
level: 'debug',
handleExceptions: true,
json: true,
colorize: true,
timestamp: true
}
};
var logger = new winston.Logger({
transports: [
new winston.transports.File(options.file),
new winston.transports.Console(options.console)
],
exitOnError: false // do not exit on handled exceptions
});
logger.stream = {
write: function (message) {
logger.info(message)
}
}
// A custom logger interface that wraps winston, making it easy to instrument
// code and still possible to replace winston in the future.
module.exports.debug = module.exports.log = function () {
logger.debug.apply(logger, formatLogArguments(arguments))
}
module.exports.info = function () {
logger.info.apply(logger, formatLogArguments(arguments))
}
module.exports.warn = function () {
logger.warn.apply(logger, formatLogArguments(arguments))
}
module.exports.error = function () {
logger.error.apply(logger, formatLogArguments(arguments))
}
module.exports.stream = logger.stream
/**
* Attempts to add file and line number info to the given log arguments.
*/
function formatLogArguments (args) {
args = Array.prototype.slice.call(args)
var stackInfo = getStackInfo(1)
if (stackInfo) {
// get file path relative to project root
var calleeStr = '(' + stackInfo.relativePath + ':' + stackInfo.line + ')'
if (typeof (args[0]) === 'string') {
args[0] = calleeStr + ' ' + args[0]
} else {
args.unshift(calleeStr)
}
}
return args
}
/**
* Parses and returns info about the call stack at the given index.
*/
function getStackInfo (stackIndex) {
// get call stack, and analyze it
// get all file, method, and line numbers
var stacklist = (new Error()).stack.split('\n').slice(3)
// stack trace format:
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
// do not remove the regex expresses to outside of this method (due to a BUG in node.js)
var stackReg = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/gi
var stackReg2 = /at\s+()(.*):(\d*):(\d*)/gi
var s = stacklist[stackIndex] || stacklist[0]
var sp = stackReg.exec(s) || stackReg2.exec(s)
if (sp && sp.length === 5) {
return {
method: sp[1],
relativePath: path.relative(PROJECT_ROOT, sp[2]),
line: sp[3],
pos: sp[4],
file: path.basename(sp[2]),
stack: stacklist.join('\n')
}
}
}
Related
I am quite new to nodejs. I am using a small library 'cukefarm'. And the file structure looks like below.
index.js
lib/support/World.js
lib/protractor.conf.js
index.js looks like:
World = require('./lib/support/World');
config = require('./lib/protractor.conf');
module.exports = {
World: World,
config: config
}
World.js looks like:
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
function World() {
this.Q = require('q');
this.currentPage = null;
this.pageObjectMap = null;
chai.use(chaiAsPromised);
this.expect = chai.expect;
};
module.exports = World;
console.log('\n' + __filename);
console.log('World ' + JSON.stringify(World, null, " "));
protractor.conf.js looks like:
var path = require('path');
module.exports = {
framework: 'custom',
frameworkPath: require.resolve('protractor-cucumber-framework'),
capabilities: {
'chromeOptions': {
args: ['--test-type']
}
},
cucumberOpts: {
require: [
path.resolve('./node_modules/cukefarm/lib/step_definitions/GeneralStepDefs'),
path.resolve('./node_modules/cukefarm/lib/support/Transform'),
path.resolve('./node_modules/cukefarm/lib/support/World')
],
format: []
},
onPrepare: function() {
browser.ignoreSynchronization = true;
browser.manage().timeouts().setScriptTimeout(5000);
return browser.manage().timeouts().implicitlyWait(5000);
}
};
console.log('\n' + __filename);
console.log('module.exports ' + JSON.stringify(module.exports, null, " "));
I try to console log the exports object in both World.js and protractor.conf.js.
protractor.conf works fine, it prints the object no problem.
But World shows log as:
World undefined
P.S. the complete code of 'cukefarm' is located at https://github.com/ReadyTalk/cukefarm
I think you are consoled wrong, You had stringified function not the object
const World = require("./env");
console.log(JSON.stringify(World, null, 4)); // undefined
console.log("World " + JSON.stringify(World, null, " ")); // World undefined
console.log(JSON.stringify(new World(), null, 4)); // test
// env.js
module.exports = function() {
console.log("test")
}
I have a 'message' object, the response from the Gmail API, in my frontend and adapting this very useful Javascript gist I've parsed the message as well.
function indexHeaders(headers) {
if (!headers) {
return {};
} else {
return headers.reduce(function (result, header) {
result[header.name.toLowerCase()] = header.value;
return result;
}, {});
}
}
function urlB64Decode(string) {
encodedBody = string
encodedBody = encodedBody.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '');
return decodeURIComponent(escape(window.atob(encodedBody)));
}
function parseMessage(response) {
var result = {
id: response.id,
threadId: response.threadId,
labelIds: response.labelIds,
snippet: response.snippet,
historyId: response.historyId
};
if (response.internalDate) {
result.internalDate = parseInt(response.internalDate);
}
var payload = response.payload;
if (!payload) {
return result;
}
var headers = indexHeaders(payload.headers);
result.headers = headers;
var parts = [payload];
var firstPartProcessed = false;
while (parts.length !== 0) {
var part = parts.shift();
if (part.parts) {
parts = parts.concat(part.parts);
}
if (firstPartProcessed) {
headers = indexHeaders(part.headers);
}
if (!part.body) {
continue;
}
var isHtml = part.mimeType && part.mimeType.indexOf('text/html') !== -1;
var isPlain = part.mimeType && part.mimeType.indexOf('text/plain') !== -1;
var isAttachment = headers['content-disposition'] && headers['content-disposition'].indexOf('attachment') !== -1;
var isInline = headers['content-disposition'] && headers['content-disposition'].indexOf('inline') !== -1;
if (isHtml && !isAttachment) {
result.textHtml = urlB64Decode(part.body.data);
} else if (isPlain && !isAttachment) {
result.textPlain = urlB64Decode(part.body.data);
} else if (isAttachment) {
var body = part.body;
if(!result.attachments) {
result.attachments = [];
}
result.attachments.push({
filename: part.filename,
mimeType: part.mimeType,
size: body.size,
attachmentId: body.attachmentId,
headers: indexHeaders(part.headers)
});
} else if (isInline) {
var body = part.body;
if(!result.inline) {
result.inline = [];
}
result.inline.push({
filename: part.filename,
mimeType: part.mimeType,
size: body.size,
attachmentId: body.attachmentId,
headers: indexHeaders(part.headers)
});
}
firstPartProcessed = true;
}
return result;
};
So, after parsing the response, it's now in this format:
{
// id: '{MESSAGE_ID}',
// threadId: '{THREAD_ID}',
// labelIds: [ 'SENT', 'INBOX', 'UNREAD' ],
// snippet: 'This is one cool message, buddy.',
// historyId: '701725',
// internalDate: 1451995756000,
// attachments: [{
// filename: 'example.jpg',
// mimeType: 'image/jpeg',
// size: 100446,
// attachmentId: '{ATTACHMENT_ID}',
// headers: {
// 'content-type': 'image/jpeg; name="example.jpg"',
// 'content-description': 'example.jpg',
// 'content-transfer-encoding': 'base64',
// 'content-id': '...',
// ...
// }
// }],
// inline: [{
// filename: 'example.png',
// mimeType: 'image/png',
// size: 5551,
// attachmentId: '{ATTACHMENT_ID}',
// headers: {
// 'content-type': 'image/jpeg; name="example.png"',
// 'content-description': 'example.png',
// 'content-transfer-encoding': 'base64',
// 'content-id': '...',
// ...
// }
// }],
// headers: {
// subject: 'Example subject',
// from: 'Example Name <example#gmail.com>',
// to: '<foo#gmail.com>, Foo Bar <fooBar#gmail.com>',
// ...
// },
// textPlain: '<div dir="ltr">hey! 😅<div><br></div><div><div><img src="cid:ii_k0l2i10d0" alt="Image_Name 2019-09-11 at 20.47.16.jpeg" width="452" height="339"><br></div></div></div>',
// textHtml: '<div dir="ltr">hey! 😅<div><br></div><div><div><img src="cid:ii_k0l2i10d0" alt="Image_Name 2019-09-11 at 20.47.16.jpeg" width="452" height="339"><br></div></div></div>'
// }
I'm having difficulty rendering the actual inline image / attachment in html though. If I use the 'textHtml' that comes through in the parsedMessage, it renders as something like <img src='cid:ii_k0l2i10d0'> in the html, which doesn't display the image.
This Stackoverflow answer was helpful but I need help with the last part. I have the attachmentID now. It says "Get the attachment from the Gmail API and replace the cid with the base64-data" which is the part I'm struggling with. How do I do that? -- Do I have to make another call to the Gmail API for this? Or am I going wrong somewhere in the decoding?
Thanks so much for any help!
I finally got it working -- so the answer is, yes, I did have to make another call to the API to get the attachments data. That returns an attachment resource that looks like this:
{
"attachmentId": string,
"size": integer,
"data": bytes
}
Once I get the response:
// construct the new src with the data from the response
newSrc = "data:" + "image/jpeg" + ";" + "base64" + "," + response.data.replace(/-/g, `+`).replace(/_/g, `/`)
// replace the src of the image with the new src
thisImg.attr({"src":newSrc});
I am looking the way how can I post out only "INFO" level (for user) to console, and "DEBUG" level post to the file. Currently I found only one one work solution - using few "getLogger()" functions. For example:
log4js.getLogger('debug');
log4js.getLogger('info');
where each parameter of the function separate category of the configuration from configure() function.
but I don't like it and suppose that there is better option with one getLogger() function.
Last think I tried was:
log4js.configure({
appenders: {
everything:{ type: 'stdout' },
file_log: { type: 'file' ,filename: 'all-the-logs.log' },
logLevelFilter: { type:'logLevelFilter',level: 'debug', appender: 'file_log' }
},
categories: {
default: {
appenders: [ 'logLevelFilter','everything'], level: 'info'},
}
});
I see in console out - only "INFO" level, but in file I also see only "INFO" level. In case If I add to appender - level "ALL" - I will see in console out all levels and the same will be in file
log4js provided special append typed categoryFilter and logLevelFilter, see Log4js - Appenders, step as below
Add appender and category based on each log level;
Add extra appender typed logLevelFilter pointing to the related appender;
Change config.categories.default.appenders using appenders in step 2;
Finally use log4js.getLogger() create logger and logging everywhere, log file separated by love level
Written in typescript
import {Appender, Configuration, LogLevelFilterAppender} from "log4js";
const fs = require('fs');
const path = require('path');
let levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
let conf: Configuration = {
appenders : {
default: {
type : "console",
layout: {
type : "colored",
pattern: "%m"
},
}
},
categories: {
default: {
appenders: levels,
level : 'all'
}
},
};
let logs = path.join(__dirname, 'logs');
if (!fs.existsSync(logs)) {
fs.mkdirSync(logs);
}
for (let level of levels) {
let appender: Appender = {
type : "file",
filename : path.join(logs, level + '.log'),
maxLogSize: 65536,
backups : 10,
layout : {
type : "pattern",
pattern: "%d{dd/MM hh:mm} %-5p %m"
}
};
conf.appenders[level] = appender;
conf.categories[level] = {
appenders: [level],
level : level.toUpperCase(),
}
}
let prefix = 'only-';
for (let level of levels) {
let appender: LogLevelFilterAppender = {
type : "logLevelFilter",
appender: level,
level : level,
maxLevel: level,
};
let name = prefix + level;
conf.appenders[name] = appender;
conf.categories[name] = {
appenders: [level],
level : level.toUpperCase(),
}
}
conf.categories.default.appenders = levels.map(x => prefix + x);
//console.log(JSON.stringify(conf, null, 2));
export default conf;
sample usage
// const log4js = require('log4js');
import * as log4js from 'log4js';
import log4jsConfig from './log4js.config';
log4js.configure(log4jsConfig );
(function () {
const logger = log4js.getLogger();
logger.trace('Entering cheese testing');
logger.debug('Got cheese.');
logger.info('Cheese is Gouda.');
logger.warn('Cheese is quite smelly.');
logger.error('Cheese is too ripe!');
logger.fatal('Cheese was breeding ground for listeria.');
log4js.shutdown(console.log);
})();
I am trying to set the browser name with protractor test suite name. Because I am using allure reporter for test report. But the problem is I am getting browser name 'undefined' all the time. So far I have implemented browser name in an asynchronous method and I would like to use that method in suitstarted. the suitstarted method is synchronous. I cannot set suit started to asynchronous. In that way how can I wait suitstarted method and get the browser name after the suit.fullname. I am really stuck with this issue from last three days hence I am very new in Javascript.
Here is my code
require('protractor/built/logger').Logger.logLevel = 3;
exports.config = {
sync: false,
multiCapabilities:[
{
'browserName' : 'chrome',
'chromeOptions': { 'args' : ['--disable-extensions']},
'shardTestFiles': true,
'maxInstances': 1,
'unexpectedAlertBehaviour' : 'accept'
},
{
'browserName' : 'firefox',
},
{
'browserName': 'internet explorer',
'se:ieOptions': {
ignoreProtectedModeSettings: true,
'ie.ensureCleanSession': true,
}
},
],
jvmArgs: ['-Dwebdriver.ie.driver=./node_modules/webdriver-manager/selenium/IEDriverServer3.141.0.exe'],
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
isVerbose: true,
showColors: true,
defaultTimeoutInterval: 1500000
},
useAllAngular2AppRoots: true,
beforeLaunch: function() {
},
onPrepare: function() {
// ------------------------------------------------------
var Allure = require('allure-js-commons');
var path = require('path');
var allure = new Allure();
var AllureReporter = function CustomJasmine2AllureReporter(userDefinedConfig, allureReporter) {
var Status = {PASSED: 'passed', FAILED: 'failed', BROKEN: 'broken', PENDING: 'pending'};
this.allure = allureReporter || allure;
this.configure = function(userDefinedConfig) {
var pluginConfig = {};
userDefinedConfig = userDefinedConfig || {};
pluginConfig.resultsDir = userDefinedConfig.resultsDir || 'allure-results';
pluginConfig.basePath = userDefinedConfig.basePath || '.';
var outDir = path.resolve(pluginConfig.basePath, pluginConfig.resultsDir);
this.allure.setOptions({targetDir: outDir});
};
this.configure(userDefinedConfig);
var browserNameforSuit;
Edited
this.suiteStarted = function(suite) {
var capsPromise = browser.getCapabilities();
capsPromise.then(function (caps) {
browserNameforSuit = caps.get('browserName');
console.log(browserNameforSuit)
this.allure.startSuite(suite.fullName);
console.log(suite.fullName);
})
};
};
this.suiteDone = function() {
this.allure.endSuite();
};
this.specStarted = function(spec) {
this.allure.startCase(spec.description);
};
this.specDone = function(spec) {
var status = this._getTestcaseStatus(spec.status);
var errorInfo = this._getTestcaseError(spec);
this.allure.endCase(status, errorInfo);
};
this._getTestcaseStatus = function(status) {
if (status === 'disabled' || status === 'pending') {
return Status.PENDING;
} else if (status === 'passed') {
return Status.PASSED;
} else {
return Status.FAILED;
}
};
this._getTestcaseError = function(result) {
if (result.status === 'disabled') {
return {
message: 'This test was ignored',
stack: ''
};
} else if (result.status === 'pending') {
return {
message: result.pendingReason,
stack: ''
};
}
var failure = result.failedExpectations ? result.failedExpectations[0] : null;
if (failure) {
return {
message: failure.message,
stack: failure.stack
};
}
};
}
// ------------------------------------------------------
var Runtime = require('allure-js-commons/runtime');
let gallure = new Runtime(allure);
browser.manage().window().maximize();
require('ts-node').register({
project: 'e2e/tsconfig.json'
});
jasmine.getEnv().addReporter(new AllureReporter ({
resultsDir: 'allure-results'
}));
jasmine.getEnv().addReporter(reporter);
jasmine.getEnv().afterEach(function (done) {
browser.takeScreenshot().then(function (png) {
gallure.createAttachment('Screenshot', function () {
return new Buffer(png, 'base64')
},'image/png')();
done();
})
});
}
};
The index.js for Allure is
'use strict';
var assign = require('object-assign'),
Suite = require('./beans/suite'),
Test = require('./beans/test'),
Step = require('./beans/step'),
Attachment = require('./beans/attachment'),
util = require('./util'),
writer = require('./writer');
function Allure() {
this.suites = [];
this.options = {
targetDir: 'allure-results'
};
}
Allure.prototype.setOptions = function(options) {
assign(this.options, options);
};
Allure.prototype.getCurrentSuite = function() {
return this.suites[0];
};
Allure.prototype.getCurrentTest = function() {
return this.getCurrentSuite().currentTest;
};
Allure.prototype.startSuite = function(suiteName, timestamp) {
this.suites.unshift(new Suite(suiteName,timestamp));
};
Allure.prototype.endSuite = function(timestamp) {
var suite = this.getCurrentSuite();
suite.end(timestamp);
if(suite.hasTests()) {
writer.writeSuite(this.options.targetDir, suite);
}
this.suites.shift();
};
// other methods
module.exports = Allure;
And the Suit.js is
function Suite(name, timestamp){
this.name = name;
this.start = timestamp || Date.now();
this.testcases = [];
}
Suite.prototype.end = function(timestamp) {
this.stop = timestamp || Date.now();
};
Suite.prototype.hasTests = function() {
return this.testcases.length > 0;
};
Suite.prototype.addTest = function(test) {
this.testcases.push(test);
};
Suite.prototype.toXML = function() {
var result = {
'#': {
'xmlns:ns2' : 'urn:model.allure.qatools.yandex.ru',
start: this.start
},
name: this.name,
title: this.name,
'test-cases': {
'test-case': this.testcases.map(function(testcase) {
return testcase.toXML();
})
}
};
if(this.stop) {
result['#'].stop = this.stop;
}
return result;
};
module.exports = Suite;
How can I set browserName at the end of the suit.fullname. Is there any alternative way to do that.
Error
Test Suites & Specs:
1. 0030 Test for correct login
No specs found
Finished in 0.017 seconds
An error was thrown in an afterAll
AfterAll TypeError: Cannot read property 'end' of undefined
>> Done!
Summary:
Finished in 0.017 seconds
chrome
[13:23:03] E/launcher - Cannot read property 'startSuite' of undefined
[13:23:03] E/launcher - TypeError: Cannot read property 'startSuite' of
undefined
at C:\Users\mnowshin\projects\beck-hb\hb-frontend\protractor.mn.conf.js:130:18
at ManagedPromise.invokeCallback_ (\node_modules\selenium-webdriver\lib\promise.js:1376:14)
at TaskQueue.execute_ (\node_modules\selenium-webdriver\lib\promise.js:3084:14)
at TaskQueue.executeNext_ (\node_modules\selenium-webdriver\lib\promise.js:3067:27)
at asyncRun (\node_modules\selenium-webdriver\lib\promise.js:2927:27)
at \node_modules\selenium-webdriver\lib\promise.js:668:7
at process.internalTickCallback (internal/process/next_tick.js:77:7)
[13:23:03] E/launcher - Process exited with error code 199
Process finished with exit code 199
so I don't work with allure, but I can show you how I get the browser name and then you can try to apply to your case.
Try to change from this:
var browserNameforSuit;
let bName = (async () => {
try {
browserNameforSuit = (await browser.getCapabilities()).get('browserName');
return browserNameforSuit;
} catch (err) {
return "Error or smth"
}
})();
To this:
var capsPromise = browser.getCapabilities();
var browserNameforSuit;
capsPromise.then(function (caps) {
browserNameforSuit = caps.get('browserName');
//if you need you can return the result to a var in order to user somewhere else
});
This is the onComplete from my conf.js
onComplete: function()
{
var browserName, browserVersion;
var capsPromise = browser.getCapabilities();
capsPromise.then(function (caps) {
browserName = caps.get('browserName');
browserVersion = caps.get('version');
platform = caps.get('platform');
//here I prepare the testConfig adding what I need//
//here I get the xml with the test results and create an html
new HTMLReport().from('./execution_results/reports/xml/xmlresults.xml', testConfig);
});//End Of capsPromise
},
Hope it helps!
I have set up a simple winston logger in my app like this:
function logger(success, msg) {
let now = new Date().toUTCString()
let logger = new (winston.Logger)({
transports: [
new (winston.transports.File)({
filename: 'log.log',
timestamp: function() {
return new Date().toUTCString();
},
formatter: function(options) {
return `>>>>>>>>>> ${options.timestamp()} - ${options.level.toUpperCase} - ${options.message}`;
}
})
]
});
if (success) {
logger.log('info', msg)
} else {
logger.log('error', msg)
}
}
But instead of logging the formatted string, it outputs the following:
{"level":"error","message":"Nothing to upload","timestamp":"Mon, 23 Apr 2018 13:53:01 GMT"}
Ideas? Am I missing something? (Of course I am)
As you can find out in winston documentation for File you can set property json to false if you don't want information in file to be JSON objects. By default this property is true.
json: If true, messages will be logged as JSON (default true).
Could you try to change your code like this:
function logger(success, msg) {
let now = new Date().toUTCString()
let logger = new (winston.Logger)({
transports: [
new (winston.transports.File)({
filename: 'log.log',
timestamp: function() {
return new Date().toUTCString();
},
json: false,
formatter: function(options) {
return `>>>>>>>>>> ${options.timestamp()} - ${options.level.toUpperCase} - ${options.message}`;
}
})
]
});
if (success) {
logger.log('info', msg)
} else {
logger.log('error', msg)
}
}