Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CatchAll method + test on transport #3

Merged
merged 11 commits into from
Mar 9, 2018
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,12 @@
},
"keywords": [],
"nyc": {
"extension": [
".ts"
],
"exclude": [
"build/main/test/**/*"
"build/main/test/**/*",
"test/**/*"
]
},
"dependencies": {
Expand Down
101 changes: 99 additions & 2 deletions src/features/notify.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as util from 'util'

import { Feature } from './featureTypes'
import { ServiceManager } from '../index'

export interface NotifyOptions {
export class NotifyOptions {
level: string
}

Expand All @@ -23,6 +24,8 @@ export class NotifyFeature implements Feature {

this.transport = ServiceManager.get('transport')

this.catchAll()

return {
notify: this.notify
}
Expand All @@ -34,10 +37,104 @@ export class NotifyFeature implements Feature {
return this.transport.send(err)
}

if (this.levels.indexOf(this.options.level) <= this.levels.indexOf(level)) {
if (this.levels.indexOf(this.options.level) >= this.levels.indexOf(level)) {
return this.transport.send(err)
}

return null
}

catchAll (opts?: any): Boolean | void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure that's now enabled by default, we should automatically call it inside the init of the feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


if (opts === undefined) {
opts = {errors: true}
}

// Options.configureModule({
// error : opts.errors
// });

if (process.env.exec_mode === 'cluster_mode') {
return false
}

const self = this

function getUncaughtExceptionListener (listener) {
return function uncaughtListener (err) {
let error = err && err.stack ? err.stack : err

if (err && err.length) {
err._length = err.length
delete err.length
}

if (listener === 'unhandledRejection') {
console.log('You have triggered an unhandledRejection, you may have forgotten to catch a Promise rejection:')
}

console.error(error)

let errObj
if (err) {
errObj = self._interpretError(err)
}

self.transport.send({
type : 'process:exception',
data : errObj !== undefined ? errObj : {message: 'No error but ' + listener + ' was caught!' }
}, true)

if (!process.listeners(listener).filter(function (listener) {
return listener !== uncaughtListener
}).length) {

if (listener === 'uncaughtException') {
process.exit(1)
}
}
}
}

if (opts.errors === true && util.inspect(process.listeners('uncaughtException')).length === 2) {
process.once('uncaughtException', getUncaughtExceptionListener('uncaughtException'))
process.once('unhandledRejection', getUncaughtExceptionListener('unhandledRejection'))
} else if (opts.errors === false
&& util.inspect(process.listeners('uncaughtException')).length !== 2) {
process.removeAllListeners('uncaughtException')
process.removeAllListeners('unhandledRejection')
}
}

_jsonize (err) {
if (typeof(err) !== 'object') {
return err
}

const plainObject = {}

Object.getOwnPropertyNames(err).forEach(function (key) {
plainObject[key] = err[key]
})

return plainObject
}

_interpretError (err: Error | string | object) {
let sErr: any = {
message: null,
stack: null
}

if (err instanceof Error) {
// Error object type processing
sErr = err
} else {
// JSON processing
sErr.message = err
sErr.stack = err
}

return this._jsonize(sErr)
}
}
17 changes: 17 additions & 0 deletions src/features/probe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Feature } from './featureTypes'
import Meter from '../probes/meter'
Copy link
Contributor

@vmarchaud vmarchaud Mar 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to use a 3rd party package to handle the actual compute, to discuss


export default class ProbeFeature implements Feature {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we don't want to name it Probe, that was the old name, it's way easier to our customer to understand metrics

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

init (): Object {
return {
metric: null,
histogram: null,
meter: this.meter,
counter: null
}
}

meter (opts: Object) {
return new Meter(opts)
}
}
41 changes: 41 additions & 0 deletions src/probes/meter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import EWMA from '../utils/EWMA'
import units from '../utils/units'

export default class Meter {

private _tickInterval: number
private _samples: number
private _timeframe: number
private _rate
private _interval

constructor (opts?: any) {
const self = this

this._samples = opts.samples || opts.seconds || 1
this._timeframe = opts.timeframe || 60
this._tickInterval = opts.tickInterval || 5 * units.SECONDS

this._rate = new EWMA(this._timeframe * units.SECONDS, this._tickInterval)

if (opts.debug && opts.debug === true) {
return
}

this._interval = setInterval(function () {
self._rate.tick()
}, this._tickInterval)

this._interval.unref()
}

mark = function (n) {
n = n || 1

this._rate.update(n)
}

val = function () {
return Math.round(this._rate.rate(this._samples * units.SECONDS) * 100 ) / 100
}
}
32 changes: 32 additions & 0 deletions src/utils/EWMA.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import units from './units'

export default class ExponentiallyWeightedMovingAverage {
private _timePeriod: number
private _tickInterval: number
private _alpha: number
private _count: number = 0
private _rate: number = 0

private TICK_INTERVAL: number = 5 * units.SECONDS

constructor (timePeriod: number, tickInterval: number) {
this._timePeriod = timePeriod || 1 * units.MINUTES
this._tickInterval = tickInterval || this.TICK_INTERVAL
this._alpha = 1 - Math.exp(-this._tickInterval / this._timePeriod)
}

update (n) {
this._count += n
}

tick () {
const instantRate = this._count / this._tickInterval
this._count = 0

this._rate += (this._alpha * (instantRate - this._rate))
}

rate (timeUnit) {
return (this._rate || 0) * timeUnit
}
}
10 changes: 7 additions & 3 deletions src/utils/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as stringify from 'json-stringify-safe'
debug('axm:transport')

export default class Transport {
send (args: Error, print: Boolean): void {
send (args: Error | any, print?: Boolean): number {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe transport should in the manager folder since it need to be started before each feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


if (!print) print = false

Expand All @@ -16,15 +16,19 @@ export default class Transport {
}

if (!process.send) {
return
return -1
}

const msg = args instanceof Error ? args.message : args.data.message

try {
process.send(JSON.parse(stringify(args.message)))
process.send(JSON.parse(stringify(msg)))
} catch (e) {
console.error('Process disconnected from parent !')
console.error(e.stack || e)
process.exit(1)
}

return 0
}
}
14 changes: 14 additions & 0 deletions src/utils/units.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const MILLISECONDS = 1
const SECONDS = 1000 * MILLISECONDS
const MINUTES = 60 * SECONDS
const HOURS = 60 * MINUTES

export default {
NANOSECONDS: 1 / (1000 * 1000),
MICROSECONDS: 1 / 1000,
MILLISECONDS: MILLISECONDS,
SECONDS: SECONDS,
MINUTES: MINUTES,
HOURS: HOURS,
DAYS: 24 * HOURS
}
43 changes: 37 additions & 6 deletions test/features/notify.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
import { fork } from 'child_process'
import { expect } from 'chai'
import { expect, assert } from 'chai'
import 'mocha'
import SpecUtils from '../fixtures/utils'

describe('Notify', () => {
it('should send a notification', (done) => {
const child = fork('./build/main/test/fixtures/features/notifyChild.js')
child.on('message', msg => {
expect(msg).to.equal('test')
done()
describe('notify', () => {
it('should send a notification', (done) => {
const child = fork(SpecUtils.buildTestPath('fixtures/features/notifyChild.js'))
child.on('message', msg => {
expect(msg).to.equal('test')
done()
})
})

it('should send a notification for specific level', (done) => {
const child = fork(SpecUtils.buildTestPath('fixtures/features/notifyChildLevel.js'))
let count = 0
child.on('message', msg => {
count++

if (msg === 'info') {
assert.fail()
} else {
expect(msg === 'warn' || msg === 'error' || msg === 'does not exist').to.equal(true)
}

if (count === 3) {
done()
}
})
})
})

describe('catchAll', () => {
it('should catch exception', (done) => {
const child = fork(SpecUtils.buildTestPath('fixtures/features/catchAllChild.js'))
child.on('message', msg => {
expect(msg).to.equal('test')
done()
})
})
})
})
36 changes: 36 additions & 0 deletions test/features/probe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, assert } from 'chai'
import 'mocha'

import Probe from '../../src/features/probe'

describe('Probe', () => {
describe('meter', () => {
it('should calulate a meter', (done) => {
const probe = new Probe()

const meter = probe.meter({tickInterval: 50})

expect(meter.val()).to.equal(0)

setTimeout(function () {
expect(meter.val()).to.equal(0)
done()
}, 60)
})

it('should calulate a meter after mark', (done) => {
const probe = new Probe()

const meter = probe.meter({tickInterval: 50})

expect(meter.val()).to.equal(0)

meter.mark(10)

setTimeout(function () {
expect(meter.val()).to.equal(0.17)
done()
}, 60)
})
})
})
10 changes: 10 additions & 0 deletions test/fixtures/features/catchAllChild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NotifyFeature } from '../../../src/features/notify'

const notify = new NotifyFeature()
notify.init().then(() => {
notify.catchAll()

throw new Error('test')
}).catch(() => {
throw new Error('test')
})
19 changes: 19 additions & 0 deletions test/fixtures/features/notifyChildLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NotifyFeature, NotifyOptions } from '../../../src/features/notify'

const notify = new NotifyFeature()
class Option extends NotifyOptions {
constructor (level: string) {
super()
this.level = level
}
}
const option = new Option('warn')

notify.init(option).then(() => {
notify.notify(new Error('info'), 'info')
notify.notify(new Error('warn'), 'warn')
notify.notify(new Error('error'), 'errors')
notify.notify(new Error('does not exist'), 'does not exist')
}).catch(() => {
console.log('error')
})
Loading