chook 1.0.1.b1 → 1.1.5b1

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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +56 -0
  3. data/README.md +397 -145
  4. data/bin/chook-server +31 -1
  5. data/data/chook.conf.example +183 -0
  6. data/data/com.pixar.chook-server.plist +20 -0
  7. data/data/sample_handlers/RestAPIOperation.rb +11 -11
  8. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
  9. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
  10. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
  11. data/lib/chook/configuration.rb +27 -8
  12. data/lib/chook/event.rb +6 -1
  13. data/lib/chook/event/handled_event.rb +36 -9
  14. data/lib/chook/event/handled_event/handlers.rb +252 -99
  15. data/lib/chook/event/handled_event_logger.rb +86 -0
  16. data/lib/chook/event_handling.rb +1 -0
  17. data/lib/chook/foundation.rb +3 -0
  18. data/lib/chook/procs.rb +17 -1
  19. data/lib/chook/server.rb +73 -72
  20. data/lib/chook/server/auth.rb +164 -0
  21. data/lib/chook/server/log.rb +215 -0
  22. data/lib/chook/server/public/css/chook.css +133 -0
  23. data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
  24. data/lib/chook/server/public/js/chook.js +126 -0
  25. data/lib/chook/server/public/js/logstream.js +101 -0
  26. data/lib/chook/server/routes.rb +28 -0
  27. data/lib/chook/server/routes/handle_by_name.rb +65 -0
  28. data/lib/chook/server/routes/handle_webhook_event.rb +27 -3
  29. data/lib/chook/server/routes/handlers.rb +52 -0
  30. data/lib/chook/server/routes/home.rb +48 -1
  31. data/lib/chook/server/routes/log.rb +105 -0
  32. data/lib/chook/server/routes/login_logout.rb +48 -0
  33. data/lib/chook/server/views/admin.haml +11 -0
  34. data/lib/chook/server/views/bak.haml +48 -0
  35. data/lib/chook/server/views/config.haml +15 -0
  36. data/lib/chook/server/views/handlers.haml +63 -0
  37. data/lib/chook/server/views/layout.haml +64 -0
  38. data/lib/chook/server/views/logstream.haml +33 -0
  39. data/lib/chook/server/views/sketch_admin +44 -0
  40. data/lib/chook/subject.rb +13 -2
  41. data/lib/chook/subject/dep_device.rb +81 -0
  42. data/lib/chook/subject/policy_finished.rb +43 -0
  43. data/lib/chook/subject/smart_group.rb +6 -0
  44. data/lib/chook/subject/test_subject.rb +2 -2
  45. data/lib/chook/version.rb +1 -1
  46. metadata +78 -17
@@ -37,4 +37,5 @@ Chook::HandledSubject.generate_classes
37
37
  # events
38
38
  require 'chook/event'
39
39
  require 'chook/event/handled_event'
40
+ require 'chook/event/handled_event_logger'
40
41
  Chook::HandledEvent.generate_classes
@@ -27,6 +27,9 @@
27
27
  require 'json'
28
28
  require 'open-uri'
29
29
  require 'pathname'
30
+ require 'logger'
31
+ require 'English'
32
+ require 'securerandom'
30
33
 
31
34
  require 'chook/version'
32
35
  require 'chook/procs' # must load before configuration
@@ -30,12 +30,28 @@ module Chook
30
30
  module Procs
31
31
 
32
32
  TRUE_RE = /^\s*(true|yes)\s*$/i
33
+
33
34
  JSS_EPOCH_TO_TIME = proc { |val| Time.strptime val.to_s[0..-4], '%s' }
35
+
34
36
  STRING_TO_BOOLEAN = proc { |val| val =~ TRUE_RE ? true : false }
37
+
35
38
  STRING_TO_PATHNAME = proc { |val| Pathname.new val }
39
+
40
+ STRING_TO_LOG_LEVEL = proc do |level|
41
+ if (0..5).cover? level
42
+ level
43
+ else
44
+ lvl = Chook::Server::Log::LOG_LEVELS[level.to_sym]
45
+ lvl ? lvl : Logger::UNKNOWN
46
+ end # if..else
47
+ end
48
+
36
49
  MOBILE_USERID = proc { |_device| '-1' }
50
+
37
51
  PRODUCT = proc { |_device| nil }
38
- ALWAYS_TRUE = proc { |_boolean| True }
52
+
53
+ ALWAYS_TRUE = proc { |_boolean| true }
54
+
39
55
  COMPUTER_USERID = proc do |comp|
40
56
  id = '-1' unless comp.groups_accounts[:local_accounts].find { |acct| acct[:name] == comp.username }
41
57
  id.is_a?(Hash) ? id[:uid] : '-1'
@@ -22,9 +22,15 @@
22
22
  ### language governing permissions and limitations under the Apache License.
23
23
  ###
24
24
  ###
25
- require 'chook/event_handling'
25
+
26
26
  require 'sinatra/base'
27
+ require 'sinatra/custom_logger'
28
+ require 'haml'
27
29
  require 'openssl'
30
+ require 'chook/event_handling'
31
+ require 'chook/server/log'
32
+ require 'chook/server/auth'
33
+ require 'chook/server/routes'
28
34
 
29
35
  module Chook
30
36
 
@@ -32,90 +38,85 @@ module Chook
32
38
  # the engine of your choice.
33
39
  class Server < Sinatra::Base
34
40
 
35
- DEFAULT_SERVER_ENGINE = :webrick
36
- DEFAULT_PORT = 8000
41
+ DEFAULT_PORT = 80
42
+ DEFAULT_SSL_PORT = 443
43
+ DEFAULT_CONCURRENCY = true
44
+ DEFAULT_SESSION_EXPIRE = 24 * 60 * 60 # one day
37
45
 
38
- @server_engine = Chook::CONFIG.server_engine || DEFAULT_SERVER_ENGINE
39
- require @server_engine.to_s
40
- @server_port = Chook::CONFIG.server_port || DEFAULT_PORT
46
+ # set defaults in config
47
+ Chook.config.port ||= Chook.config.use_ssl ? DEFAULT_SSL_PORT : DEFAULT_PORT
48
+ Chook.config.admin_session_expires ||= DEFAULT_SESSION_EXPIRE
41
49
 
42
- def self.run!
43
- # trap HUPs to reload handlers
44
- Signal.trap('HUP') do
45
- Chook::HandledEvent::Handlers.load_handlers reload: true
46
- end
47
- Chook::HandledEvent::Handlers.load_handlers
48
- chook_configure
49
- case @server_engine.to_sym
50
- when :webrick
50
+ # can't use ||= here cuz nil and false have different meanings
51
+ Chook.config.concurrency = DEFAULT_CONCURRENCY if Chook.config.concurrency.nil?
52
+
53
+ # Run the server
54
+ ###################################
55
+ def self.run!(log_level: nil)
56
+ prep_to_run
57
+
58
+ if Chook.config.use_ssl
59
+ super do |server|
60
+ server.ssl = true
61
+ server.ssl_options = {
62
+ cert_chain_file: Chook.config.ssl_cert_path.to_s,
63
+ private_key_file: Chook.config.ssl_private_key_path.to_s,
64
+ verify_peer: false
65
+ }
66
+ end # super do
67
+
68
+ else # no ssl
51
69
  super
52
- when :thin
53
- if Chook::CONFIG.use_ssl
54
- super do |server|
55
- server.ssl = true
56
- server.ssl_options = {
57
- cert_chain_file: Chook::CONFIG.ssl_cert_path.to_s,
58
- private_key_file: Chook::CONFIG.ssl_private_key_path.to_s,
59
- verify_peer: false
60
- }
61
- end # super do
62
- else
63
- super
64
- end # if use ssl
65
- end # case
70
+ end # if use ssl
66
71
  end # self.run
67
72
 
68
- # Sinatra Settings
69
- def self.chook_configure
73
+ def self.prep_to_run
74
+ @start_time = Time.now
75
+ log_level ||= Chook.config.log_level
76
+ @log_level = Chook::Procs::STRING_TO_LOG_LEVEL.call log_level
77
+
70
78
  configure do
71
- set :environment, :production
72
- enable :logging, :lock
79
+ set :logger, Log.startup(@log_level)
80
+ set :server, :thin
73
81
  set :bind, '0.0.0.0'
74
- set :server, @server_engine
75
- set :port, @server_port
76
-
77
- if Chook::CONFIG.use_ssl
78
- case @server_engine.to_sym
79
- when :webrick
80
- require 'webrick/https'
81
- key = Chook::CONFIG.ssl_private_key_path.read
82
- cert = Chook::CONFIG.ssl_cert_path.read
83
- cert_name = Chook::CONFIG.ssl_cert_name
84
- set :SSLEnable, true
85
- set :SSLVerifyClient, OpenSSL::SSL::VERIFY_NONE
86
- set :SSLPrivateKey, OpenSSL::PKey::RSA.new(key, ssl_key_password)
87
- set :SSLCertificate, OpenSSL::X509::Certificate.new(cert)
88
- set :SSLCertName, [['CN', cert_name]]
89
- when :thin
90
- true
91
- end # case
92
- end # if ssl
82
+ set :port, Chook.config.port
83
+ set :show_exceptions, :after_handler if development?
84
+ set :root, "#{File.dirname __FILE__}/server"
85
+ enable :static
86
+ enable :sessions
87
+ set :sessions, expire_after: Chook.config.admin_session_expires if Chook.config.admin_user
88
+ if Chook.config.concurrency
89
+ set :threaded, true
90
+ else
91
+ enable :lock
92
+ end
93
93
  end # configure
94
- end # chook_configure
95
94
 
96
- def self.ssl_key_password
97
- path = Chook::CONFIG.ssl_private_key_pw_path
98
- raise 'No config setting for "ssl_private_key_pw_path"' unless path
99
- file = Pathname.new path
95
+ Chook::HandledEvent::Handlers.load_handlers
96
+ end # prep to run
100
97
 
101
- # if the path ends with a pipe, its a command that will
102
- # return the desired password, so remove the pipe,
103
- # execute it, and return stdout from it.
104
- if path.end_with? '|'
105
- raise 'ssl_private_key_pw_path: #{path} is not an executable file.' unless file.executable?
106
- return `#{path.chomp '|'}`.chomp
107
- end
98
+ def self.starttime
99
+ @start_time
100
+ end
108
101
 
109
- raise 'ssl_private_key_pw_path: #{path} is not a readable file.' unless file.readable?
110
- stat = file.stat
111
- raise "Password file for '#{pw}' has insecure permissions, must be 0600." unless ('%o' % stat.mode).end_with? '0600'
102
+ def self.uptime
103
+ @start_time ? "#{humanize_secs(Time.now - @start_time)} ago" : 'Not Running'
104
+ end
112
105
 
113
- # chomping an empty string removes all trailing \n's and \r\n's
114
- file.read.chomp('')
115
- end # ssl_key_password
106
+ # Very handy!
107
+ # lifted from
108
+ # http://stackoverflow.com/questions/4136248/how-to-generate-a-human-readable-time-range-using-ruby-on-rails
109
+ #
110
+ def self.humanize_secs(secs)
111
+ [[60, :second], [60, :minute], [24, :hour], [7, :day], [52.179, :week], [1_000_000, :year]].map do |count, name|
112
+ next unless secs.positive?
113
+
114
+ secs, n = secs.divmod(count)
115
+ n = n.to_i
116
+ "#{n} #{n == 1 ? name : (name.to_s + 's')}"
117
+ end.compact.reverse.join(' ')
118
+ end
116
119
 
117
120
  end # class server
118
121
 
119
122
  end # module
120
-
121
- require 'chook/server/routes'
@@ -0,0 +1,164 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ module Chook
27
+
28
+ # the server
29
+ class Server < Sinatra::Base
30
+
31
+ # helper module for authentication
32
+ module Auth
33
+
34
+ USE_JAMF_ADMIN_USER = 'use_jamf'.freeze
35
+
36
+ def protect_via_basic_auth!
37
+ # don't protect if user isn't defined
38
+ return unless Chook.config.webhooks_user
39
+ return if webhook_user_authorized?
40
+ headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
41
+ halt 401, "Not authorized\n"
42
+ end
43
+
44
+ def webhook_user_authorized?
45
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
46
+
47
+ # gotta have basic auth presented to us
48
+ unless @auth.provided? && @auth.basic? && @auth.credentials
49
+ Chook.logger.debug "No basic auth provided on protected route: #{request.path_info} from: #{request.ip}"
50
+ return false
51
+ end
52
+
53
+ authenticate_webhooks_user @auth.credentials
54
+ end # authorized?
55
+
56
+ # webhook user auth always comes from config
57
+ def authenticate_webhooks_user(creds)
58
+ if creds.first == Chook.config.webhooks_user && creds.last == Chook::Server.webhooks_user_pw
59
+ Chook.logger.debug "Got HTTP Basic auth for webhooks user: #{Chook.config.webhooks_user}@#{request.ip}"
60
+ true
61
+ else
62
+ Chook.logger.error "FAILED auth for webhooks user: #{Chook.config.webhooks_user}@#{request.ip}"
63
+ false
64
+ end
65
+ end # authenticate_webhooks_user
66
+
67
+ # admin user auth might come from config, might come from Jamf Pro
68
+ def authenticate_admin(user, pw)
69
+ return authenticate_jamf_admin(user, pw) if Chook.config.admin_user == USE_JAMF_ADMIN_USER
70
+ authenticate_admin_user(user, pw)
71
+ end
72
+
73
+ # admin auth from config
74
+ def authenticate_admin_user(user, pw)
75
+ if user == Chook.config.admin_user && pw == Chook::Server.admin_user_pw
76
+ Chook.logger.debug "Got auth for admin user: #{user}@#{request.ip}"
77
+ session[:authed_admin] = user
78
+ true
79
+ else
80
+ Chook.logger.warn "FAILED auth for admin user: #{user}@#{request.ip}"
81
+ session[:authed_admin] = nil
82
+ false
83
+ end
84
+ end
85
+
86
+ # admin auth from jamf pro
87
+ def authenticate_jamf_admin(user, pw)
88
+ require 'ruby-jss'
89
+ JSS::APIConnection.new(
90
+ user: user,
91
+ pw: pw,
92
+ server: Chook.config.jamf_server,
93
+ port: Chook.config.jamf_port,
94
+ use_ssl: Chook.config.jamf_use_ssl,
95
+ verify_cert: Chook.config.jamf_verify_cert
96
+ )
97
+ Chook.logger.debug "Jamf Admin login for: #{user}@#{request.ip}"
98
+
99
+ session[:authed_admin] = user
100
+ true
101
+ rescue JSS::AuthenticationError
102
+ Chook.logger.warn "Jamf Admin login FAILED for: #{user}@#{request.ip}"
103
+ session[:authed_admin] = nil
104
+ false
105
+ end # authenticate_jamf_admin
106
+
107
+ end # module auth
108
+
109
+ helpers Chook::Server::Auth
110
+
111
+ # Learn the webhook or admin passwords from config.
112
+ # so we can authenticate them from the browser and the JSS
113
+ #
114
+ # This is at the Server level, since we only need read it
115
+ # once per server startup, so we store it in a server
116
+ # instance var.
117
+ ###################################
118
+ def self.webhooks_user_pw
119
+ @webhooks_user_pw ||= pw_from_conf Chook.config.webhooks_user_pw
120
+ end # self.webhooks_user_pw
121
+
122
+ def self.admin_user_pw
123
+ @admin_user_pw ||= pw_from_conf Chook.config.admin_pw
124
+ end
125
+
126
+ def self.pw_from_conf(setting)
127
+ return '' unless setting
128
+
129
+ # if the path ends with a pipe, its a command that will
130
+ # return the desired password, so remove the pipe,
131
+ # execute it, and return stdout from it.
132
+ return pw_from_command(setting) if setting.end_with? '|'
133
+
134
+ # otherwise its a file path, and read the pw from the contents
135
+ pw_from_file(setting)
136
+ end # def pw_from_conf(setting)
137
+
138
+ def self.pw_from_command(cmd)
139
+ cmd = cmd.chomp '|'
140
+ output = `#{cmd} 2>&1`.chomp
141
+ raise "Can't get password from #{setting}: #{output}" unless $CHILD_STATUS.exitstatus.zero?
142
+ output
143
+ end
144
+
145
+ def self.pw_from_file(file)
146
+ file = Pathname.new file
147
+ return nil unless file.file?
148
+ stat = file.stat
149
+ mode = format('%o', stat.mode)
150
+ raise "Password file #{setting} has insecure mode, must be 0600." unless mode.end_with?('0600')
151
+ raise "Password file #{setting} has insecure owner, must be owned by UID #{Process.euid}." unless stat.owned?
152
+ # chomping an empty string removes all trailing \n's and \r\n's
153
+ file.read.chomp('')
154
+ end
155
+
156
+
157
+ end # server
158
+
159
+ end # Chook
160
+
161
+ require 'chook/server/routes/home'
162
+ require 'chook/server/routes/handle_webhook_event'
163
+ require 'chook/server/routes/handlers'
164
+ require 'chook/server/routes/log'
@@ -0,0 +1,215 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ # the main module
27
+ module Chook
28
+
29
+ # the server app
30
+ class Server < Sinatra::Base
31
+
32
+ # the CustomLogger helper allows us to
33
+ # use our own Logger instance as the
34
+ # Sinatra `logger` available in routes vis `set :logger...`
35
+ helpers Sinatra::CustomLogger
36
+
37
+ # This module defines our custom Logger instance from the config settings
38
+ # and makes it available in the .logger module method,
39
+ # which is used anywhere outside of a route
40
+ # (inside of a route, the #logger method is locally available)
41
+ #
42
+ # General access to the logger:
43
+ # from anywhere in Chook, as long as a server is running, the Logger
44
+ # instance is available at Chook.logger or Chook::Server::Log.logger
45
+ #
46
+ #
47
+ # Logging from inside a route:
48
+ # inside a Sinatra route, the local `logger` method returnes the
49
+ # Logger instance.
50
+ #
51
+ # Logging from an internal handler:
52
+ # In an internal WebHook handler, starting with `Chook.event_handler do |event|`
53
+ # the logger should be accesses via the event's own wrapper: event.logger
54
+ # Each message will be prepended with the event'd ID
55
+ #
56
+ # Logging from an external handler:
57
+ # from an external handler you can POST a JSON formatted log entry to
58
+ # http(s)://chookserver/log. The body must be a JSON object with 2 keys:
59
+ # 'level' and 'message', with non-empty strings.
60
+ # The level must be one of: fatal, error, warn, info, or debug. Any other
61
+ # value as the level will always be logged regardless of current logging
62
+ # level. NOTE: if your server requires authentication, you must provide it
63
+ # when using this route.
64
+ #
65
+ # Here's an example with curl, split to multi-line for clarity:
66
+ #
67
+ # curl -H "Content-Type: application/json" \
68
+ # -X POST \
69
+ # --data '{"level":"debug", "message":"It Worked"}' \
70
+ # https://user:passwd@chookserver.myorg.org:443/log
71
+ #
72
+ module Log
73
+
74
+ # Using an instance of this as the Logger target sends logfile writes
75
+ # to all registered streams as well as the file
76
+ class LogFileWithStream < File
77
+
78
+ # ServerSent Events data lines always start with this
79
+ LOGSTREAM_DATA_PFX = 'data:'.freeze
80
+
81
+ def write(str)
82
+ super # writes out to the file
83
+ flush
84
+ # send to any active streams
85
+ Chook::Server::Log.log_streams.keys.each do |active_stream|
86
+ # ignore streams closed at the client end,
87
+ # they get removed when a new stream starts
88
+ # see the route: get '/subscribe_to_log_stream'
89
+ next if active_stream.closed?
90
+
91
+ # send new data to the stream
92
+ active_stream << "#{LOGSTREAM_DATA_PFX}#{str}\n\n"
93
+ end
94
+ end
95
+ end # class
96
+
97
+ # mapping of integer levels to symbols
98
+ LOG_LEVELS = {
99
+ fatal: Logger::FATAL,
100
+ error: Logger::ERROR,
101
+ warn: Logger::WARN,
102
+ info: Logger::INFO,
103
+ debug: Logger::DEBUG
104
+ }.freeze
105
+
106
+ # log Streaming
107
+ # ServerSent Events data lines always start with this
108
+ LOGSTREAM_DATA_PFX = 'data:'.freeze
109
+
110
+ # Send this to the clients at least every LOGSTREAM_KEEPALIVE_MAX secs
111
+ # even if there's no data for the stream
112
+ LOGSTREAM_KEEPALIVE_MSG = "#{LOGSTREAM_DATA_PFX} I'm Here!\n\n".freeze
113
+ LOGSTREAM_KEEPALIVE_MAX = 10
114
+
115
+ # the clients will recognize M3_LOG_STREAM_CLOSED and stop trying
116
+ # to connect via ssh.
117
+ LOGSTREAM_CLOSED_PFX = "#{LOGSTREAM_DATA_PFX} M3_LOG_STREAM_CLOSED:".freeze
118
+
119
+ DEFAULT_FILE = Pathname.new '/var/log/chook-server.log'
120
+ DEFAULT_MAX_MEGS = 10
121
+ DEFAULT_TO_KEEP = 10
122
+ DEFAULT_LEVEL = Logger::INFO
123
+
124
+ # set defaults in config
125
+ Chook.config.log_file ||= DEFAULT_FILE
126
+ Chook.config.logs_to_keep ||= DEFAULT_TO_KEEP
127
+ Chook.config.log_max_megs ||= DEFAULT_MAX_MEGS
128
+ Chook.config.log_level ||= DEFAULT_LEVEL
129
+
130
+ # Create the logger,
131
+ # make the first log entry for this run,
132
+ # and return it so it can be used by the server
133
+ # when it does `set :logger, Log.startup(@log_level)`
134
+ def self.startup(level = Chook.config.log_level)
135
+ # create the logger using a LogFileWithStream instance
136
+ @logger =
137
+ if Chook.config.logs_to_keep && Chook.config.logs_to_keep > 0
138
+ Logger.new(
139
+ LogFileWithStream.new(Chook.config.log_file, 'a'),
140
+ Chook.config.logs_to_keep,
141
+ (Chook.config.log_max_megs * 1024 * 1024)
142
+ )
143
+ else
144
+ Logger.new(LogFileWithStream.new(Chook.config.log_file, 'a'))
145
+ end
146
+
147
+ # date and line format
148
+ @logger.datetime_format = '%Y-%m-%d %H:%M:%S'
149
+
150
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
151
+ "#{datetime}: [#{severity}] #{msg}\n"
152
+ end
153
+
154
+ # level
155
+ level &&= Chook::Procs::STRING_TO_LOG_LEVEL.call level
156
+ level ||= Chook.config.log_level
157
+ level ||= DEFAULT_LEVEL
158
+ @logger.level = level
159
+
160
+ # first startup entry
161
+ @logger.unknown "Chook Server v#{Chook::VERSION} starting up. PID: #{$PROCESS_ID}, Port: #{Chook.config.port}, SSL: #{Chook.config.use_ssl}"
162
+
163
+ # if debug, log our config
164
+ if level == Logger::DEBUG
165
+ @logger.debug 'Config: '
166
+ Chook::Configuration::CONF_KEYS.keys.each do |key|
167
+ @logger.debug " Chook.config.#{key} = #{Chook.config.send key}"
168
+ end
169
+ end
170
+
171
+ # return the logger, the server uses it as a helper
172
+ @logger
173
+ end # log
174
+
175
+ # general access to the logger as Chook::Server::Log.logger
176
+ def self.logger
177
+ @logger ||= startup
178
+ end
179
+
180
+ # a Hash of registered log streams
181
+ # streams are keys, valus are their IP addrs
182
+ # see the `get '/subscribe_to_log_stream'` route
183
+ #
184
+ def self.log_streams
185
+ @log_streams ||= {}
186
+ end
187
+
188
+ def self.clean_log_streams
189
+ log_streams.delete_if do |stream, ip|
190
+ if stream.closed?
191
+ logger.debug "Removing closed log stream for #{ip}"
192
+ true
193
+ else
194
+ false
195
+ end # if
196
+ end # delete if
197
+ end # clean_log_streams
198
+
199
+ end # module
200
+
201
+ end # server
202
+
203
+ # access from everywhere as Chook.logger
204
+ def self.logger
205
+ Server::Log.logger
206
+ end
207
+
208
+ # log an exception - multiple log lines
209
+ # the first being the error message the rest being indented backtrace
210
+ def self.log_exception(exception)
211
+ logger.error exception.to_s
212
+ exception.backtrace.each { |l| logger.error "..#{l}" }
213
+ end
214
+
215
+ end # Chook