diff --git a/packages/database/package.json b/packages/database/package.json index f4b84eed31b..73a9d081f2a 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@firebase/database-types": "0.1.2", + "@firebase/logger": "0.1.0", "@firebase/util": "0.1.10", "faye-websocket": "0.11.1", "tslib": "^1.9.0" diff --git a/packages/database/src/core/PersistentConnection.ts b/packages/database/src/core/PersistentConnection.ts index 541e8a8868d..6f595c29b82 100644 --- a/packages/database/src/core/PersistentConnection.ts +++ b/packages/database/src/core/PersistentConnection.ts @@ -935,7 +935,7 @@ export class PersistentConnection extends ServerActions { if (this.securityDebugCallback_) { this.securityDebugCallback_(body); } else { - if ('msg' in body && typeof console !== 'undefined') { + if ('msg' in body) { console.log('FIREBASE: ' + body['msg'].replace('\n', '\nFIREBASE: ')); } } diff --git a/packages/database/src/core/util/util.ts b/packages/database/src/core/util/util.ts index bd501505ea9..72b25e04811 100644 --- a/packages/database/src/core/util/util.ts +++ b/packages/database/src/core/util/util.ts @@ -27,6 +27,9 @@ import { stringToByteArray } from '@firebase/util'; import { stringify } from '@firebase/util'; import { SessionStorage } from '../storage/storage'; import { isNodeSdk } from '@firebase/util'; +import { Logger } from '@firebase/logger'; + +const logClient = new Logger('@firebase/database'); /** * Returns a locally-unique ID (generated by just incrementing up from 0 each time its called). @@ -105,16 +108,7 @@ export const enableLogging = function( "Can't turn on custom loggers persistently." ); if (logger_ === true) { - if (typeof console !== 'undefined') { - if (typeof console.log === 'function') { - logger = console.log.bind(console); - } else if (typeof console.log === 'object') { - // IE does this. - logger = function(message) { - console.log(message); - }; - } - } + logger = logClient.log.bind(logClient); if (persistent) SessionStorage.set('logging_enabled', true); } else if (typeof logger_ === 'function') { logger = logger_; @@ -157,36 +151,25 @@ export const logWrapper = function( * @param {...string} var_args */ export const error = function(...var_args: string[]) { - if (typeof console !== 'undefined') { - const message = 'FIREBASE INTERNAL ERROR: ' + buildLogMessage_(...var_args); - if (typeof console.error !== 'undefined') { - console.error(message); - } else { - console.log(message); - } - } + const message = 'FIREBASE INTERNAL ERROR: ' + buildLogMessage_(...var_args); + logClient.error(message); }; /** * @param {...string} var_args */ export const fatal = function(...var_args: string[]) { - const message = buildLogMessage_(...var_args); - throw new Error('FIREBASE FATAL ERROR: ' + message); + const message = `FIREBASE FATAL ERROR: ${buildLogMessage_(...var_args)}`; + logClient.error(message); + throw new Error(message); }; /** * @param {...*} var_args */ export const warn = function(...var_args: any[]) { - if (typeof console !== 'undefined') { - const message = 'FIREBASE WARNING: ' + buildLogMessage_(...var_args); - if (typeof console.warn !== 'undefined') { - console.warn(message); - } else { - console.log(message); - } - } + const message = 'FIREBASE WARNING: ' + buildLogMessage_(...var_args); + logClient.warn(message); }; /** diff --git a/packages/firestore/package.json b/packages/firestore/package.json index 96fb38ef60d..bd291401261 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -23,6 +23,7 @@ "license": "Apache-2.0", "dependencies": { "@firebase/firestore-types": "0.2.2", + "@firebase/logger": "0.1.0", "@firebase/webchannel-wrapper": "0.2.6", "grpc": "^1.9.1", "tslib": "^1.9.0" diff --git a/packages/firestore/src/util/log.ts b/packages/firestore/src/util/log.ts index 579d76743c2..bca80685b90 100644 --- a/packages/firestore/src/util/log.ts +++ b/packages/firestore/src/util/log.ts @@ -19,6 +19,9 @@ import { SDK_VERSION } from '../core/version'; import { AnyJs } from './misc'; import { PlatformSupport } from '../platform/platform'; +import { Logger, LogLevel as FirebaseLogLevel } from '@firebase/logger'; + +const logClient = new Logger('@firebase/firestore'); export enum LogLevel { DEBUG, @@ -26,29 +29,48 @@ export enum LogLevel { SILENT } -let logLevel = LogLevel.ERROR; - // Helper methods are needed because variables can't be exported as read/write export function getLogLevel(): LogLevel { - return logLevel; + if (logClient.logLevel === FirebaseLogLevel.DEBUG) { + return LogLevel.DEBUG; + } else if (logClient.logLevel === FirebaseLogLevel.SILENT) { + return LogLevel.SILENT; + } else { + return LogLevel.ERROR; + } } export function setLogLevel(newLevel: LogLevel): void { - logLevel = newLevel; + /** + * Map the new log level to the associated Firebase Log Level + */ + switch (newLevel) { + case LogLevel.DEBUG: + logClient.logLevel = FirebaseLogLevel.DEBUG; + break; + case LogLevel.ERROR: + logClient.logLevel = FirebaseLogLevel.ERROR; + break; + case LogLevel.SILENT: + logClient.logLevel = FirebaseLogLevel.SILENT; + break; + default: + logClient.error( + `Firestore (${SDK_VERSION}): Invalid value passed to \`setLogLevel\`` + ); + } } export function debug(tag: string, msg: string, ...obj: AnyJs[]): void { - if (logLevel <= LogLevel.DEBUG) { - const time = new Date().toISOString(); + if (logClient.logLevel <= FirebaseLogLevel.DEBUG) { const args = obj.map(argToString); - console.log(`Firestore (${SDK_VERSION}) ${time} [${tag}]: ${msg}`, ...args); + logClient.debug(`Firestore (${SDK_VERSION}) [${tag}]: ${msg}`, ...args); } } export function error(msg: string, ...obj: AnyJs[]): void { - if (logLevel <= LogLevel.ERROR) { - const time = new Date().toISOString(); + if (logClient.logLevel <= FirebaseLogLevel.ERROR) { const args = obj.map(argToString); - console.error(`Firestore (${SDK_VERSION}) ${time}: ${msg}`, ...args); + logClient.error(`Firestore (${SDK_VERSION}): ${msg}`, ...args); } } diff --git a/packages/logger/.npmignore b/packages/logger/.npmignore new file mode 100644 index 00000000000..6de0b6d2896 --- /dev/null +++ b/packages/logger/.npmignore @@ -0,0 +1 @@ +# This file is left intentionally blank \ No newline at end of file diff --git a/packages/logger/README.md b/packages/logger/README.md new file mode 100644 index 00000000000..69c1642e2ae --- /dev/null +++ b/packages/logger/README.md @@ -0,0 +1,40 @@ +# @firebase/logger + +This package serves as the base of all logging in the JS SDK. Any logging that +is intended to be visible to Firebase end developers should go through this +module. + +## Basic Usage + +Firebase components should import the `Logger` class and instantiate a new +instance by passing a component name (e.g. `@firebase/`) to the +constructor. + +_e.g._ + +```typescript +import { Logger } from '@firebase/logger'; + +const logClient = new Logger(`@firebase/`); +``` + +Each `Logger` instance supports 5 log functions each to be used in a specific +instance: + +- `debug`: Internal logs; use this to allow developers to send us their debug + logs for us to be able to diagnose an issue. +- `log`: Use to inform your user about things they may need to know. +- `info`: Use if you have to inform the user about something that they need to + take a concrete action on. Once they take that action, the log should go away. +- `warn`: Use when a product feature may stop functioning correctly; unexpected + scenario. +- `error`: Only use when user App would stop functioning correctly - super rare! + +## Log Format + +Each log will be formatted in the following manner: + +```typescript +`[${new Date()}] ${COMPONENT_NAME}: ${...args}` +``` + diff --git a/packages/logger/gulpfile.js b/packages/logger/gulpfile.js new file mode 100644 index 00000000000..dc8ec17b593 --- /dev/null +++ b/packages/logger/gulpfile.js @@ -0,0 +1,31 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const gulp = require('gulp'); +const tools = require('../../tools/build'); + +const buildModule = gulp.parallel([ + tools.buildCjs(__dirname), + tools.buildEsm(__dirname) +]); + +const setupWatcher = () => { + gulp.watch(['index.ts', 'src/**/*'], buildModule); +}; + +gulp.task('build', buildModule); + +gulp.task('dev', gulp.parallel([setupWatcher])); diff --git a/packages/logger/index.ts b/packages/logger/index.ts new file mode 100644 index 00000000000..9abb0eacb28 --- /dev/null +++ b/packages/logger/index.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { instances, LogLevel } from './src/logger'; + +export function setLogLevel(level: LogLevel) { + instances.forEach(inst => { + inst.logLevel = level; + }); +} + +export { Logger, LogLevel, LogHandler } from './src/logger'; diff --git a/packages/logger/karma.conf.js b/packages/logger/karma.conf.js new file mode 100644 index 00000000000..9a064313342 --- /dev/null +++ b/packages/logger/karma.conf.js @@ -0,0 +1,31 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const karma = require('karma'); +const path = require('path'); +const karmaBase = require('../../config/karma.base'); + +module.exports = function(config) { + const karmaConfig = Object.assign({}, karmaBase, { + // files to load into karma + files: [{ pattern: `test/**/*` }], + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }); + + config.set(karmaConfig); +}; diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 00000000000..fa20ae34233 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,54 @@ +{ + "name": "@firebase/logger", + "version": "0.1.0", + "private": true, + "description": "A logger package for use in the Firebase JS SDK", + "author": "Firebase (https://firebase.google.com/)", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "scripts": { + "dev": "gulp dev", + "test": "run-p test:browser test:node", + "test:browser": "karma start --single-run", + "test:browser:debug": "karma start --browsers Chrome --auto-watch", + "test:node": "nyc --reporter lcovonly -- mocha test/**/*.test.* --compilers ts:ts-node/register --exit", + "prepare": "gulp build" + }, + "license": "Apache-2.0", + "devDependencies": { + "@types/chai": "^4.1.2", + "@types/mocha": "^2.2.48", + "@types/sinon": "^4.1.3", + "chai": "^4.1.1", + "gulp": "^4.0.0", + "karma": "^2.0.0", + "karma-chrome-launcher": "^2.2.0", + "karma-cli": "^1.0.1", + "karma-mocha": "^1.3.0", + "karma-sauce-launcher": "^1.2.0", + "karma-spec-reporter": "^0.0.32", + "karma-webpack": "^2.0.9", + "mocha": "^5.0.1", + "npm-run-all": "^4.1.1", + "nyc": "^11.4.1", + "ts-loader": "^3.5.0", + "ts-node": "^5.0.0", + "typescript": "^2.7.2", + "webpack": "^3.11.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk/tree/master/packages/logger" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "typings": "dist/esm/index.d.ts", + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + }, + "dependencies": {} +} diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts new file mode 100644 index 00000000000..8b162f816f8 --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,156 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A container for all of the Logger instances + */ +export const instances: Logger[] = []; + +/** + * The JS SDK supports 5 log levels and also allows a user the ability to + * silence the logs altogether. + * + * The order is a follows: + * DEBUG < VERBOSE < INFO < WARN < ERROR + * + * All of the log types above the current log level will be captured (i.e. if + * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and + * `VERBOSE` logs will not) + */ +export enum LogLevel { + DEBUG, + VERBOSE, + INFO, + WARN, + ERROR, + SILENT +} + +/** + * The default log level + */ +const defaultLogLevel: LogLevel = LogLevel.INFO; + +/** + * We allow users the ability to pass their own log handler. We will pass the + * type of log, the current log level, and any other arguments passed (i.e. the + * messages that the user wants to log) to this function. + */ +export type LogHandler = ( + loggerInstance: Logger, + logType: LogLevel, + ...args: any[] +) => void; + +/** + * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR + * messages on to their corresponding console counterparts (if the log method + * is supported by the current log level) + */ +const defaultLogHandler: LogHandler = (instance, logType, ...args) => { + if (logType < instance.logLevel) return; + const now = new Date().toISOString(); + switch (logType) { + /** + * By default, `console.debug` is not displayed in the developer console (in + * chrome). To avoid forcing users to have to opt-in to these logs twice + * (i.e. once for firebase, and once in the console), we are sending `DEBUG` + * logs to the `console.log` function. + */ + case LogLevel.DEBUG: + console.log(`[${now}] ${instance.name}:`, ...args); + break; + case LogLevel.VERBOSE: + console.log(`[${now}] ${instance.name}:`, ...args); + break; + case LogLevel.INFO: + console.info(`[${now}] ${instance.name}:`, ...args); + break; + case LogLevel.WARN: + console.warn(`[${now}] ${instance.name}:`, ...args); + break; + case LogLevel.ERROR: + console.error(`[${now}] ${instance.name}:`, ...args); + break; + default: + throw new Error( + `Attempted to log a message with an invalid logType (value: ${logType})` + ); + } +}; + +export class Logger { + /** + * Gives you an instance of a Logger to capture messages according to + * Firebase's logging scheme. + * + * @param name The name that the logs will be associated with + */ + constructor(public name: string) { + /** + * Capture the current instance for later use + */ + instances.push(this); + } + + /** + * The log level of the given Logger instance. + */ + private _logLevel = defaultLogLevel; + get logLevel(): LogLevel { + return this._logLevel; + } + set logLevel(val: LogLevel) { + if (!(val in LogLevel)) { + throw new TypeError('Invalid value assigned to `logLevel`'); + } + this._logLevel = val; + } + + /** + * The log handler for the Logger instance. + */ + private _logHandler: LogHandler = defaultLogHandler; + get logHandler(): LogHandler { + return this._logHandler; + } + set logHandler(val: LogHandler) { + if (typeof val !== 'function') { + throw new TypeError('Value assigned to `logHandler` must be a function'); + } + this._logHandler = val; + } + + /** + * The functions below are all based on the `console` interface + */ + + debug(...args) { + this._logHandler(this, LogLevel.DEBUG, ...args); + } + log(...args) { + this._logHandler(this, LogLevel.VERBOSE, ...args); + } + info(...args) { + this._logHandler(this, LogLevel.INFO, ...args); + } + warn(...args) { + this._logHandler(this, LogLevel.WARN, ...args); + } + error(...args) { + this._logHandler(this, LogLevel.ERROR, ...args); + } +} diff --git a/packages/logger/test/logger.test.ts b/packages/logger/test/logger.test.ts new file mode 100644 index 00000000000..900880e83a0 --- /dev/null +++ b/packages/logger/test/logger.test.ts @@ -0,0 +1,99 @@ +/** + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { spy as Spy } from 'sinon'; +import { Logger, LogLevel } from '../src/logger'; +import { setLogLevel } from '../index'; +import { debug } from 'util'; + +describe('@firebase/logger', () => { + const message = 'Hello there!'; + let client: Logger; + const spies = { + logSpy: null, + infoSpy: null, + warnSpy: null, + errorSpy: null + }; + /** + * Before each test, instantiate a new instance of Logger and establish spies + * on all of the console methods so we can assert against them as needed + */ + beforeEach(() => { + client = new Logger('@firebase/test-logger'); + + spies.logSpy = Spy(console, 'log'); + spies.infoSpy = Spy(console, 'info'); + spies.warnSpy = Spy(console, 'warn'); + spies.errorSpy = Spy(console, 'error'); + }); + + afterEach(() => { + spies.logSpy.restore(); + spies.infoSpy.restore(); + spies.warnSpy.restore(); + spies.errorSpy.restore(); + }); + + function testLog(message, channel, shouldLog) { + /** + * Ensure that `debug` logs assert against the `console.log` function. The + * rationale here is explained in `logger.ts`. + */ + channel = channel === 'debug' ? 'log' : channel; + + it(`Should ${ + shouldLog ? '' : 'not' + } call \`console.${channel}\` if \`.${channel}\` is called`, () => { + client[channel](message); + expect( + spies[`${channel}Spy`] && spies[`${channel}Spy`].called, + `Expected ${channel} to ${shouldLog ? '' : 'not'} log` + ).to.be[shouldLog ? 'true' : 'false']; + }); + } + + describe('Class instance methods', () => { + beforeEach(() => { + setLogLevel(LogLevel.DEBUG); + }); + testLog(message, 'debug', true); + testLog(message, 'log', true); + testLog(message, 'info', true); + testLog(message, 'warn', true); + testLog(message, 'error', true); + }); + + describe('Defaults to LogLevel.NOTICE', () => { + testLog(message, 'debug', false); + testLog(message, 'log', false); + testLog(message, 'info', true); + testLog(message, 'warn', true); + testLog(message, 'error', true); + }); + + describe(`Doesn't log if LogLevel.SILENT is set`, () => { + beforeEach(() => { + setLogLevel(LogLevel.SILENT); + }); + testLog(message, 'debug', false); + testLog(message, 'log', false); + testLog(message, 'info', false); + testLog(message, 'warn', false); + testLog(message, 'error', false); + }); +}); diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 00000000000..a06ed9a374c --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": [ + "dist/**/*" + ] +} \ No newline at end of file diff --git a/packages/template/index.ts b/packages/template/index.ts index d362d85d4eb..907a7b0e607 100644 --- a/packages/template/index.ts +++ b/packages/template/index.ts @@ -14,13 +14,6 @@ * limitations under the License. */ -/** - * This is the file that people using Node.js will actually import. You should - * only include this file if you have something specific about your - * implementation that mandates having a separate entrypoint. Otherwise you can - * just use index.ts - */ - import { testFxn } from './src'; testFxn();