expresser
Version:
A ready to use Node.js web app wrapper, built on top of Express.
399 lines (331 loc) • 16.1 kB
text/coffeescript
# EXPRESSER LOGGER
# --------------------------------------------------------------------------
# Handles server logging using local files, Logentries or Loggly.
# Multiple services can be enabled at the same time.
# <!--
# @see Settings.logger
# -->
class Logger
events = require "./events.coffee"
fs = require "fs"
lodash = require "lodash"
moment = require "moment"
path = require "path"
settings = require "./settings.coffee"
utils = require "./utils.coffee"
# Local logging objects will be set on `init`.
bufferDispatcher = null
localBuffer = null
flushing = false
# Remote logging providers will be set on `init`.
logentries = null
loggly = null
loggerLogentries = null
loggerLoggly = null
# The `serverIP` will be set on init, but only if `settings.logger.sendIP` is true.
serverIP = null
# Timer used for automatic logs cleaning.
timerCleanLocal = null
# @property [Method] Custom method to call when logs are sent to logging server or flushed to disk.
onLogSuccess: null
# @property [Method] Custom method to call when errors are triggered by the logging transport.
onLogError: null
# @property [Array] Holds a list of current active logging services.
# @private
activeServices = []
# Holds a copy of emails sent for critical logs.
criticalEmailCache: {}
# # CONSTRUCTOR, INIT AND STOP
# --------------------------------------------------------------------------
# Logger constructor.
constructor: ->
@setEvents() if settings.events.enabled
# Bind event listeners.
setEvents: =>
events.on "Logger.debug", @debug
events.on "Logger.info", @info
events.on "Logger.warn", @warn
events.on "Logger.error", @error
events.on "Logger.critical", @critical
# Init the Logger module. Verify which services are set, and add the necessary transports.
# IP address and timestamp will be appended to logs depending on the settings.
# @param [Object] options Logger init options.
init: (options) =>
bufferDispatcher = null
localBuffer = null
logentries = null
loggly = null
serverIP = null
activeServices = []
# Get a valid server IP to be appended to logs.
if settings.logger.sendIP
serverIP = utils.getServerIP true
# Define server IP.
if serverIP?
ipInfo = "IP #{serverIP}"
else
ipInfo = "No server IP set."
# Init transports.
@initLocal()
@initLogentries()
@initLoggly()
# Check if uncaught exceptions should be logged. If so, try logging unhandled
# exceptions using the logger, otherwise log to the console.
if settings.logger.uncaughtException
@debug "Logger.init", "Catching unhandled exceptions."
process.on "uncaughtException", (err) =>
try
@error "Unhandled exception!", err.message, err.stack
catch ex
console.error "Unhandled exception!", err.message, err.stack, ex
# Start logging!
if not localBuffer? and not logentries? and not loggly?
@warn "Logger.init", "No transports enabled.", "Logger module will only log to the console!"
else
@info "Logger.init", activeServices.join(), ipInfo
# Init the Local transport. Check if logs should be saved locally. If so, create the logs buffer
# and a timer to flush logs to disk every X milliseconds.
initLocal: =>
if settings.logger.local.enabled
if fs.existsSync?
folderExists = fs.existsSync settings.path.logsDir
else
folderExists = path.existsSync settings.path.logsDir
# Create logs folder, if it doesn't exist.
if not folderExists
fs.mkdirSync settings.path.logsDir
if settings.general.debug
console.log "Logger.initLocal", "Created #{settings.path.logsDir} folder."
# Set local buffer.
localBuffer = {info: [], warn: [], error: []}
bufferDispatcher = setInterval @flushLocal, settings.logger.local.bufferInterval
activeServices.push "Local"
# Check the maxAge of local logs.
if settings.logger.local.maxAge? and settings.logger.local.maxAge > 0
if timerCleanLocal?
clearInterval timerCleanLocal
timerCleanLocal = setInterval @cleanLocal, 86400
else
@stopLocal()
# Init the Logentries transport. Check if Logentries should be used, and create the Logentries objects.
initLogentries: =>
if settings.logger.logentries.enabled and settings.logger.logentries.token? and settings.logger.logentries.token isnt ""
logentries = require "node-logentries"
loggerLogentries = logentries.logger {token: settings.logger.logentries.token, timestamp: settings.logger.sendTimestamp}
loggerLogentries.on("log", @onLogSuccess) if lodash.isFunction @onLogSuccess
loggerLogentries.on("error", @onLogError) if lodash.isFunction @onLogError
activeServices.push "Logentries"
else
@stopLogentries()
# Init the Loggly transport. Check if Loggly should be used, and create the Loggly objects.
initLoggly: =>
if settings.logger.loggly.enabled and settings.logger.loggly.subdomain? and settings.logger.loggly.token? and settings.logger.loggly.token isnt ""
loggly = require "loggly"
loggerLoggly = loggly.createClient {token: settings.logger.loggly.token, subdomain: settings.logger.loggly.subdomain, json: false}
activeServices.push "Loggly"
else
@stopLoggly()
# Disable and remove Local transport from the list of active services.
stopLocal: =>
@flushLocal()
clearInterval bufferDispatcher if bufferDispatcher?
bufferDispatcher = null
localBuffer = null
i = activeServices.indexOf "Local"
activeServices.splice(i, 1) if i >= 0
# Disable and remove Logentries transport from the list of active services.
stopLogentries: =>
logentries = null
loggerLogentries = null
i = activeServices.indexOf "Logentries"
activeServices.splice(i, 1) if i >= 0
# Disable and remove Loggly transport from the list of active services.
stopLoggly: =>
loggly = null
loggerLoggly = null
i = activeServices.indexOf "Loggly"
activeServices.splice(i, 1) if i >= 0
# LOG METHODS
# --------------------------------------------------------------------------
# Generic log method.
# @param [String] logType The log type (for example: warning, error, info, security, etc).
# @param [String] logFunc Optional, the logging function name to be passed to the console and Logentries.
# @param [Array] args Array of arguments to be stringified and logged.
log: (logType, logFunc, args) =>
if not args? and logFunc?
args = logFunc
logFunc = "info"
# Get message out of the arguments.
msg = @getMessage args
# Log to different transports.
if settings.logger.local.enabled and localBuffer?
@logLocal logType, msg
if settings.logger.logentries.enabled and loggerLogentries?
loggerLogentries.log logFunc, msg
if settings.logger.loggly.enabled and loggerLoggly?
loggerLoggly.log msg, @logglyCallback
# Log to the console depending on `console` setting.
if settings.logger.console
args.unshift moment().format "HH:mm:ss.SS"
if settings.logger.errorLogTypes.indexOf(logType) >= 0
console.error.apply this, args
else
console.log.apply this, args
# Log to the active transports as `debug`, only if the debug flag is enabled.
# All arguments are transformed to readable strings.
debug: =>
return if not settings.general.debug
args = Array.prototype.slice.call arguments
args.unshift "DEBUG"
@log "debug", "info", args
# Log to the active transports as `log`.
# All arguments are transformed to readable strings.
info: =>
return if settings.logger.levels.indexOf("info") < 0
args = Array.prototype.slice.call arguments
args.unshift "INFO"
@log "info", "info", args
# Log to the active transports as `warn`.
# All arguments are transformed to readable strings.
warn: =>
return if settings.logger.levels.indexOf("warn") < 0
args = Array.prototype.slice.call arguments
args.unshift "WARN"
@log "warn", "warning", args
# Log to the active transports as `error`.
# All arguments are transformed to readable strings.
error: =>
return if settings.logger.levels.indexOf("error") < 0
args = Array.prototype.slice.call arguments
args.unshift "ERROR"
@log "error", "err", args
# Log to the active transports as `critical`.
# All arguments are transformed to readable strings.
critical: =>
return if settings.logger.levels.indexOf("critical") < 0
args = Array.prototype.slice.call arguments
args.unshift "CRITICAL"
@log "critical", "err", args
# If the `criticalEmailTo` is set, dispatch a mail send event.
if settings.logger.criticalEmailTo? and settings.logger.criticalEmailTo isnt ""
body = args.join ", "
maxAge = moment().subtract(settings.logger.criticalEmailExpireMinutes, "m").unix()
# Do not proceed if this critical email was sent recently.
return if @criticalEmailCache[body]? and @criticalEmailCache[body] > maxAge
# Set mail options.
mailOptions =
subject: "CRITICAL: #{args[1]}"
body: body
to: settings.logger.criticalEmailTo
logError: false
# Emit mail send message.
events.emit "Mailer.send", mailOptions, (err) ->
console.error "Logger.critical", "Can't send email!", err if err?
# Save to critical email cache.
@criticalEmailCache[body] = moment().unix()
# LOCAL LOGGING
# --------------------------------------------------------------------------
# Log locally. The path is defined on `Settings.Path.logsDir`.
# @param [String] logType The log type (info, warn, error, debug, etc).
# @param [String] message Message to be logged.
# @private
logLocal: (logType, message) ->
now = moment()
message = now.format("HH:mm:ss.SSS") + " - " + message
localBuffer[logType] = [] if not localBuffer[logType]?
localBuffer[logType].push message
# Flush all local buffered log messages to disk. This is usually called by the `bufferDispatcher` timer.
flushLocal: ->
return if flushing
# Set flushing and current date.
flushing = true
now = moment()
date = now.format "YYYYMMDD"
# Flush all buffered logs to disk. Please note that messages from the last seconds of the previous day
# can be saved to the current day depending on how long it takes for the bufferDispatcher to run.
# Default is every 10 seconds, so messages from 23:59:50 onwards could be saved on the next day.
for key, logs of localBuffer
if logs.length > 0
writeData = logs.join("\n")
filePath = path.join settings.path.logsDir, "#{date}.#{key}.log"
successMsg = "#{logs.length} records logged to disk."
# Reset this local buffer.
localBuffer[key] = []
# Only use `appendFile` on new versions of Node.
if fs.appendFile?
fs.appendFile filePath, writeData, (err) =>
flushing = false
if err?
console.error "Logger.flushLocal", err
@onLogError err if @onLogError?
else
@onLogSuccess successMsg if @onLogSuccess?
else
fs.open filePath, "a", 666, (err1, fd) =>
if err1?
flushing = false
console.error "Logger.flushLocal.open", err1
@onLogError err1 if @onLogError?
else
fs.write fd, writeData, null, settings.general.encoding, (err2) =>
flushing = false
if err2?
console.error "Logger.flushLocal.write", err2
@onLogError err2 if @onLogError?
else
@onLogSuccess successMsg if @onLogSuccess?
fs.closeSync fd
# Delete old log files from disk. The maximum date is defined on the settings.
cleanLocal: ->
maxDate = moment().subtract settings.logger.local.maxAge, "d"
fs.readdir settings.path.logsDir, (err, files) ->
if err?
console.error "Logger.cleanLocal", err
else
for f in files
date = moment f.split(".")[1], "yyyyMMdd"
if date.isBefore maxDate
fs.unlink path.join(settings.path.logsDir, f), (err) ->
console.error "Logger.cleanLocal", err if err?
# HELPER METHODS
# --------------------------------------------------------------------------
# Returns a human readable message out of the arguments.
# @return [String] The human readable, parsed JSON message.
# @private
getMessage: ->
separated = []
args = arguments
args = args[0] if args.length is 1
# Parse all arguments and stringify objects. Please note that fields defined
# on the `Settings.logger.removeFields` won't be added to the message.
for a in args
if settings.logger.removeFields.indexOf(a) < 0
if lodash.isArray a
for b in a
separated.push b if settings.logger.removeFields.indexOf(b) < 0
else if lodash.isObject a
try
separated.push JSON.stringify a
catch ex
separated.push a
else
separated.push a
# Append IP address, if `serverIP` is set.
separated.push "IP #{serverIP}" if serverIP?
# Return single string log message.
return separated.join " | "
# Wrapper callback for `onLogSuccess` and `onLogError` to be used by Loggly.
# @param [String] err The Loggly error.
# @param [String] result The Loggly logging result.
# @private
logglyCallback: (err, result) =>
if err? and @onLogError?
@onLogError err
else if @onLogSuccess?
@onLogSuccess result
# Singleton implementation
# --------------------------------------------------------------------------
Logger.getInstance = ->
@instance = new Logger() if not @instance?
return @instance
module.exports = exports = Logger.getInstance()