loggingManagerTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. const SandboxedModule = require("sandboxed-module");
  2. const bunyan = require("bunyan");
  3. const chai = require("chai");
  4. const path = require("path");
  5. const sinon = require("sinon");
  6. const sinonChai = require("sinon-chai");
  7. chai.use(sinonChai);
  8. chai.should();
  9. const modulePath = path.join(__dirname, "../../logging-manager.js");
  10. describe("LoggingManager", function() {
  11. beforeEach(function() {
  12. this.start = Date.now();
  13. this.clock = sinon.useFakeTimers(this.start);
  14. this.captureException = sinon.stub();
  15. this.mockBunyanLogger = {
  16. debug: sinon.stub(),
  17. error: sinon.stub(),
  18. fatal: sinon.stub(),
  19. info: sinon.stub(),
  20. level: sinon.stub(),
  21. warn: sinon.stub()
  22. };
  23. this.mockRavenClient = {
  24. captureException: this.captureException,
  25. once: sinon.stub().yields()
  26. };
  27. this.LoggingManager = SandboxedModule.require(modulePath, {
  28. globals: { console },
  29. requires: {
  30. bunyan: (this.Bunyan = {
  31. createLogger: sinon.stub().returns(this.mockBunyanLogger),
  32. RingBuffer: bunyan.RingBuffer
  33. }),
  34. raven: (this.Raven = {
  35. Client: sinon.stub().returns(this.mockRavenClient)
  36. }),
  37. request: (this.Request = sinon.stub())
  38. }
  39. });
  40. this.loggerName = "test";
  41. this.logger = this.LoggingManager.initialize(this.loggerName);
  42. this.logger.initializeErrorReporting("test_dsn");
  43. });
  44. afterEach(function() {
  45. this.clock.restore();
  46. });
  47. describe("initialize", function() {
  48. beforeEach(function() {
  49. this.checkLogLevelStub = sinon.stub(this.LoggingManager, "checkLogLevel");
  50. this.Bunyan.createLogger.reset();
  51. });
  52. afterEach(function() {
  53. this.checkLogLevelStub.restore();
  54. });
  55. describe("not in production", function() {
  56. beforeEach(function() {
  57. this.logger = this.LoggingManager.initialize(this.loggerName);
  58. });
  59. it("should default to log level debug", function() {
  60. this.Bunyan.createLogger.firstCall.args[0].streams[0].level.should.equal(
  61. "debug"
  62. );
  63. });
  64. it("should not run checkLogLevel", function() {
  65. this.checkLogLevelStub.should.not.have.been.called;
  66. });
  67. });
  68. describe("in production", function() {
  69. beforeEach(function() {
  70. process.env.NODE_ENV = "production";
  71. this.logger = this.LoggingManager.initialize(this.loggerName);
  72. });
  73. afterEach(() => delete process.env.NODE_ENV);
  74. it("should default to log level warn", function() {
  75. this.Bunyan.createLogger.firstCall.args[0].streams[0].level.should.equal(
  76. "warn"
  77. );
  78. });
  79. it("should run checkLogLevel", function() {
  80. this.checkLogLevelStub.should.have.been.calledOnce;
  81. });
  82. describe("after 1 minute", () =>
  83. it("should run checkLogLevel again", function() {
  84. this.clock.tick(61 * 1000);
  85. this.checkLogLevelStub.should.have.been.calledTwice;
  86. }));
  87. describe("after 2 minutes", () =>
  88. it("should run checkLogLevel again", function() {
  89. this.clock.tick(121 * 1000);
  90. this.checkLogLevelStub.should.have.been.calledThrice;
  91. }));
  92. });
  93. describe("when LOG_LEVEL set in env", function() {
  94. beforeEach(function() {
  95. process.env.LOG_LEVEL = "trace";
  96. this.LoggingManager.initialize();
  97. });
  98. afterEach(() => delete process.env.LOG_LEVEL);
  99. it("should use custom log level", function() {
  100. this.Bunyan.createLogger.firstCall.args[0].streams[0].level.should.equal(
  101. "trace"
  102. );
  103. });
  104. });
  105. });
  106. describe("bunyan logging", function() {
  107. beforeEach(function() {
  108. this.logArgs = [{ foo: "bar" }, "foo", "bar"];
  109. });
  110. it("should log debug", function() {
  111. this.logger.debug(this.logArgs);
  112. this.mockBunyanLogger.debug.should.have.been.calledWith(this.logArgs);
  113. });
  114. it("should log error", function() {
  115. this.logger.error(this.logArgs);
  116. this.mockBunyanLogger.error.should.have.been.calledWith(this.logArgs);
  117. });
  118. it("should log fatal", function() {
  119. this.logger.fatal(this.logArgs);
  120. this.mockBunyanLogger.fatal.should.have.been.calledWith(this.logArgs);
  121. });
  122. it("should log info", function() {
  123. this.logger.info(this.logArgs);
  124. this.mockBunyanLogger.info.should.have.been.calledWith(this.logArgs);
  125. });
  126. it("should log warn", function() {
  127. this.logger.warn(this.logArgs);
  128. this.mockBunyanLogger.warn.should.have.been.calledWith(this.logArgs);
  129. });
  130. it("should log err", function() {
  131. this.logger.err(this.logArgs);
  132. this.mockBunyanLogger.error.should.have.been.calledWith(this.logArgs);
  133. });
  134. it("should log log", function() {
  135. this.logger.log(this.logArgs);
  136. this.mockBunyanLogger.info.should.have.been.calledWith(this.logArgs);
  137. });
  138. });
  139. describe("logger.error", function() {
  140. it("should report a single error to sentry", function() {
  141. this.logger.error({ foo: "bar" }, "message");
  142. this.captureException.called.should.equal(true);
  143. });
  144. it("should report the same error to sentry only once", function() {
  145. const error1 = new Error("this is the error");
  146. this.logger.error({ foo: error1 }, "first message");
  147. this.logger.error({ bar: error1 }, "second message");
  148. this.captureException.callCount.should.equal(1);
  149. });
  150. it("should report two different errors to sentry individually", function() {
  151. const error1 = new Error("this is the error");
  152. const error2 = new Error("this is the error");
  153. this.logger.error({ foo: error1 }, "first message");
  154. this.logger.error({ bar: error2 }, "second message");
  155. this.captureException.callCount.should.equal(2);
  156. });
  157. it("should remove the path from fs errors", function() {
  158. const fsError = new Error(
  159. "Error: ENOENT: no such file or directory, stat '/tmp/3279b8d0-da10-11e8-8255-efd98985942b'"
  160. );
  161. fsError.path = "/tmp/3279b8d0-da10-11e8-8255-efd98985942b";
  162. this.logger.error({ err: fsError }, "message");
  163. this.captureException
  164. .calledWith(
  165. sinon.match.has(
  166. "message",
  167. "Error: ENOENT: no such file or directory, stat"
  168. )
  169. )
  170. .should.equal(true);
  171. });
  172. it("for multiple errors should only report a maximum of 5 errors to sentry", function() {
  173. this.logger.error({ foo: "bar" }, "message");
  174. this.logger.error({ foo: "bar" }, "message");
  175. this.logger.error({ foo: "bar" }, "message");
  176. this.logger.error({ foo: "bar" }, "message");
  177. this.logger.error({ foo: "bar" }, "message");
  178. this.logger.error({ foo: "bar" }, "message");
  179. this.logger.error({ foo: "bar" }, "message");
  180. this.logger.error({ foo: "bar" }, "message");
  181. this.logger.error({ foo: "bar" }, "message");
  182. this.captureException.callCount.should.equal(5);
  183. });
  184. it("for multiple errors with a minute delay should report 10 errors to sentry", function() {
  185. // the first five errors should be reported to sentry
  186. this.logger.error({ foo: "bar" }, "message");
  187. this.logger.error({ foo: "bar" }, "message");
  188. this.logger.error({ foo: "bar" }, "message");
  189. this.logger.error({ foo: "bar" }, "message");
  190. this.logger.error({ foo: "bar" }, "message");
  191. // the following errors should not be reported
  192. this.logger.error({ foo: "bar" }, "message");
  193. this.logger.error({ foo: "bar" }, "message");
  194. this.logger.error({ foo: "bar" }, "message");
  195. this.logger.error({ foo: "bar" }, "message");
  196. // allow a minute to pass
  197. this.clock.tick(this.start + 61 * 1000);
  198. // after a minute the next five errors should be reported to sentry
  199. this.logger.error({ foo: "bar" }, "message");
  200. this.logger.error({ foo: "bar" }, "message");
  201. this.logger.error({ foo: "bar" }, "message");
  202. this.logger.error({ foo: "bar" }, "message");
  203. this.logger.error({ foo: "bar" }, "message");
  204. // the following errors should not be reported to sentry
  205. this.logger.error({ foo: "bar" }, "message");
  206. this.logger.error({ foo: "bar" }, "message");
  207. this.logger.error({ foo: "bar" }, "message");
  208. this.logger.error({ foo: "bar" }, "message");
  209. this.captureException.callCount.should.equal(10);
  210. });
  211. });
  212. describe("checkLogLevel", function() {
  213. it("should request log level override from google meta data service", function() {
  214. this.logger.checkLogLevel();
  215. const options = {
  216. headers: {
  217. "Metadata-Flavor": "Google"
  218. },
  219. uri: `http://metadata.google.internal/computeMetadata/v1/project/attributes/${
  220. this.loggerName
  221. }-setLogLevelEndTime`
  222. };
  223. this.Request.should.have.been.calledWithMatch(options);
  224. });
  225. describe("when request has error", function() {
  226. beforeEach(function() {
  227. this.Request.yields("error");
  228. this.logger.checkLogLevel();
  229. });
  230. it("should only set default level", function() {
  231. this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
  232. "debug"
  233. );
  234. });
  235. });
  236. describe("when statusCode is not 200", function() {
  237. beforeEach(function() {
  238. this.Request.yields(null, { statusCode: 404 });
  239. this.logger.checkLogLevel();
  240. });
  241. it("should only set default level", function() {
  242. this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
  243. "debug"
  244. );
  245. });
  246. });
  247. describe("when time value returned that is less than current time", function() {
  248. beforeEach(function() {
  249. this.Request.yields(null, { statusCode: 200 }, "1");
  250. this.logger.checkLogLevel();
  251. });
  252. it("should only set default level", function() {
  253. this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
  254. "debug"
  255. );
  256. });
  257. });
  258. describe("when time value returned that is less than current time", function() {
  259. describe("when level is already set", function() {
  260. beforeEach(function() {
  261. this.mockBunyanLogger.level.returns(10);
  262. this.Request.yields(null, { statusCode: 200 }, this.start + 1000);
  263. this.logger.checkLogLevel();
  264. });
  265. it("should set trace level", function() {
  266. this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
  267. "trace"
  268. );
  269. });
  270. });
  271. describe("when level is not already set", function() {
  272. beforeEach(function() {
  273. this.mockBunyanLogger.level.returns(20);
  274. this.Request.yields(null, { statusCode: 200 }, this.start + 1000);
  275. this.logger.checkLogLevel();
  276. });
  277. it("should set trace level", function() {
  278. this.mockBunyanLogger.level.should.have.been.calledOnce.and.calledWith(
  279. "trace"
  280. );
  281. });
  282. });
  283. });
  284. });
  285. describe("ringbuffer", function() {
  286. beforeEach(function() {
  287. this.logBufferMock = [
  288. {
  289. msg: "log 1"
  290. },
  291. {
  292. msg: "log 2"
  293. }
  294. ];
  295. });
  296. describe("in production", function() {
  297. beforeEach(function() {
  298. process.env["NODE_ENV"] = "production";
  299. this.logger = this.LoggingManager.initialize(this.loggerName);
  300. this.logger.ringBuffer.records = this.logBufferMock;
  301. this.logger.error({}, "error");
  302. });
  303. afterEach(function() {
  304. process.env["NODE_ENV"] = undefined;
  305. });
  306. it("should include buffered logs in error log", function() {
  307. this.mockBunyanLogger.error.lastCall.args[0].logBuffer.should.equal(
  308. this.logBufferMock
  309. );
  310. });
  311. });
  312. describe("not in production", function() {
  313. beforeEach(function() {
  314. this.logger = this.LoggingManager.initialize(this.loggerName);
  315. this.logger.ringBuffer.records = this.logBufferMock;
  316. this.logger.error({}, "error");
  317. });
  318. it("should not include buffered logs in error log", function() {
  319. chai.expect(this.mockBunyanLogger.error.lastCall.args[0].logBuffer).be
  320. .undefined;
  321. });
  322. });
  323. });
  324. });