semantic_logger 2.21.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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