semantic_logger 2.21.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'socket'
4
+ require 'json'
5
+
6
+ # Log to any HTTP(S) server that accepts log messages in JSON form
7
+ #
8
+ # Features:
9
+ # * JSON Formatted messages.
10
+ # * Uses a persistent http connection, if the server supports it.
11
+ # * SSL encryption (https).
12
+ #
13
+ # Example:
14
+ # appender = SemanticLogger::Appender::Http.new(
15
+ # url: 'http://localhost:8088/path'
16
+ # )
17
+ #
18
+ # # Optional: Exclude health_check log entries, etc.
19
+ # appender.filter = Proc.new { |log| log.message !~ /(health_check|Not logged in)/}
20
+ #
21
+ # SemanticLogger.add_appender(appender)
22
+ class SemanticLogger::Appender::Http < SemanticLogger::Appender::Base
23
+ attr_accessor :username, :application, :host, :compress, :header
24
+ attr_reader :http, :url, :server, :port, :request_uri, :ssl_options
25
+
26
+ # Create HTTP(S) log appender
27
+ #
28
+ # Parameters:
29
+ # url: [String]
30
+ # Valid URL to post to.
31
+ # Example: http://example.com/some_path
32
+ # To enable SSL include https in the URL.
33
+ # Example: https://example.com/some_path
34
+ # verify_mode will default: OpenSSL::SSL::VERIFY_PEER
35
+ #
36
+ # application: [String]
37
+ # Name of this application to appear in log messages.
38
+ # Default: SemanticLogger.application
39
+ #
40
+ # host: [String]
41
+ # Name of this host to appear in log messages.
42
+ # Default: SemanticLogger.host
43
+ #
44
+ # username: [String]
45
+ # User name for basic Authentication.
46
+ # Default: nil ( do not use basic auth )
47
+ #
48
+ # password: [String]
49
+ # Password for basic Authentication.
50
+ #
51
+ # compress: [true|false]
52
+ # Whether to compress the JSON string with GZip.
53
+ # Default: false
54
+ #
55
+ # ssl: [Hash]
56
+ # Specific SSL options: For more details see NET::HTTP.start
57
+ # ca_file, ca_path, cert, cert_store, ciphers, key, open_timeout, read_timeout, ssl_timeout,
58
+ # ssl_version, use_ssl, verify_callback, verify_depth and verify_mode.
59
+ #
60
+ # level: [:trace | :debug | :info | :warn | :error | :fatal]
61
+ # Override the log level for this appender.
62
+ # Default: SemanticLogger.default_level
63
+ #
64
+ # filter: [Regexp|Proc]
65
+ # RegExp: Only include log messages where the class name matches the supplied.
66
+ # regular expression. All other messages will be ignored.
67
+ # Proc: Only include log messages where the supplied Proc returns true
68
+ # The Proc must return true or false.
69
+ def initialize(options, &block)
70
+ @options = options.dup
71
+ level = @options.delete(:level)
72
+ filter = @options.delete(:filter)
73
+ @url = options.delete(:url)
74
+ @ssl_options = options.delete(:ssl)
75
+ @username = options.delete(:username)
76
+ @password = options.delete(:password)
77
+ @application = options.delete(:application) || 'Semantic Logger'
78
+ @host = options.delete(:host) || SemanticLogger.host
79
+ @compress = options.delete(:compress) || false
80
+ raise(ArgumentError, "Unknown options: #{options.inspect}") if options.size > 0
81
+
82
+ raise(ArgumentError, 'Missing mandatory parameter :url') unless @url
83
+
84
+ @header = {
85
+ 'Accept' => 'application/json',
86
+ 'Content-Type' => 'application/json'
87
+ }
88
+ @header['Content-Encoding'] = 'gzip' if @compress
89
+
90
+ uri = URI.parse(@url)
91
+ (@ssl_options ||= {})[:use_ssl] = true if uri.scheme == 'https'
92
+
93
+ @server = uri.host
94
+ @port = uri.port
95
+ @username = uri.user if !@username && uri.user
96
+ @password = uri.password if !@password && uri.password
97
+ @request_uri = uri.request_uri
98
+
99
+ reopen
100
+
101
+ # Pass on the level and custom formatter if supplied
102
+ super(level, filter, &block)
103
+ end
104
+
105
+ # Re-open after process fork
106
+ def reopen
107
+ # On Ruby v2.0 and greater, Net::HTTP.new uses a persistent connection if the server allows it
108
+ @http = @ssl_options ? Net::HTTP.new(server, port, @ssl_options) : Net::HTTP.new(server, port)
109
+ end
110
+
111
+ # Forward log messages to HTTP Server
112
+ def log(log)
113
+ return false if (level_index > (log.level_index || 0)) ||
114
+ !include_message?(log) # Filtered out?
115
+
116
+ post(formatter.call(log, self))
117
+ end
118
+
119
+ # Use the JSON formatter
120
+ def default_formatter
121
+ self.class.json_formatter
122
+ end
123
+
124
+ private
125
+
126
+ def compress_data(data)
127
+ str = StringIO.new
128
+ gz = Zlib::GzipWriter.new(str)
129
+ gz << data
130
+ gz.close
131
+ str.string
132
+ end
133
+
134
+ # HTTP Post
135
+ def post(body, request_uri = nil)
136
+ request = Net::HTTP::Post.new(request_uri || @request_uri, @header)
137
+ process_request(request, body)
138
+ end
139
+
140
+ # HTTP Put
141
+ def put(body, request_uri = nil)
142
+ request = Net::HTTP::Put.new(request_uri || @request_uri, @header)
143
+ process_request(request, body)
144
+ end
145
+
146
+ # HTTP Delete
147
+ def delete(request_uri = nil)
148
+ request = Net::HTTP::Delete.new(request_uri || @request_uri, @header)
149
+ process_request(request)
150
+ end
151
+
152
+ # Process HTTP Request
153
+ def process_request(request, body = nil)
154
+ if body
155
+ body = compress_data(body) if compress
156
+ request.body = body
157
+ end
158
+ request.basic_auth(@username, @password) if @username
159
+ response = @http.request(request)
160
+ if response.code == '200' || response.code == '201'
161
+ true
162
+ else
163
+ # Failures are logged to the global semantic logger failsafe logger (Usually stderr or file)
164
+ SemanticLogger::Logger.logger.error("Bad HTTP response code: #{response.code}, #{response.body}")
165
+ false
166
+ end
167
+ end
168
+
169
+ end
@@ -1,98 +1,115 @@
1
1
  require 'socket'
2
- require 'mongo'
2
+ begin
3
+ require 'mongo'
4
+ rescue LoadError
5
+ raise 'Gem mongo is required for logging to MongoDB. Please add the gem "mongo" to your Gemfile.'
6
+ end
7
+
3
8
  module SemanticLogger
4
9
  module Appender
5
10
  # The Mongo Appender for the SemanticLogger
6
11
  #
7
12
  # Mongo Document Schema:
8
- # _id : ObjectId("4d9cbcbf7abb3abdaf9679cd"),
9
- # time : ISODate("2011-04-06T19:19:27.006Z"),
10
- # host_name : "Name of the host on which this log entry originated",
11
- # application: 'Name of application or service logging the data - clarity_base, nginx, tomcat',
12
- # pid : process id
13
- # thread_name: "name or id of thread",
14
- # name : "com.clarity.MyClass",
15
- # level : 'trace|debug|info|warn|error|fatal'
13
+ # _id: ObjectId("4d9cbcbf7abb3abdaf9679cd"),
14
+ # time: ISODate("2011-04-06T19:19:27.006Z"),
15
+ # host: 'Name of the host on which this log entry originated',
16
+ # application 'Name of application or service logging the data - clarity_base, nginx, tomcat',
17
+ # pid: process id
18
+ # thread: "name or id of thread",
19
+ # name: "com.clarity.MyClass",
20
+ # level: 'trace|debug|info|warn|error|fatal'
16
21
  # level_index: 0|1|2|3|4|5
17
- # message : "Message supplied to the logging call",
18
- # duration : ms, # Set by Logger#benchmark
19
- # tags : "Some tracking id" | ["id1", "id2"]
20
- # payload : {
21
- # Optional. Any user supplied data, including any thread specific context variables
22
- # values supplied on a per log entry will override any thread context values
23
- # }
24
- # # When an exception is supplied as the first or second parameter
25
- # # If supplied as the first parameter, message='exception name'
22
+ # message: "Message supplied to the logging call",
23
+ # duration: 'human readable duration',
24
+ # duration_ms: ms,
25
+ # tags: ["id1", "id2"]
26
26
  # exception: {
27
27
  # name: 'MyException',
28
28
  # message: 'Invalid value',
29
29
  # stack_trace: []
30
30
  # }
31
31
  # # When a backtrace is captured
32
- # file_name: 'my_class.rb'
32
+ # file_name: 'my_class.rb'
33
33
  # line_number: 42
34
34
  #
35
+ # Example:
36
+ # require 'semantic_logger'
37
+ #
38
+ # client = Mongo::MongoClient.new
39
+ # database = client['test']
40
+ #
41
+ # appender = SemanticLogger::Appender::MongoDB.new(
42
+ # db: database,
43
+ # collection_size: 1024**3 # 1.gigabyte
44
+ # )
45
+ # SemanticLogger.add_appender(appender)
46
+ #
47
+ # logger = SemanticLogger['Example']
48
+ #
49
+ # # Log some messages
50
+ # logger.info 'This message is written to mongo as a document'
35
51
  class MongoDB < SemanticLogger::Appender::Base
36
52
  attr_reader :db, :collection_name, :collection
37
- attr_accessor :host_name, :write_concern, :application
53
+ attr_accessor :host, :write_concern, :application
38
54
 
39
55
  # Create a MongoDB Appender instance
40
56
  #
41
- # SemanticLogger::Appender::MongoDB.new(db: Mongo::Connection.new['database'])
42
- #
43
57
  # Parameters:
44
- # :db [Mongo::Database]
45
- # The MongoDB database connection to use, not the database name
58
+ # db: [Mongo::Database]
59
+ # The MongoDB database connection to use, not the database name
46
60
  #
47
- # :collection_name [String]
48
- # Name of the collection to store log data in
49
- # Default: semantic_logger
61
+ # collection_name: [String]
62
+ # Name of the collection to store log data in
63
+ # Default: semantic_logger
50
64
  #
51
- # :host_name [String]
52
- # host_name to include in the document logged to Mongo
53
- # Default: first part of host name extracted from Socket
65
+ # write_concern: [Integer]
66
+ # Write concern to use
67
+ # see: http://docs.mongodb.org/manual/reference/write-concern/
68
+ # Default: 0
54
69
  #
55
- # :write_concern [Integer]
56
- # Write concern to use
57
- # see: http://docs.mongodb.org/manual/reference/write-concern/
58
- # Default: 1
70
+ # host: [String]
71
+ # host name to include in the document logged to Mongo
72
+ # Default: SemanticLogger.host_name
59
73
  #
60
- # :application [String]
61
- # Name of the application to include in the document written to mongo
62
- # Default: nil (None)
74
+ # application: [String]
75
+ # Name of the application to include in the document written to mongo
76
+ # Default: nil (None)
63
77
  #
64
- # :collection_size [Integer]
65
- # The size of the MongoDB capped collection to create in bytes
66
- # Default: 1 GB
78
+ # collection_size: [Integer]
79
+ # The size of the MongoDB capped collection to create in bytes
80
+ # Default: 1 GB
81
+ # Examples:
82
+ # Prod: 25GB (.5GB per day across 4 servers over 10 days)
83
+ # Dev: .5GB
84
+ # Test: File
85
+ # Release: 4GB
67
86
  #
68
- # Some examples
69
- # Prod: 25GB (.5GB per day across 4 servers over 10 days)
70
- # Dev: .5GB
71
- # Test: File
72
- # Release: 4GB
87
+ # collection_max: [Integer]
88
+ # Maximum number of log entries that the capped collection will hold.
73
89
  #
74
- # :collection_max [Integer]
75
- # Maximum number of log entries that the capped collection will hold
90
+ # level: [:trace | :debug | :info | :warn | :error | :fatal]
91
+ # Override the log level for this appender.
92
+ # Default: SemanticLogger.default_level
76
93
  #
77
- # :level [Symbol]
78
- # Only allow log entries of this level or higher to be written to MongoDB
79
- #
80
- # :filter [Regexp|Proc]
81
- # RegExp: Only include log messages where the class name matches the supplied
82
- # regular expression. All other messages will be ignored
83
- # Proc: Only include log messages where the supplied Proc returns true
84
- # The Proc must return true or false
85
- def initialize(params={}, &block)
86
- @db = params[:db] || raise('Missing mandatory parameter :db')
87
- @collection_name = params[:collection_name] || 'semantic_logger'
88
- @host_name = params[:host_name] || Socket.gethostname.split('.').first
89
- @write_concern = params[:write_concern] || 1
90
- @application = params[:application]
91
- filter = params[:filter]
94
+ # filter: [Regexp|Proc]
95
+ # RegExp: Only include log messages where the class name matches the supplied.
96
+ # regular expression. All other messages will be ignored.
97
+ # Proc: Only include log messages where the supplied Proc returns true
98
+ # The Proc must return true or false.
99
+ def initialize(options = {}, &block)
100
+ options = options.dup
101
+ level = options.delete(:level)
102
+ filter = options.delete(:filter)
103
+ @db = options.delete(:db) || raise('Missing mandatory parameter :db')
104
+ @collection_name = options.delete(:collection_name) || 'semantic_logger'
105
+ @host = options.delete(:host) || options.delete(:host_name) || SemanticLogger.host
106
+ @write_concern = options.delete(:write_concern) || 0
107
+ @application = options.delete(:application) || SemanticLogger.application
92
108
 
93
109
  # Create a collection that will hold the lesser of 1GB space or 10K documents
94
- @collection_size = params[:collection_size] || 1024**3
95
- @collection_max = params[:collection_max]
110
+ @collection_size = options.delete(:collection_size) || 1024**3
111
+ @collection_max = options.delete(:collection_max)
112
+ raise(ArgumentError, "Unknown options: #{options.inspect}") if options.size > 0
96
113
 
97
114
  reopen
98
115
 
@@ -100,7 +117,7 @@ module SemanticLogger
100
117
  create_indexes
101
118
 
102
119
  # Set the log level and formatter
103
- super(params[:level], filter, &block)
120
+ super(level, filter, &block)
104
121
  end
105
122
 
106
123
  # After forking an active process call #reopen to re-open
@@ -109,7 +126,8 @@ module SemanticLogger
109
126
  @collection = db[@collection_name]
110
127
  end
111
128
 
112
- # Create the required capped collection
129
+ # Create the required capped collection.
130
+ #
113
131
  # Features of capped collection:
114
132
  # * No indexes by default (not even on _id)
115
133
  # * Documents cannot be deleted,
@@ -117,7 +135,7 @@ module SemanticLogger
117
135
  # * Documents are always stored in insertion order
118
136
  # * A find will always return the documents in their insertion order
119
137
  #
120
- # Creates an index based on tags to support faster lookups
138
+ # Creates an index based on tags to support faster searches.
121
139
  def create_indexes
122
140
  options = {capped: true, size: @collection_size}
123
141
  options[:max] = @collection_max if @collection_max
@@ -144,59 +162,20 @@ module SemanticLogger
144
162
  # Replace this formatter by supplying a Block to the initializer
145
163
  def default_formatter
146
164
  Proc.new do |log|
147
- document = {
148
- time: log.time,
149
- host_name: host_name,
150
- pid: $$,
151
- thread_name: log.thread_name,
152
- name: log.name,
153
- level: log.level,
154
- level_index: log.level_index,
155
- }
156
- document[:application] = application if application
157
- document[:message] = log.cleansed_message if log.message
158
- document[:duration] = log.duration if log.duration
159
- document[:tags] = log.tags if log.tags && (log.tags.size > 0)
160
- document[:payload] = log.payload if log.payload
161
- if log.exception
162
- root = document
163
- log.each_exception do |exception, i|
164
- name = i == 0 ? :exception : :cause
165
- root[name] = {
166
- name: exception.class.name,
167
- message: exception.message,
168
- stack_trace: exception.backtrace
169
- }
170
- root = root[name]
171
- end
172
- end
173
-
174
- file, line = log.file_name_and_line
175
- if file
176
- document[:file_name] = file
177
- document[:line_number] = line.to_i
178
- end
179
- document
165
+ h = log.to_h
166
+ h[:host] = host
167
+ h[:application] = application
168
+ h
180
169
  end
181
170
  end
182
171
 
183
- # Default host_name to use if none is supplied to the appenders initializer
184
- def self.host_name
185
- @@host_name ||= Socket.gethostname.split('.').first
186
- end
187
-
188
- # Override the default host_name
189
- def self.host_name=(host_name)
190
- @@host_name = host_name
191
- end
192
-
193
172
  # Log the message to MongoDB
194
173
  def log(log)
195
174
  # Ensure minimum log level is met, and check filter
196
175
  return false if (level_index > (log.level_index || 0)) || !include_message?(log)
197
176
 
198
177
  # Insert log entry into Mongo
199
- collection.insert(formatter.call(log), w: @write_concern)
178
+ collection.insert(formatter.call(log, self), w: @write_concern)
200
179
  true
201
180
  end
202
181
 
@@ -1,81 +1,47 @@
1
- =begin
2
- New Relic appender for SemanticLogger
3
-
4
- The :error and :fatal log entries will show up under Applications > "Application Name" > Events > Errors in New Relic
5
-
6
- Note: Payload information is not filtered, so take care not to push any sensitive information when logging with tags or a payload.
7
-
8
-
9
- Example 1
10
-
11
- Adding the New Relic appender will send :error and :fatal log entries to New Relic as error events.
12
-
13
- For a Rails application already configured to use SemanticLogger and New Relic, create a file called <Rails Root>/config/initializers/newrelic_appender.rb with the following contents and restart the application:
14
-
15
- # Send :error and :fatal log messages to New Relic
16
- SemanticLogger.add_appender(SemanticLogger::Appender::NewRelic.new)
17
- Rails.logger.info 'SemanticLogger New Relic Appender added.'
18
-
19
-
20
- Example 2
21
-
22
- For a non-Rails application, send :info and more severe log entries to a file called application.log and also send :error and :fatal log entries to New Relic.
23
-
24
- # ./newrelic.yml needs to be set up -- see https://docs.newrelic.com/docs/ruby/ruby-agent-installation for more information.
25
-
26
- require 'semantic_logger'
27
- require 'newrelic_rpm'
28
-
29
- # New Relic setup
30
- NewRelic::Agent.manual_start
31
-
32
- # SemanticLogger setup
33
- SemanticLogger.default_level = :info
34
- SemanticLogger.add_appender('application.log')
35
- SemanticLogger.add_appender(SemanticLogger::Appender::NewRelic.new)
36
- logger = SemanticLogger['Example']
37
-
38
- # Log some messages
39
- logger.info 'This is only written to application.log'
40
- logger.error 'This is written to application.log and will also be sent to New Relic as an error event'
41
-
42
- # The appender will send tags, payloads and benchmark duration to New Relic
43
- logger.tagged('test') do
44
- logger.with_payload( {key1: 123, key2: 'abc'} ) do
45
- logger.benchmark_error(@message) do
46
- sleep 0.001
47
- end
48
- end
1
+ begin
2
+ require 'newrelic_rpm'
3
+ rescue LoadError
4
+ raise 'Gem newrelic_rpm is required for logging to New Relic. Please add the gem "newrelic_rpm" to your Gemfile.'
49
5
  end
50
6
 
51
- # New Relic does not seem to receive any errors if the application exits too soon after sending error alerts.
52
- sleep 10
53
-
54
- # New Relic shutdown - should send any queued data before exiting
55
- ::NewRelic::Agent.shutdown
56
- =end
57
-
58
-
59
- require 'newrelic_rpm'
60
-
7
+ # Send log messages to NewRelic
8
+ #
9
+ # The :error and :fatal log entries will show up under
10
+ # "Applications" > "Application Name" > "Events" > "Errors" in New Relic.
11
+ #
12
+ # Example:
13
+ # SemanticLogger.add_appender(SemanticLogger::Appender::NewRelic.new)
14
+ #
61
15
  class SemanticLogger::Appender::NewRelic < SemanticLogger::Appender::Base
62
- # Allow the level for this appender to be overwritten
63
- # Default: :error
64
- # Note: Not recommended to set the log level to :info, :debug, or :trace as that would flood NewRelic with Error notices
65
- def initialize(level=:error, &block)
66
- super(level, &block)
16
+ # Create Appender
17
+ #
18
+ # Parameters
19
+ # level: [:trace | :debug | :info | :warn | :error | :fatal]
20
+ # Override the log level for this appender.
21
+ # Default: :error
22
+ #
23
+ # filter: [Regexp|Proc]
24
+ # RegExp: Only include log messages where the class name matches the supplied.
25
+ # regular expression. All other messages will be ignored.
26
+ # Proc: Only include log messages where the supplied Proc returns true
27
+ # The Proc must return true or false.
28
+ def initialize(options = {}, &block)
29
+ # Backward compatibility
30
+ options = {level: options} unless options.is_a?(Hash)
31
+ @options = options.dup
32
+ level = @options.delete(:level) || :error
33
+ filter = @options.delete(:filter)
34
+
35
+ super(level, filter, &block)
67
36
  end
68
37
 
69
38
  # Returns [Hash] of parameters to send to New Relic.
70
39
  def default_formatter
71
- Proc.new do |log|
72
- custom_params = {thread_name: log.thread_name}
73
- custom_params[:duration] = "#{log.duration} ms" if log.duration
74
- custom_params[:payload] = log.payload if log.payload
75
- custom_params[:tags] = log.tags if log.tags && (log.tags.size > 0)
76
- custom_params[:message] = log.message if log.exception
77
-
78
- {metric: log.metric, custom_params: custom_params}
40
+ Proc.new do |log, logger|
41
+ h = log.to_h
42
+ h.delete(:time)
43
+ h.delete(:exception)
44
+ {metric: log.metric, custom_params: h}
79
45
  end
80
46
  end
81
47
 
@@ -96,7 +62,7 @@ class SemanticLogger::Appender::NewRelic < SemanticLogger::Appender::Base
96
62
  # For more documentation on the NewRelic::Agent.notice_error method see:
97
63
  # http://rubydoc.info/github/newrelic/rpm/NewRelic/Agent#notice_error-instance_method
98
64
  # and https://docs.newrelic.com/docs/ruby/ruby-agent-api
99
- NewRelic::Agent.notice_error(exception, formatter.call(log))
65
+ NewRelic::Agent.notice_error(exception, formatter.call(log, self))
100
66
  true
101
67
  end
102
68