├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── angular-loggly-logger.js ├── angular-loggly-logger.min.js ├── angular-loggly-logger.min.js.map ├── angular-loggly-logger.min.map ├── bower.json ├── demo ├── app.js └── index.html ├── index.js ├── karma.conf.js ├── package.json └── test └── unit └── logglySenderSpec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/* 2 | !.gitkeep 3 | build/ 4 | node_modules/ 5 | bower_components/ 6 | tmp 7 | .DS_Store 8 | .idea 9 | *.iml 10 | 11 | *.swp 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 4, // {int} Number of spaces to use for indentation 16 | "latedef" : false, // true: Require variables/functions to be defined before being used 17 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` & `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // Unused variables: 30 | // true : all variables, last function parameter 31 | // "vars" : all variables only 32 | // "strict" : all variables, all function parameters 33 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 34 | "maxparams" : false, // {int} Max number of formal params allowed per function 35 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 36 | "maxstatements" : false, // {int} Max number statements per function 37 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 38 | "maxlen" : false, // {int} Max number of characters per line 39 | "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. 40 | 41 | // Relaxing 42 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 43 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 44 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 45 | "eqnull" : false, // true: Tolerate use of `== null` 46 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 47 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 48 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 49 | // (ex: `for each`, multiple try/catch, function expression…) 50 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 51 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 52 | "funcscope" : false, // true: Tolerate defining variables inside control statements 53 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 54 | "iterator" : false, // true: Tolerate using the `__iterator__` property 55 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 56 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 57 | "laxcomma" : false, // true: Tolerate comma-first style coding 58 | "loopfunc" : false, // true: Tolerate functions being defined in loops 59 | "multistr" : false, // true: Tolerate multi-line strings 60 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 61 | "notypeof" : false, // true: Tolerate invalid typeof operator values 62 | "proto" : false, // true: Tolerate using the `__proto__` property 63 | "scripturl" : false, // true: Tolerate script-targeted URLs 64 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 65 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 66 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 67 | "validthis" : false, // true: Tolerate using this in a non-constructor function 68 | 69 | // Environments 70 | "browser" : true, // Web Browser (window, document, etc) 71 | "browserify" : false, // Browserify (node.js code in the browser) 72 | "couch" : false, // CouchDB 73 | "devel" : true, // Development/debugging (alert, confirm, etc) 74 | "dojo" : false, // Dojo Toolkit 75 | "jasmine" : false, // Jasmine 76 | "jquery" : false, // jQuery 77 | "mocha" : true, // Mocha 78 | "mootools" : false, // MooTools 79 | "node" : false, // Node.js 80 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 81 | "phantom" : false, // PhantomJS 82 | "prototypejs" : false, // Prototype and Scriptaculous 83 | "qunit" : false, // QUnit 84 | "rhino" : false, // Rhino 85 | "shelljs" : false, // ShellJS 86 | "typed" : false, // Globals for typed array constructions 87 | "worker" : false, // Web Workers 88 | "wsh" : false, // Windows Scripting Host 89 | "yui" : false, // Yahoo User Interface 90 | 91 | // Custom Globals 92 | "globals" : {} // additional predefined global variables 93 | } 94 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | deploy: 5 | - provider: npm 6 | email: aj@ajbrown.org 7 | api_key: 8 | secure: C07sCtkimrvDgQZZUVWjlPD/axfRAYJEWMYW6Cu5T1qSz1BZwKmXxNLJouKk1Q+jwlWVWNnJ0kOL/cUTN7oo1YaIsHbE7ufxJoHeyPnOu7oz0FRxYDplZ/3ukQteDHxZkIYjgzfgXWGXbEDoOfzT63+DsDMXQDn7CQrs6bXxv0E= 9 | on: 10 | branch: master 11 | tags: true 12 | 13 | before_script: 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | - npm install -g grunt-cli 17 | - npm start > /dev/null & 18 | - npm run update-webdriver 19 | - sleep 1 20 | script: 21 | - grunt test 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | Contributions are awesome, welcomed, and wanted. Please make sure your pull request targets the "develop" branch. Don't forget to include new or updated tests! 5 | 6 | Please commit the re-minified versions of files after making your changes. Minification can be done using `grunt`. Always fix any jsHint issues that are reported. 7 | 8 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | 5 | jshint: { 6 | options: { 7 | jshintrc: true 8 | }, 9 | files: { 10 | src: ['angular-loggly-logger.js'] 11 | } 12 | }, 13 | 14 | uglify: { 15 | options : { 16 | sourceMap: true 17 | }, 18 | main: { 19 | files: { 'angular-loggly-logger.min.js': ['angular-loggly-logger.js'] } 20 | } 21 | }, 22 | 23 | karma: { 24 | unit: { 25 | configFile: 'karma.conf.js' 26 | }, 27 | //continuous integration mode: run tests once in PhantomJS browser. 28 | travis: { 29 | configFile: 'karma.conf.js', 30 | singleRun: true, 31 | browsers: ['Firefox'] 32 | } 33 | }, 34 | 35 | watch: { 36 | 37 | //run JSHint on JS files 38 | jshint: { 39 | files: ['angular-loggly-logger.js'], 40 | tasks: ['jshint'] 41 | }, 42 | 43 | //run unit tests with karma (server needs to be already running) 44 | karma: { 45 | files: ['*.js', '!*.min.js'], 46 | tasks: ['karma:unit:run'] //NOTE the :run flag 47 | } 48 | } 49 | 50 | }); 51 | 52 | grunt.loadNpmTasks('grunt-contrib-jshint'); 53 | grunt.loadNpmTasks('grunt-contrib-uglify'); 54 | grunt.loadNpmTasks('grunt-contrib-watch'); 55 | grunt.loadNpmTasks('grunt-karma'); 56 | 57 | grunt.registerTask('default', ['jshint','uglify', 'test-all'] ); 58 | grunt.registerTask('test', [ 'karma:travis' ] ); 59 | grunt.registerTask('test-all', ['karma:unit'] ); 60 | }; 61 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 [A.J. Brown](https://ajbrown.org) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ajbrown/angular-loggly-logger.svg)](https://travis-ci.org/ajbrown/angular-loggly-logger) 2 | [![Coverage Status](https://coveralls.io/repos/ajbrown/angular-loggly-logger/badge.svg?branch=master)](https://coveralls.io/r/ajbrown/angular-loggly-logger?branch=master) 3 | [![Join the chat at https://gitter.im/ajbrown/angular-loggly-logger](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ajbrown/angular-loggly-logger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | 6 | Angular Loggly Logger is a module which will decorate Angular's $log service, 7 | and provide a `LogglyLogger` service which can be used to manually send messages 8 | of any kind to the [Loggly](https://www.loggly.com) cloud log management service. 9 | 10 | 11 | ### Getting Started 12 | 13 | LogglyLogger can be installed with bower: 14 | 15 | ``` 16 | bower install angular-loggly-logger 17 | ``` 18 | 19 | Or with npm: 20 | 21 | ``` 22 | npm install --save angular-loggly-logger 23 | ``` 24 | 25 | Once configured (by including "logglyLogger" as a module dependency), the `$log` 26 | service will automatically be decorated, and all messages logged will be handled 27 | as normal as well as formated and passed to LogglyLogger.sendMessage. 28 | The plain text messages are sent into the "json.message" field with the decorated 29 | log while custom JSON objects are sent via "json.messageObj" field as Loggly 30 | only supports one type per field. 31 | 32 | To use both the decorated $log and the LogglyLogger service, you must first 33 | configure it with an inputToken, which is done via the LogglyLoggerProvider: 34 | 35 | ```javascript 36 | angular.module( 'myApp', [require('angular-loggly-logger')] ) 37 | 38 | .config(["LogglyLoggerProvider", function( LogglyLoggerProvider ) { 39 | LogglyLoggerProvider.inputToken( '' ); 40 | } ]); 41 | 42 | .run(["LogglyLogger", "$log", function( LogglyLogger, $log ) { 43 | 44 | //This will be sent to both the console and Loggly 45 | $log.info( "I'm a little teapot." ); 46 | 47 | //This will be sent to loggly only 48 | LogglyLogger.sendMessage( { message : 'Short and Stout.' } ); 49 | }]) 50 | 51 | ``` 52 | 53 | ### $log decoration 54 | 55 | When sent through the `$log` decorator, messages will be formatted as follows: 56 | 57 | ```javascript 58 | 59 | // Example: $log.warn( 'Danger! Danger!' ); 60 | { 61 | level: "WARN", 62 | timestamp: "2014-05-01T13:10Z", 63 | msg: "Danger! Danger!", 64 | url: "https://github.com/ajbrown/angular-loggly-logger/demo/index.html", 65 | } 66 | 67 | // Example: $log.debug( 'User submitted something:', { foo: 'A.J', bar: 'Space' } ) 68 | 69 | { 70 | level: "DEBUG", 71 | timestamp: "2014-05-01T13:18Z", 72 | msg: ["User submitted something", { foo: 'A.J.', bar: 'Space' }], 73 | url: "https://github.com/ajbrown/angular-loggly-logger/demo/index.html", 74 | } 75 | ``` 76 | 77 | > However, 'url' and 'timestamp' are not included by default. You must enable those options in your application config (see below). 78 | 79 | 80 | Note that if you do not call `LogglyLoggerProvider.inputToken()` in a config method, messages will not be sent to loggly. At the moment, there is no warning -- your message is just ignored. 81 | 82 | ### Configuration 83 | 84 | The following configuration options are available. 85 | 86 | ```javascript 87 | 88 | LogglyLoggerProvider 89 | 90 | // set the logging level for messages sent to Loggly. Default is 'DEBUG', 91 | // which will send all log messages. 92 | .level( 'DEBUG' ) 93 | 94 | // set the token of the loggly input to use. Must be set, or no logs 95 | // will be sent. 96 | .inputToken( '' ) 97 | 98 | // set whether or not HTTPS should be used for sending messages. Default 99 | // is true 100 | .useHttps( true ) 101 | 102 | // should the value of $location.absUrl() be sent as a "url" key in the 103 | // message object that's sent to loggly? Default is false. 104 | .includeUrl( false ) 105 | 106 | // should the value of $window.navigator.userAgent be sent as a "userAgent" key in the 107 | // message object that's sent to loggly? Default is false. 108 | .includeUserAgent( false ) 109 | 110 | // should the current timestamp be included? Default is false. 111 | .includeTimestamp( false ) 112 | 113 | // set comma-seperated tags that should be included with the log events. 114 | // Default is "angular" 115 | .inputTag("angular,customTag") 116 | 117 | // Send console error stack traces to Loggly. Default is false. 118 | .sendConsoleErrors( false ) 119 | 120 | // Toggle logging to console. When set to false, messages will not be 121 | // be passed along to the original $log methods. This makes it easy to 122 | // keep sending messages to Loggly in production without also sending them 123 | // to the console. Default is true. 124 | .logToConsole( true ) 125 | 126 | //Toggle delete other headers. If there are any other headers than Accept 127 | //and Content-Type in request, the browser will first send pre-flight OPTIONS 128 | //request. 129 | //Turn this on if you see HTTP 405 errors in console. 130 | .deleteHeaders( false ) 131 | 132 | // Custom labels for standard log fields. Use this to customize your log 133 | // message format or to shorten your logging payload. All available labels 134 | // are listed in this example. 135 | .labels({ 136 | col: 'c', 137 | level: 'lvl', 138 | line: 'l', 139 | logger: 'lgr', 140 | message: 'msg', 141 | stack: 'stk', 142 | timestamp: 'ts', 143 | url: 'url', 144 | userAgent: 'userAgent' 145 | }) 146 | 147 | ``` 148 | 149 | ### Sending JSON Fields 150 | 151 | You can also default some "extra/default" information to be sent with each log message. When this is set, `LogglyLogger` 152 | will include the key/values provided with all messages, plus the data to be sent for each specific logging request. 153 | 154 | ```javascript 155 | 156 | LogglyLoggerProvider.fields( { appVersion: 1.1.0, browser: 'Chrome' } ); 157 | 158 | //... 159 | 160 | $log.warn( 'Danger! Danger!' ) 161 | 162 | >> { appVersion: 1.1.0, browser: 'Chrome', level: 'WARN', message: 'Danger! Danger', url: 'http://google.com' } 163 | ``` 164 | 165 | Extra fields can also be added at runtime using the `LogglyLogger` service: 166 | 167 | ```javascript 168 | app.controller( 'MainCtrl', ["$scope", "$log", "LogglyLogger", function( $scope, $log, LogglyLogger ) { 169 | 170 | logglyLogger.fields( { username: "foobar" } ); 171 | 172 | //... 173 | 174 | $log.info( 'All is good!' ); 175 | 176 | >> { appVersion: 1.1.0, browser: 'Chrome', username: 'foobar', level: 'WARN', message: 'All is good', url: 'http://google.com' } 177 | }]) 178 | 179 | ``` 180 | 181 | 182 | Beware that when using `setExtra` with `LogglyLogger.sendMessage( obj )`, any properties in your `obj` that are the same as your `extra` will be overwritten. 183 | 184 | ## ChangeLog 185 | - v0.2.2 - Fixes preflight cross origin issues. 186 | - v0.2.3 - Fixes npm install issues related to Bower. 187 | - v0.2.4 - Adds customizable labels, error stacktraces, and user-agent logging. 188 | 189 | 190 | ## Contributing 191 | 192 | Contributions are awesome, welcomed, and wanted. Please contribute ideas by [opening a new issue](http://github.com/ajbrown/angular-loggy-logger/issues), or code by creating a new pull request. Please make sure your pull request targets the "develop" branch. 193 | -------------------------------------------------------------------------------- /angular-loggly-logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * logglyLogger is a module which will send your log messages to a configured 3 | * [Loggly](http://loggly.com) connector. 4 | * 5 | * Major credit should go to Thomas Burleson, who's highly informative blog 6 | * post on [Enhancing AngularJs Logging using Decorators](http://bit.ly/1pOI0bb) 7 | * provided the foundation (if not the majority of the brainpower) for this 8 | * module. 9 | * 10 | * @version 0.3.0 11 | * @author A.J. Brown 12 | * @license MIT License, http://www.opensource.org/licenses/MIT 13 | */ 14 | (function( angular ) { 15 | "use strict"; 16 | 17 | angular.module( 'logglyLogger.logger', [] ) 18 | .provider( 'LogglyLogger', function() { 19 | var self = this; 20 | 21 | var logLevels = [ 'DEBUG', 'INFO', 'WARN', 'ERROR' ]; 22 | 23 | var https = true; 24 | var extra = {}; 25 | var includeCurrentUrl = false; 26 | var includeTimestamp = false; 27 | var includeUserAgent = false; 28 | var tag = null; 29 | var sendConsoleErrors = false; 30 | var logToConsole = true; 31 | var loggingEnabled = true; 32 | var labels = {}; 33 | var deleteHeaders = false; 34 | 35 | // The minimum level of messages that should be sent to loggly. 36 | var level = 0; 37 | 38 | var token = null; 39 | var endpoint = '://logs-01.loggly.com/inputs/'; 40 | 41 | var buildUrl = function () { 42 | return (https ? 'https' : 'http') + endpoint + token + '/tag/' + (tag ? tag : 'AngularJS' ) + '/'; 43 | }; 44 | 45 | this.setExtra = function (d) { 46 | extra = d; 47 | return self; 48 | }; 49 | 50 | this.fields = function ( d ) { 51 | if( angular.isDefined( d ) ) { 52 | angular.extend(extra, d); 53 | return self; 54 | } 55 | 56 | return extra; 57 | }; 58 | 59 | this.labels = function(l) { 60 | if (angular.isObject(l)) { 61 | labels = l; 62 | return self; 63 | } 64 | 65 | return labels; 66 | }; 67 | 68 | this.inputToken = function ( s ) { 69 | if (angular.isDefined(s)) { 70 | token = s; 71 | return self; 72 | } 73 | 74 | return token; 75 | }; 76 | 77 | this.useHttps = function (flag) { 78 | if (angular.isDefined(flag)) { 79 | https = !!flag; 80 | return self; 81 | } 82 | 83 | return https; 84 | }; 85 | 86 | this.includeUrl = function (flag) { 87 | if (angular.isDefined(flag)) { 88 | includeCurrentUrl = !!flag; 89 | return self; 90 | } 91 | 92 | return includeCurrentUrl; 93 | }; 94 | 95 | this.includeTimestamp = function (flag) { 96 | if (angular.isDefined(flag)) { 97 | includeTimestamp = !!flag; 98 | return self; 99 | } 100 | 101 | return includeTimestamp; 102 | }; 103 | 104 | this.includeUserAgent = function (flag) { 105 | if (angular.isDefined(flag)) { 106 | includeUserAgent = !!flag; 107 | return self; 108 | } 109 | 110 | return includeUserAgent; 111 | }; 112 | 113 | this.inputTag = function (usrTag){ 114 | if (angular.isDefined(usrTag)) { 115 | tag = usrTag; 116 | return self; 117 | } 118 | 119 | return tag; 120 | }; 121 | 122 | this.sendConsoleErrors = function (flag){ 123 | if (angular.isDefined(flag)) { 124 | sendConsoleErrors = !!flag; 125 | return self; 126 | } 127 | 128 | return sendConsoleErrors; 129 | }; 130 | 131 | this.level = function ( name ) { 132 | 133 | if( angular.isDefined( name ) ) { 134 | var newLevel = logLevels.indexOf( name.toUpperCase() ); 135 | 136 | if( newLevel < 0 ) { 137 | throw "Invalid logging level specified: " + name; 138 | } else { 139 | level = newLevel; 140 | } 141 | 142 | return self; 143 | } 144 | 145 | return logLevels[level]; 146 | }; 147 | 148 | this.isLevelEnabled = function( name ) { 149 | return logLevels.indexOf( name.toUpperCase() ) >= level; 150 | }; 151 | 152 | this.loggingEnabled = function (flag) { 153 | if (angular.isDefined(flag)) { 154 | loggingEnabled = !!flag; 155 | return self; 156 | } 157 | 158 | return loggingEnabled; 159 | }; 160 | 161 | 162 | this.logToConsole = function (flag) { 163 | if (angular.isDefined(flag)) { 164 | logToConsole = !!flag; 165 | return self; 166 | } 167 | 168 | return logToConsole; 169 | }; 170 | 171 | this.deleteHeaders = function (flag) { 172 | if (angular.isDefined(flag)) { 173 | deleteHeaders = !!flag; 174 | return self; 175 | } 176 | 177 | return deleteHeaders; 178 | }; 179 | 180 | 181 | 182 | this.$get = [ '$injector', function ($injector) { 183 | 184 | var lastLog = null; 185 | 186 | 187 | /** 188 | * Send the specified data to loggly as a json message. 189 | * @param data 190 | */ 191 | var sendMessage = function (data) { 192 | //If a token is not configured, don't do anything. 193 | if (!token || !loggingEnabled) { 194 | return; 195 | } 196 | 197 | //TODO we're injecting this here to resolve circular dependency issues. Is this safe? 198 | var $window = $injector.get( '$window' ); 199 | var $location = $injector.get( '$location' ); 200 | //we're injecting $http 201 | var $http = $injector.get( '$http' ); 202 | 203 | lastLog = new Date(); 204 | 205 | var sentData = angular.extend({}, extra, data); 206 | 207 | if (includeCurrentUrl) { 208 | sentData.url = $location.absUrl(); 209 | } 210 | 211 | if( includeTimestamp ) { 212 | sentData.timestamp = lastLog.toISOString(); 213 | } 214 | 215 | if( includeUserAgent ) { 216 | sentData.userAgent = $window.navigator.userAgent; 217 | } 218 | 219 | //Loggly's API doesn't send us cross-domain headers, so we can't interact directly 220 | //Set header 221 | var config = { 222 | headers: { 223 | 'Content-Type': 'text/plain' 224 | }, 225 | withCredentials: false 226 | }; 227 | 228 | if (deleteHeaders) { 229 | //Delete other headers - this tells browser it's no need to pre-flight OPTIONS request 230 | var headersToDelete = Object.keys($http.defaults.headers.common).concat(Object.keys($http.defaults.headers.post)); 231 | headersToDelete = headersToDelete.filter(function(item) { 232 | return item !== 'Accept' && item !== 'Content-Type'; 233 | }); 234 | 235 | for (var index = 0; index < headersToDelete.length; index++) { 236 | var headerName = headersToDelete[index]; 237 | config.headers[headerName] = undefined; 238 | } 239 | } 240 | 241 | // Apply labels 242 | for (var label in labels) { 243 | if (label in sentData) { 244 | sentData[labels[label]] = sentData[label]; 245 | delete sentData[label]; 246 | } 247 | } 248 | 249 | //Ajax call to send data to loggly 250 | $http.post(buildUrl(),sentData,config).catch(console.error); 251 | }; 252 | 253 | var attach = function() { 254 | }; 255 | 256 | var inputToken = function(s) { 257 | if (angular.isDefined(s)) { 258 | token = s; 259 | } 260 | 261 | return token; 262 | }; 263 | 264 | return { 265 | lastLog: function(){ return lastLog; }, 266 | sendConsoleErrors: function(){ return sendConsoleErrors; }, 267 | level : function() { return level; }, 268 | loggingEnabled: self.loggingEnabled, 269 | isLevelEnabled : self.isLevelEnabled, 270 | inputTag: self.inputTag, 271 | attach: attach, 272 | sendMessage: sendMessage, 273 | logToConsole: logToConsole, 274 | inputToken: inputToken, 275 | 276 | /** 277 | * Get or set the fields to be sent with all logged events. 278 | * @param d 279 | * @returns {*} 280 | */ 281 | fields: function( d ) { 282 | if( angular.isDefined( d ) ) { 283 | self.fields( d ); 284 | } 285 | return self.fields(); 286 | } 287 | }; 288 | }]; 289 | 290 | } ); 291 | 292 | 293 | angular.module( 'logglyLogger', ['logglyLogger.logger'] ) 294 | .config( [ '$provide', function( $provide ) { 295 | 296 | $provide.decorator('$log', [ "$delegate", '$injector', function ( $delegate, $injector ) { 297 | 298 | var logger = $injector.get('LogglyLogger'); 299 | 300 | // install a window error handler 301 | if(logger.sendConsoleErrors() === true) { 302 | var _onerror = window.onerror; 303 | 304 | //send console error messages to Loggly 305 | window.onerror = function (msg, url, line, col, error) { 306 | logger.sendMessage({ 307 | level : 'ERROR', 308 | message: msg, 309 | line: line, 310 | col: col, 311 | stack: error && error.stack 312 | }); 313 | 314 | if (_onerror && typeof _onerror === 'function') { 315 | _onerror.apply(window, arguments); 316 | } 317 | }; 318 | } 319 | 320 | var wrapLogFunction = function(logFn, level, loggerName) { 321 | 322 | var wrappedFn = function () { 323 | var args = Array.prototype.slice.call(arguments); 324 | 325 | if(logger.logToConsole) { 326 | logFn.apply(null, args); 327 | } 328 | 329 | // Skip messages that have a level that's lower than the configured level for this logger. 330 | if(!logger.loggingEnabled() || !logger.isLevelEnabled( level ) ) { 331 | return; 332 | } 333 | 334 | var msg = (args.length === 1 ? args[0] : args) || {}; 335 | var sending = { level: level }; 336 | 337 | if(angular.isDefined(msg.stack) || (angular.isDefined(msg[0]) && angular.isDefined(msg[0].stack))) { 338 | //handling console errors 339 | if(logger.sendConsoleErrors() === true) { 340 | sending.message = msg.message ? msg.message : (msg[0] && msg[0].message) ? msg[0].message : null; 341 | sending.stack = msg.stack || msg[0].stack; 342 | } 343 | else { 344 | return; 345 | } 346 | } 347 | else if(angular.isObject(msg)) { 348 | //handling JSON objects 349 | sending = angular.extend({}, msg, sending); 350 | } 351 | else{ 352 | //sending plain text 353 | sending.message = msg; 354 | } 355 | 356 | if( loggerName ) { 357 | sending.logger = msg; 358 | } 359 | 360 | //Send the message to through the loggly sender 361 | logger.sendMessage( sending ); 362 | }; 363 | 364 | wrappedFn.logs = []; 365 | 366 | return wrappedFn; 367 | }; 368 | 369 | var _$log = (function ($delegate) { 370 | return { 371 | log: $delegate.log, 372 | info: $delegate.info, 373 | warn: $delegate.warn, 374 | error: $delegate.error 375 | }; 376 | })($delegate); 377 | 378 | var getLogger = function ( name ) { 379 | return { 380 | log: wrapLogFunction( _$log.log, 'INFO', name ), 381 | debug: wrapLogFunction( _$log.debug, 'DEBUG', name ), 382 | info: wrapLogFunction( _$log.info, 'INFO', name ), 383 | warn: wrapLogFunction( _$log.warn, 'WARN', name ), 384 | error: wrapLogFunction( _$log.error, 'ERROR', name ) 385 | }; 386 | }; 387 | 388 | //wrap the existing API 389 | $delegate.log = wrapLogFunction($delegate.log, 'INFO'); 390 | $delegate.debug = wrapLogFunction($delegate.debug, 'DEBUG'); 391 | $delegate.info = wrapLogFunction($delegate.info, 'INFO'); 392 | $delegate.warn = wrapLogFunction($delegate.warn, 'WARN'); 393 | $delegate.error = wrapLogFunction($delegate.error, 'ERROR'); 394 | 395 | //Add some methods 396 | $delegate.getLogger = getLogger; 397 | 398 | return $delegate; 399 | }]); 400 | 401 | }]); 402 | 403 | 404 | 405 | })(window.angular); 406 | -------------------------------------------------------------------------------- /angular-loggly-logger.min.js: -------------------------------------------------------------------------------- 1 | !function(a){"use strict";a.module("logglyLogger.logger",[]).provider("LogglyLogger",function(){var b=this,c=["DEBUG","INFO","WARN","ERROR"],d=!0,e={},f=!1,g=!1,h=!1,i=null,j=!1,k=!0,l=!0,m={},n=!1,o=0,p=null,q=function(){return(d?"https":"http")+"://logs-01.loggly.com/inputs/"+p+"/tag/"+(i||"AngularJS")+"/"};this.setExtra=function(a){return e=a,b},this.fields=function(c){return a.isDefined(c)?(a.extend(e,c),b):e},this.labels=function(c){return a.isObject(c)?(m=c,b):m},this.inputToken=function(c){return a.isDefined(c)?(p=c,b):p},this.useHttps=function(c){return a.isDefined(c)?(d=!!c,b):d},this.includeUrl=function(c){return a.isDefined(c)?(f=!!c,b):f},this.includeTimestamp=function(c){return a.isDefined(c)?(g=!!c,b):g},this.includeUserAgent=function(c){return a.isDefined(c)?(h=!!c,b):h},this.inputTag=function(c){return a.isDefined(c)?(i=c,b):i},this.sendConsoleErrors=function(c){return a.isDefined(c)?(j=!!c,b):j},this.level=function(d){if(a.isDefined(d)){var e=c.indexOf(d.toUpperCase());if(e<0)throw"Invalid logging level specified: "+d;return o=e,b}return c[o]},this.isLevelEnabled=function(a){return c.indexOf(a.toUpperCase())>=o},this.loggingEnabled=function(c){return a.isDefined(c)?(l=!!c,b):l},this.logToConsole=function(c){return a.isDefined(c)?(k=!!c,b):k},this.deleteHeaders=function(c){return a.isDefined(c)?(n=!!c,b):n},this.$get=["$injector",function(c){var d=null,i=function(b){if(p&&l){var i=c.get("$window"),j=c.get("$location"),k=c.get("$http");d=new Date;var o=a.extend({},e,b);f&&(o.url=j.absUrl()),g&&(o.timestamp=d.toISOString()),h&&(o.userAgent=i.navigator.userAgent);var r={headers:{"Content-Type":"text/plain"},withCredentials:!1};if(n){var s=Object.keys(k.defaults.headers.common).concat(Object.keys(k.defaults.headers.post));s=s.filter(function(a){return"Accept"!==a&&"Content-Type"!==a});for(var t=0;t 2 | 3 | 4 | 5 | 6 | 7 | 8 |

9 | Make sure you edit demo/app.js, and add a valid inputToken to the "logglyInputToken" constant's value.! 10 |

11 | 12 |
13 |

14 |
15 | 16 |

17 | 18 |

19 | 20 | 21 | 22 |

23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('angular'); 2 | require('./angular-loggly-logger'); 3 | module.exports = 'logglyLogger'; 4 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config){ 2 | config.set({ 3 | 4 | files : [ 5 | 'node_modules/angular/angular.js', 6 | 'node_modules/angular-mocks/angular-mocks.js', 7 | 'angular-loggly-logger.js', 8 | 'test/unit/**/*.js' 9 | ], 10 | 11 | autoWatch : true, 12 | 13 | frameworks: ['jasmine'], 14 | 15 | browsers : ['Chrome','Firefox'], 16 | 17 | reporters: ['spec','coverage', 'coveralls'], 18 | 19 | preprocessors: { 20 | // source files, that you wanna generate coverage for 21 | // do not include tests or libraries 22 | // (these files will be instrumented by Istanbul) 23 | '*.js': ['coverage'] 24 | }, 25 | 26 | plugins : [ 27 | 'karma-chrome-launcher', 28 | 'karma-firefox-launcher', 29 | 'karma-phantomjs-launcher', 30 | 'karma-spec-reporter', 31 | 'karma-jasmine', 32 | 'karma-coverage', 33 | 'karma-coveralls' 34 | ], 35 | 36 | junitReporter : { 37 | outputFile: 'build/reports/test-results/unit.xml', 38 | suite: 'unit' 39 | }, 40 | 41 | coverageReporter: { 42 | dir : 'build/reports/coverage/', 43 | reporters: [ 44 | // reporters not supporting the `file` property 45 | { type: 'html', subdir: 'report-html' }, 46 | { type: 'lcov', subdir: 'report-lcov' }, 47 | // reporters supporting the `file` property, use `subdir` to directly 48 | // output them in the `dir` directory 49 | { type: 'cobertura', subdir: '.', file: 'cobertura.txt' }, 50 | { type: 'lcovonly', subdir: '.', file: 'report-lcovonly.txt' }, 51 | { type: 'teamcity', subdir: '.', file: 'teamcity.txt' }, 52 | { type: 'text', subdir: '.', file: 'text.txt' }, 53 | { type: 'text-summary', subdir: '.', file: 'text-summary.txt' }, 54 | ] 55 | } 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-loggly-logger", 3 | "version": "0.3.2", 4 | "main": "index.js", 5 | "description": "An AngularJs service and $log decorator for Loggly", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/ajbrown/angular-loggly-logger.git" 9 | }, 10 | "bugs": "https://github.com/ajbrown/angular-loggly-logger/issues", 11 | "author": "A.J. Brown (http://ajbrown.org)", 12 | "contributors": [ 13 | { 14 | "name": "Milos Janjic", 15 | "email": "milosh012@gmail.com", 16 | "url": "https://github.com/milosh012", 17 | "contributions": 1, 18 | "additions": 7, 19 | "deletions": 6, 20 | "hireable": true 21 | }, 22 | { 23 | "name": "Will McClellan", 24 | "email": null, 25 | "url": "https://github.com/willmcclellan", 26 | "contributions": 1, 27 | "additions": 1, 28 | "deletions": 0, 29 | "hireable": null 30 | }, 31 | { 32 | "name": "Theodore Brockman", 33 | "email": null, 34 | "url": "https://github.com/tbrockman", 35 | "contributions": 1, 36 | "additions": 48, 37 | "deletions": 4, 38 | "hireable": null 39 | }, 40 | { 41 | "name": "Guillaume Grégoire", 42 | "email": null, 43 | "url": "https://github.com/ggregoire", 44 | "contributions": 1, 45 | "additions": 3, 46 | "deletions": 3, 47 | "hireable": true 48 | }, 49 | { 50 | "name": "Willian Ganzert Lopes", 51 | "email": null, 52 | "url": "https://github.com/willianganzert", 53 | "contributions": 1, 54 | "additions": 6, 55 | "deletions": 6, 56 | "hireable": true 57 | }, 58 | { 59 | "name": "James Whitney", 60 | "email": "james@whitney.io", 61 | "url": "https://github.com/whitneyit", 62 | "contributions": 2, 63 | "additions": 32, 64 | "deletions": 26, 65 | "hireable": null 66 | }, 67 | { 68 | "name": "Fkscorpion", 69 | "email": null, 70 | "url": "https://github.com/Fkscorpion", 71 | "contributions": 1, 72 | "additions": 4, 73 | "deletions": 4, 74 | "hireable": null 75 | }, 76 | { 77 | "name": "Korotaev Alexander", 78 | "email": null, 79 | "url": "https://github.com/lekzd", 80 | "contributions": 1, 81 | "additions": 3, 82 | "deletions": 2, 83 | "hireable": null 84 | }, 85 | { 86 | "name": "Elisha Terada", 87 | "email": "elishaterada@gmail.com", 88 | "url": "https://github.com/elishaterada", 89 | "contributions": 2, 90 | "additions": 122, 91 | "deletions": 15, 92 | "hireable": null 93 | }, 94 | { 95 | "name": "Jason Skowronski", 96 | "email": null, 97 | "url": "https://github.com/mostlyjason", 98 | "contributions": 3, 99 | "additions": 50, 100 | "deletions": 25, 101 | "hireable": null 102 | }, 103 | { 104 | "name": null, 105 | "email": null, 106 | "url": "https://github.com/varshneyjayant", 107 | "contributions": 3, 108 | "additions": 359, 109 | "deletions": 364, 110 | "hireable": null 111 | }, 112 | { 113 | "name": "A.J. Brown", 114 | "email": "aj@ajbrown.org", 115 | "url": "https://github.com/ajbrown", 116 | "contributions": 45, 117 | "additions": 1624, 118 | "deletions": 669, 119 | "hireable": null 120 | }, 121 | { 122 | "name": "Vin Halbwachs", 123 | "email": null, 124 | "url": "https://github.com/vhalbwachs", 125 | "contributions": 2, 126 | "additions": 56, 127 | "deletions": 52, 128 | "hireable": true 129 | }, 130 | { 131 | "name": "Lukáš Marek", 132 | "email": "lukas.marek@gmail.com", 133 | "url": "https://github.com/krtek", 134 | "contributions": 1, 135 | "additions": 72, 136 | "deletions": 3, 137 | "hireable": true 138 | }, 139 | { 140 | "name": "Brennon Bortz", 141 | "email": "brennon@brennonbortz.com", 142 | "url": "https://github.com/brennon", 143 | "contributions": 2, 144 | "additions": 19, 145 | "deletions": 4, 146 | "hireable": true 147 | } 148 | ], 149 | "keywords": [ 150 | "angular", 151 | "logging", 152 | "logger", 153 | "log" 154 | ], 155 | "license": "MIT", 156 | "devDependencies": { 157 | "angular-mocks": "^1.5.6", 158 | "grunt": "^1.0.1", 159 | "grunt-browser-sync": "^2.2.0", 160 | "grunt-connect-proxy": "^0.2.0", 161 | "grunt-contrib-jshint": "^1.1.0", 162 | "grunt-contrib-uglify": "^2.1.0", 163 | "grunt-contrib-watch": "^1.0.0", 164 | "grunt-karma": "^2.0.0", 165 | "http-server": "^0.6.1", 166 | "jasmine-core": "^2.2.0", 167 | "karma": "^1.5.0", 168 | "karma-chrome-launcher": "^2.0.0", 169 | "karma-coverage": "^1.1.1", 170 | "karma-coveralls": "^1.1.2", 171 | "karma-firefox-launcher": "^1.0.0", 172 | "karma-jasmine": "^1.1.0", 173 | "karma-phantomjs-launcher": "^1.0.2", 174 | "karma-spec-reporter": "0.0.26", 175 | "protractor": "~0.17.0", 176 | "shelljs": "^0.2.6" 177 | }, 178 | "scripts": { 179 | "start": "http-server -p 8000", 180 | "update-webdriver": "webdriver-manager update", 181 | "test": "grunt karma:unit", 182 | "test-single-run": "grunt karma:travis:run", 183 | "update-index-async": "node -e \"require('shelljs/global'); sed('-i', /\\/\\/@@NG_LOADER_START@@[\\s\\S]*\\/\\/@@NG_LOADER_END@@/, '//@@NG_LOADER_START@@\\n' + cat('bower_components/angular-loader/angular-loader.min.js') + '\\n//@@NG_LOADER_END@@', 'app/index-async.html');\"" 184 | }, 185 | "dependencies": { 186 | "angular": "^1.6.2" 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /test/unit/logglySenderSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for services go here */ 4 | 5 | describe('logglyLogger Module:', function() { 6 | var logglyLoggerProvider, 7 | levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'], 8 | realOnerror, mockOnerror; 9 | 10 | beforeEach(function () { 11 | // Karma defines window.onerror to kill the test when it's called, so stub out window.onerror 12 | // Jasmine still wraps all tests in a try/catch, so tests that throw errors will still be handled gracefully 13 | realOnerror = window.onerror; 14 | mockOnerror = jasmine.createSpy(); 15 | window.onerror = mockOnerror; 16 | 17 | // Initialize the service provider 18 | // by injecting it to a fake module's config block 19 | var fakeModule = angular.module('testing.harness', ['logglyLogger'], function () {}); 20 | fakeModule.config( function(LogglyLoggerProvider) { 21 | logglyLoggerProvider = LogglyLoggerProvider; 22 | logglyLoggerProvider.sendConsoleErrors(true) 23 | }); 24 | 25 | // Initialize test.app injector 26 | module('logglyLogger', 'testing.harness'); 27 | 28 | // Kickstart the injectors previously registered 29 | // with calls to angular.mock.module 30 | inject(function() {}); 31 | }); 32 | 33 | afterEach(function() { 34 | window.onerror = realOnerror; 35 | }); 36 | 37 | describe( 'LogglyLoggerProvider', function() { 38 | it( 'can have a logging level configured', function() { 39 | 40 | for( var i in levels ) { 41 | logglyLoggerProvider.level( levels[i] ); 42 | expect( logglyLoggerProvider.level() ).toEqual( levels[i] ); 43 | } 44 | }); 45 | 46 | it( 'will throw an exception if an invalid level is supplied', function() { 47 | 48 | expect( function() { logglyLoggerProvider.level('TEST') } ).toThrow(); 49 | }); 50 | 51 | it( 'can determine if a given level is enabled', function() { 52 | for( var a in levels ) { 53 | 54 | logglyLoggerProvider.level( levels[a] ); 55 | 56 | for( var b in levels ) { 57 | expect( logglyLoggerProvider.isLevelEnabled( levels[b] )).toBe( b >= a ); 58 | } 59 | } 60 | }); 61 | 62 | it( 'can specify extra fields to be sent with each log message', function() { 63 | var extra = { "test": "extra" }; 64 | 65 | logglyLoggerProvider.fields( extra ); 66 | 67 | expect( logglyLoggerProvider.fields()).toEqual( extra ); 68 | 69 | }); 70 | 71 | }); 72 | 73 | describe( 'LogglyLogger', function() { 74 | var token = 'test123456', 75 | tag = 'logglyLogger', 76 | message, service, $log, $httpBackend, $http; 77 | 78 | beforeEach(function () { 79 | message = {message: 'A test message'}; 80 | 81 | inject(function ($injector) { 82 | $log = $injector.get('$log'); 83 | $httpBackend = $injector.get('$httpBackend'); 84 | $http = $injector.get('$http'); 85 | service = $injector.get('LogglyLogger'); 86 | service.attach(); 87 | }); 88 | }); 89 | 90 | afterEach(function () { 91 | $httpBackend.verifyNoOutstandingExpectation(); 92 | $httpBackend.verifyNoOutstandingRequest(); 93 | }); 94 | 95 | it('should be registered', function () { 96 | expect(service).not.toBe(null); 97 | }); 98 | 99 | it('will not send a message to loggly if a token is not specified', function () { 100 | var url = 'https://logs-01.loggly.com'; 101 | var forbiddenCallTriggered = false; 102 | $httpBackend 103 | .when(url) 104 | .respond(function () { 105 | forbiddenCallTriggered = true; 106 | return [400, '']; 107 | }); 108 | 109 | service.sendMessage("A test message"); 110 | // Let test fail when request was triggered. 111 | expect(forbiddenCallTriggered).toBe(false); 112 | }); 113 | 114 | it('will send a message to loggly only when properly configured', function () { 115 | var expectMessage = { message: 'A test message' }; 116 | var tag = 'logglyLogger'; 117 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/logglyLogger/'; 118 | var generatedURL; 119 | 120 | logglyLoggerProvider.inputToken(token); 121 | logglyLoggerProvider.includeUrl(false); 122 | logglyLoggerProvider.inputTag(tag); 123 | 124 | $httpBackend 125 | .expectPOST(testURL, expectMessage) 126 | .respond(function (method, url, data) { 127 | generatedURL = url; 128 | return [200, "", {}]; 129 | }); 130 | 131 | service.sendMessage(message); 132 | $httpBackend.flush(); 133 | 134 | expect(generatedURL).toEqual(testURL); 135 | }); 136 | 137 | it('will use http if useHttps is set to false', function () { 138 | var testURL = 'http://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 139 | var generatedURL; 140 | 141 | logglyLoggerProvider.inputToken(token); 142 | logglyLoggerProvider.useHttps(false); 143 | logglyLoggerProvider.includeUrl(false); 144 | 145 | $httpBackend 146 | .expectPOST(testURL, message) 147 | .respond(function (method, url, data) { 148 | generatedURL = new URL(url); 149 | return [200, "", {}]; 150 | }); 151 | 152 | service.sendMessage(message); 153 | 154 | $httpBackend.flush(); 155 | 156 | expect(generatedURL.protocol).toEqual('http:'); 157 | 158 | }); 159 | 160 | it('will include the current url if includeUrl() is not set to false', function () { 161 | var expectMessage = angular.extend({}, message, { url: 'http://bloggly.com' }); 162 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 163 | var payload; 164 | 165 | inject(function ($injector) { 166 | // mock browser url 167 | $injector.get('$browser').url('http://bloggly.com'); 168 | }); 169 | 170 | logglyLoggerProvider.inputToken( token ); 171 | logglyLoggerProvider.includeUrl( true ); 172 | 173 | $httpBackend 174 | .expectPOST(testURL, expectMessage) 175 | .respond(function (method, url, data) { 176 | payload = JSON.parse(data); 177 | return [200, "", {}]; 178 | }); 179 | 180 | service.sendMessage( message ); 181 | 182 | $httpBackend.flush(); 183 | expect(payload.url).toEqual('http://bloggly.com'); 184 | 185 | }); 186 | 187 | it('will include the current userAgent if includeUserAgent() is not set to false', function () { 188 | var expectMessage = angular.extend({}, message, { userAgent: window.navigator.userAgent }); 189 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 190 | var payload; 191 | 192 | logglyLoggerProvider.inputToken( token ); 193 | logglyLoggerProvider.includeUserAgent( true ); 194 | 195 | $httpBackend 196 | .expectPOST(testURL, expectMessage) 197 | .respond(function (method, url, data) { 198 | payload = JSON.parse(data); 199 | return [200, "", {}]; 200 | }); 201 | 202 | service.sendMessage( message ); 203 | 204 | $httpBackend.flush(); 205 | expect(payload.userAgent).toEqual(window.navigator.userAgent); 206 | 207 | }); 208 | 209 | it( 'can set extra fields using the fields method', function() { 210 | var extra = { appVersion: '1.1.0', browser: 'Chrome' }; 211 | 212 | expect( service.fields( extra )).toEqual( extra ); 213 | expect( service.fields() ).toEqual( extra ); 214 | }); 215 | 216 | 217 | it( 'will include extra fields if set via provider and service', function() { 218 | var payload, payload2; 219 | var extra = { appVersion: '1.1.0', browser: 'Chrome' }; 220 | var expectMessage = angular.extend({}, message, extra); 221 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 222 | 223 | logglyLoggerProvider.inputToken( token ); 224 | 225 | 226 | logglyLoggerProvider.fields( extra ); 227 | $httpBackend 228 | .expectPOST(testURL, expectMessage) 229 | .respond(function (method, url, data) { 230 | payload = JSON.parse(data); 231 | return [200, "", {}]; 232 | }); 233 | service.sendMessage(message); 234 | 235 | $httpBackend.flush(); 236 | expect(payload).toEqual(expectMessage); 237 | 238 | var expectMessage2 = angular.extend({}, message, { appVersion: '1.1.0', browser: 'Chrome', username: 'baldrin' }); 239 | 240 | service.fields({username: "baldrin"}); 241 | $httpBackend 242 | .expectPOST(testURL, expectMessage2) 243 | .respond(function (method, url, data) { 244 | payload2 = JSON.parse(data); 245 | return [200, "", {}]; 246 | }); 247 | service.sendMessage(message); 248 | 249 | $httpBackend.flush(); 250 | expect(payload2).toEqual(expectMessage2); 251 | }); 252 | 253 | it( 'will include extra fields if set via the service', function() { 254 | var payload; 255 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 256 | var extra = { appVersion: '1.1.0', browser: 'Chrome' }; 257 | var expectMessage = angular.extend({}, message, extra); 258 | 259 | logglyLoggerProvider.inputToken( token ); 260 | logglyLoggerProvider.fields( extra ); 261 | 262 | $httpBackend 263 | .expectPOST(testURL, expectMessage) 264 | .respond(function (method, url, data) { 265 | payload = JSON.parse(data); 266 | return [200, "", {}]; 267 | }); 268 | 269 | service.sendMessage(message); 270 | 271 | $httpBackend.flush(); 272 | expect(payload).toEqual(expectMessage); 273 | }); 274 | 275 | it( '$log has a logglySender attached', function() { 276 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 277 | var payload, expectMessage; 278 | 279 | logglyLoggerProvider.inputToken( token ); 280 | logglyLoggerProvider.includeUrl( false ); 281 | 282 | angular.forEach( levels, function (level) { 283 | expectMessage = angular.extend({}, message, { level: level }); 284 | $httpBackend 285 | .expectPOST(testURL, expectMessage) 286 | .respond(function (method, url, data) { 287 | payload = JSON.parse(data); 288 | return [200, "", {}]; 289 | }); 290 | $log[level.toLowerCase()].call($log, message); 291 | $httpBackend.flush(); 292 | expect(payload.level).toEqual(level); 293 | }); 294 | }); 295 | 296 | it( 'will not send messages for levels that are not enabled', function() { 297 | spyOn(service, 'sendMessage').and.callThrough(); 298 | 299 | for( var a in levels ) { 300 | 301 | logglyLoggerProvider.level( levels[a] ); 302 | 303 | for( var b in levels ) { 304 | 305 | $log[levels[b].toLowerCase()].call($log, message.message); 306 | if( b >= a ) { 307 | expect(service.sendMessage).toHaveBeenCalled(); 308 | } else { 309 | expect(service.sendMessage).not.toHaveBeenCalled(); 310 | } 311 | 312 | service.sendMessage.calls.reset(); 313 | } 314 | } 315 | }); 316 | 317 | it( 'will not send messages if logs are not enabled', function() { 318 | var url = 'https://logs-01.loggly.com/inputs/' + token; 319 | var tag = 'logglyLogger'; 320 | 321 | logglyLoggerProvider.inputToken(token); 322 | logglyLoggerProvider.includeUrl(false); 323 | logglyLoggerProvider.loggingEnabled(false); 324 | logglyLoggerProvider.inputTag(tag); 325 | 326 | var forbiddenCallTriggered = false; 327 | $httpBackend 328 | .when(url) 329 | .respond(function () { 330 | forbiddenCallTriggered = true; 331 | return [400, '']; 332 | }); 333 | service.sendMessage("A test message"); 334 | // Let test fail when request was triggered. 335 | expect(forbiddenCallTriggered).toBe(false); 336 | }); 337 | 338 | it( 'will disable logs after config had them enabled and not send messages', function() { 339 | var tag = 'logglyLogger'; 340 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/logglyLogger/'; 341 | var generatedURL; 342 | 343 | logglyLoggerProvider.inputToken(token); 344 | logglyLoggerProvider.includeUrl(false); 345 | logglyLoggerProvider.loggingEnabled(true); 346 | logglyLoggerProvider.inputTag(tag); 347 | 348 | $httpBackend 349 | .expectPOST(testURL, message) 350 | .respond(function (method, url, data) { 351 | generatedURL = url; 352 | return [200, "", {}]; 353 | }); 354 | 355 | service.sendMessage(message); 356 | $httpBackend.flush(); 357 | expect(generatedURL).toEqual(testURL); 358 | }); 359 | 360 | it( 'will not fail if the logged message is null or undefined', function() { 361 | var undefinedMessage; 362 | var nullMessage = null; 363 | 364 | expect( function() { 365 | $log.debug( undefinedMessage ); 366 | }).not.toThrow(); 367 | 368 | expect( function() { 369 | $log.debug( nullMessage ); 370 | }).not.toThrow(); 371 | }); 372 | 373 | it( 'can update the Loggly token', function() { 374 | logglyLoggerProvider.inputToken(''); 375 | service.inputToken('foo'); 376 | expect(logglyLoggerProvider.inputToken()).toEqual('foo'); 377 | }); 378 | 379 | it('will override labels as specified', function () { 380 | var expectMessage = { msg: message.message }; 381 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 382 | 383 | logglyLoggerProvider.inputToken( token ); 384 | logglyLoggerProvider.labels({ 385 | message: 'msg' 386 | }); 387 | 388 | $httpBackend 389 | .whenPOST(testURL) 390 | .respond(function (method, url, data) { 391 | expect(JSON.parse(data)).toEqual(expectMessage); 392 | return [200, "", {}]; 393 | }); 394 | 395 | service.sendMessage( message ); 396 | 397 | $httpBackend.flush(); 398 | }); 399 | 400 | it('should log console errors if sendConsoleErrors() is not false', function() { 401 | var error = new Error("some error"); 402 | var expectMessage = {level: 'ERROR', message: error.message, line: 1, col: 2, stack: error.stack}; 403 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 404 | 405 | logglyLoggerProvider.inputToken(token); 406 | 407 | $httpBackend 408 | .expectPOST(testURL, expectMessage) 409 | .respond(function () { 410 | return [200, "", {}]; 411 | }); 412 | 413 | window.onerror(error.message, "foo.com", 1, 2, error); 414 | 415 | // Ensure the preexisting window.onerror is called 416 | expect(mockOnerror).toHaveBeenCalled(); 417 | 418 | $httpBackend.flush(); 419 | }); 420 | 421 | it('should keep http headers if deleteHeaders is set to false', function() { 422 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 423 | $http.defaults.headers.common.Authorization = 'token'; 424 | logglyLoggerProvider.inputToken(token); 425 | 426 | $httpBackend 427 | .expectPOST(testURL, {}, function(headers) { 428 | return headers['Authorization'] === 'token'; 429 | }) 430 | .respond(function () { 431 | return [200, "", {}]; 432 | }); 433 | 434 | service.sendMessage("A test message"); 435 | 436 | $httpBackend.flush(); 437 | }); 438 | 439 | it('should delete http headers if deleteHeaders is set to true', function() { 440 | var testURL = 'https://logs-01.loggly.com/inputs/test123456/tag/AngularJS/'; 441 | $http.defaults.headers.common.Authorization = 'token'; 442 | logglyLoggerProvider.deleteHeaders(true); 443 | logglyLoggerProvider.inputToken(token); 444 | 445 | $httpBackend 446 | .expectPOST(testURL, {}, function(headers) { 447 | return headers['Authorization'] === undefined; 448 | }) 449 | .respond(function () { 450 | return [200, "", {}]; 451 | }); 452 | 453 | service.sendMessage("A test message"); 454 | 455 | $httpBackend.flush(); 456 | }); 457 | }); 458 | }); 459 | --------------------------------------------------------------------------------