port-authority 0.4.8 → 0.5.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.
- checksums.yaml +4 -4
- data/bin/pa-lbaas-agent +3 -0
- data/bin/pa-service-list +2 -2
- data/lib/port-authority.rb +0 -8
- data/lib/port-authority/agent.rb +164 -0
- data/lib/port-authority/agents/lbaas.rb +134 -0
- data/lib/port-authority/config.rb +49 -0
- data/lib/port-authority/etcd.rb +47 -0
- data/lib/port-authority/logger.rb +56 -0
- data/lib/port-authority/mechanism/floating_ip.rb +51 -0
- data/lib/port-authority/mechanism/load_balancer.rb +89 -0
- data/lib/port-authority/tool.rb +9 -0
- data/lib/port-authority/{services.rb → tools/service_list.rb} +12 -16
- metadata +16 -18
- data/bin/pa-manager +0 -3
- data/lib/port-authority/manager.rb +0 -153
- data/lib/port-authority/manager/init.rb +0 -49
- data/lib/port-authority/manager/threads/icmp.rb +0 -30
- data/lib/port-authority/manager/threads/swarm.rb +0 -35
- data/lib/port-authority/util/config.rb +0 -62
- data/lib/port-authority/util/etcd.rb +0 -50
- data/lib/port-authority/util/helpers.rb +0 -27
- data/lib/port-authority/util/loadbalancer.rb +0 -78
- data/lib/port-authority/util/logger.rb +0 -57
- data/lib/port-authority/util/vip.rb +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 221e5235470801ff068ea1fd1cf29494ac4e5c7d
|
4
|
+
data.tar.gz: d916df00dc140f8d88692dc3df7dd1d427522e8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99168b0111a49424e6166a91a0586819b65df8007edfe766740df902bb922400c442ef58a52ae4611f3737d5f609b52bdabf92a24b319849d5fd3e1af669eae1
|
7
|
+
data.tar.gz: 12f4a20fc48a4606d9e841f5fbba75189294090f668d7f32822d236d39ebe979b871f3e2a4efc19ed395593d9ef174535989493ae25ecab29674e4ac11f8ffd2
|
data/bin/pa-lbaas-agent
ADDED
data/bin/pa-service-list
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require 'port-authority/
|
3
|
-
PortAuthority::
|
2
|
+
require 'port-authority/tools/service_list'
|
3
|
+
PortAuthority::Tools::ServiceList.new
|
data/lib/port-authority.rb
CHANGED
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'digest/sha2'
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
require 'port-authority'
|
6
|
+
require 'port-authority/config'
|
7
|
+
require 'port-authority/logger'
|
8
|
+
require 'port-authority/etcd'
|
9
|
+
|
10
|
+
module PortAuthority
|
11
|
+
# Scaffolding class for agents
|
12
|
+
class Agent
|
13
|
+
# Common agent process init. Contains configuration load,
|
14
|
+
# common signal responses and runtime variables init.
|
15
|
+
# Implements execution of actual agents via \+run+ method.
|
16
|
+
# Also handles any uncaught exceptions.
|
17
|
+
def initialize
|
18
|
+
Thread.current[:name] = 'main' # name main thread
|
19
|
+
@@_exit = false # prepare exit flag
|
20
|
+
@@_semaphores = { log: Mutex.new } # init semaphores
|
21
|
+
@@_threads = {} # init threads
|
22
|
+
Signal.trap('INT') { exit!(1) } # end immediatelly
|
23
|
+
Signal.trap('TERM') { end! } # end gracefully
|
24
|
+
Config.load! || exit!(1) # load config or die
|
25
|
+
begin # all-wrapping exception ;)
|
26
|
+
run # hook to child class
|
27
|
+
rescue StandardError => e
|
28
|
+
Logger.alert "UNCAUGHT EXCEPTION IN THREAD main! Dying! X.X"
|
29
|
+
Logger.alert [' ', "#{e.class}:", e.message].join(' ')
|
30
|
+
e.backtrace.each {|line| Logger.debug " #{line}"}
|
31
|
+
exit! 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Setup the agent process.
|
36
|
+
# Initializes logging, system process parameters,
|
37
|
+
# daemonizing.
|
38
|
+
#
|
39
|
+
# There are 4 optional parameters:
|
40
|
+
# +:name+:: \+String+ Agent name. Defaults to \+self.class.downcase+ of the child agent
|
41
|
+
# +:root+:: \+Bool+ Require to be ran as root. Defaults to \+false+.
|
42
|
+
# +:daemonize+:: \+Bool+ Daemonize the process. Defaults to \+false+.
|
43
|
+
# +:nice+:: \+Int+ nice of the process. Defaults to \+0+
|
44
|
+
def setup(args = {})
|
45
|
+
name = args[:name] || self.class.to_s.downcase.split('::').last
|
46
|
+
args[:root] ||= false
|
47
|
+
args[:daemonize] ||= false
|
48
|
+
args[:nice] ||= 0
|
49
|
+
Logger.init! @@_semaphores[:log]
|
50
|
+
Logger.info 'Starting main thread'
|
51
|
+
Logger.debug 'Setting process name'
|
52
|
+
if RUBY_VERSION >= '2.1'
|
53
|
+
Process.setproctitle("pa-#{name}-agent")
|
54
|
+
else
|
55
|
+
$0 = "pa-#{name}-agent"
|
56
|
+
end
|
57
|
+
if args[:root] && Process.uid != 0
|
58
|
+
Logger.alert 'Must run under root user!'
|
59
|
+
exit! 1
|
60
|
+
end
|
61
|
+
Logger.debug 'Setting CPU nice level'
|
62
|
+
Process.setpriority(Process::PRIO_PROCESS, 0, args[:nice])
|
63
|
+
if args[:daemonize]
|
64
|
+
Logger.info 'Daemonizing process'
|
65
|
+
if RUBY_VERSION < '1.9'
|
66
|
+
exit if fork
|
67
|
+
Process.setsid
|
68
|
+
exit if fork
|
69
|
+
Dir.chdir('/')
|
70
|
+
else
|
71
|
+
Process.daemon
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Has the exit flag been raised?
|
77
|
+
def exit?
|
78
|
+
@@_exit
|
79
|
+
end
|
80
|
+
|
81
|
+
# Raise the exit flag
|
82
|
+
def end!
|
83
|
+
@@_exit = true
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create a named \+Mutex+ semaphore
|
87
|
+
def sem_create(name)
|
88
|
+
@@_semaphores.merge!(Hash[name.to_sym], Mutex.new)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create a named \+Thread+ with its \+Mutex+ semaphore.
|
92
|
+
# The definition includes \+&block+ of code that should run
|
93
|
+
# within the thread.
|
94
|
+
#
|
95
|
+
# The method requires 3 parameters:
|
96
|
+
# +name+:: \+Symbol+ Thread/Mutex name.
|
97
|
+
# +interval+:: \+Integer+ Thread loop interval.
|
98
|
+
# +&block+:: \+Proc+ Block of code to run.
|
99
|
+
def thr_create(name, interval, &block)
|
100
|
+
@@_semaphores.merge!(Hash[name.to_sym, Mutex.new])
|
101
|
+
@@_threads.merge!(Hash[name.to_sym, Thread.new do
|
102
|
+
Thread.current[:name] = name.to_s
|
103
|
+
Logger.info "Starting thread #{Thread.current[:name]}"
|
104
|
+
begin
|
105
|
+
until exit?
|
106
|
+
yield block
|
107
|
+
sleep interval
|
108
|
+
end
|
109
|
+
Logger.info "Ending thread #{Thread.current[:name]}"
|
110
|
+
rescue StandardError => e
|
111
|
+
Logger.alert "UNCAUGHT EXCEPTION IN THREAD #{Thread.current[:name]}"
|
112
|
+
Logger.alert [' ', "#{e.class}:", e.message].join(' ')
|
113
|
+
e.backtrace.each {|line| Logger.debug " #{line}"}
|
114
|
+
end!
|
115
|
+
end
|
116
|
+
end
|
117
|
+
])
|
118
|
+
end
|
119
|
+
|
120
|
+
# Run thread-safe code.
|
121
|
+
# The \+name+ parameter can be omitted when used
|
122
|
+
# from within a block of thread code. In this case
|
123
|
+
# the Mutex with the same \+:name+ will be used.
|
124
|
+
#
|
125
|
+
# The method accepts following parameters:
|
126
|
+
# +name+:: \+Symbol+ Mutex name.
|
127
|
+
# +&block+:: \+Proc+ Block of code to run.
|
128
|
+
def thr_safe(name=Thread.current[:name].to_sym, &block)
|
129
|
+
@@_semaphores[name.to_sym].synchronize do
|
130
|
+
yield block
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Start named thread.
|
135
|
+
# If the name is omitted, applies to all spawned threads ;)
|
136
|
+
def thr_start(name=nil)
|
137
|
+
return @@_threads[name].run if name
|
138
|
+
@@_threads.each_value(&:run)
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
# Wait for named thread to finish.
|
143
|
+
# If the name is omitted, applies to all spawned threads ;)
|
144
|
+
def thr_wait(name=nil)
|
145
|
+
return @@_threads[name].join if name
|
146
|
+
@@_threads.each_value(&:join)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Run command in Bash.
|
150
|
+
def shellcmd(*args)
|
151
|
+
cmd = args.join(' ').to_s
|
152
|
+
cksum = Digest::SHA256.hexdigest(args.join.to_s)[0..15]
|
153
|
+
Logger.debug "Executing shellcommand #{cksum} - #{cmd}"
|
154
|
+
ret = system cmd
|
155
|
+
Logger.debug "Shellcommand #{cksum} returned #{ret.to_s}"
|
156
|
+
end
|
157
|
+
|
158
|
+
# Return hostname.
|
159
|
+
def hostname
|
160
|
+
@hostname ||= Socket.gethostname
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# rubocop:disable MethodLength, CyclomaticComplexity, Metrics/BlockNesting, Metrics/LineLength, Metrics/AbcSize, Metrics/PerceivedComplexity
|
2
|
+
require 'socket'
|
3
|
+
require 'port-authority/agent'
|
4
|
+
require 'port-authority/mechanism/load_balancer'
|
5
|
+
require 'port-authority/mechanism/floating_ip'
|
6
|
+
|
7
|
+
module PortAuthority
|
8
|
+
module Agents
|
9
|
+
class LBaaS < PortAuthority::Agent
|
10
|
+
include PortAuthority::Mechanism
|
11
|
+
|
12
|
+
def run
|
13
|
+
setup(daemonize: Config.daemonize, nice: -10, root: true)
|
14
|
+
Signal.trap('HUP') { Config.load! && LoadBalancer.init! && FloatingIP.init! }
|
15
|
+
Signal.trap('USR1') { Logger.debug! }
|
16
|
+
Signal.trap('USR2') { @lb_update_hook = true }
|
17
|
+
@status_swarm = false
|
18
|
+
@etcd = PortAuthority::Etcd.cluster_connect Config.etcd
|
19
|
+
|
20
|
+
thr_create(:swarm, Config.lbaas[:swarm_interval] || Config.lbaas[:interval]) do
|
21
|
+
begin
|
22
|
+
Logger.debug 'Checking Swarm state'
|
23
|
+
status = @etcd.am_i_swarm_leader?
|
24
|
+
thr_safe { @status_swarm = status }
|
25
|
+
Logger.debug "I am Swarm #{status ? 'leader' : 'follower' }"
|
26
|
+
rescue StandardError => e
|
27
|
+
Logger.error [ e.class, e.message ].join(': ')
|
28
|
+
e.backtrace.each {|line| Logger.debug " #{line}"}
|
29
|
+
thr_safe { @status_swarm = false }
|
30
|
+
sleep(Config.lbaas[:swarm_interval] || Config.lbaas[:interval])
|
31
|
+
retry unless exit?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
thr_start
|
36
|
+
|
37
|
+
FloatingIP.init!
|
38
|
+
LoadBalancer.init!
|
39
|
+
LoadBalancer.container || ( LoadBalancer.pull! && LoadBalancer.create! )
|
40
|
+
|
41
|
+
Logger.debug 'Waiting for threads to gather something...'
|
42
|
+
sleep Config.lbaas[:interval]
|
43
|
+
first_cycle = true
|
44
|
+
status_time = Time.now.to_i - 60
|
45
|
+
|
46
|
+
until exit?
|
47
|
+
status_swarm = false if first_cycle
|
48
|
+
if @lb_update_hook
|
49
|
+
Logger.notice 'LoadBalancer update triggerred'
|
50
|
+
LoadBalancer.update!
|
51
|
+
@lb_update_hook = false
|
52
|
+
Logger.notice 'LoadBalancer update finished'
|
53
|
+
end
|
54
|
+
sleep Config.lbaas[:interval]
|
55
|
+
thr_safe(:swarm) { status_swarm = @status_swarm }
|
56
|
+
# main logic
|
57
|
+
if status_swarm
|
58
|
+
# handle FloatingIP on leader
|
59
|
+
Logger.debug 'I am the LEADER'
|
60
|
+
if FloatingIP.up?
|
61
|
+
Logger.debug 'Got FloatingIP, that is OK'
|
62
|
+
else
|
63
|
+
Logger.notice 'No FloatingIP here, checking whether it is free'
|
64
|
+
FloatingIP.arp_del!
|
65
|
+
if FloatingIP.reachable?
|
66
|
+
Logger.notice 'FloatingIP is still up! (ICMP)'
|
67
|
+
else
|
68
|
+
Logger.info 'FloatingIP is unreachable by ICMP, checking for duplicates on L2'
|
69
|
+
FloatingIP.arp_del!
|
70
|
+
if FloatingIP.duplicate?
|
71
|
+
Logger.error 'FloatingIP is still assigned! (ARP)'
|
72
|
+
else
|
73
|
+
Logger.notice 'FloatingIP is free :) assigning'
|
74
|
+
FloatingIP.handle! status_swarm
|
75
|
+
Logger.notice 'Notifying the network about change'
|
76
|
+
FloatingIP.update_arp!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# handle LoadBalancer on leader
|
81
|
+
if LoadBalancer.up?
|
82
|
+
Logger.debug 'LoadBalancer is up, that is OK'
|
83
|
+
else
|
84
|
+
Logger.notice 'LoadBalancer is down, starting'
|
85
|
+
LoadBalancer.start!
|
86
|
+
end
|
87
|
+
else
|
88
|
+
# handle FloatingIP on follower
|
89
|
+
Logger.debug 'I am a follower'
|
90
|
+
if FloatingIP.up?
|
91
|
+
Logger.notice 'I got FloatingIP and should not, removing'
|
92
|
+
FloatingIP.handle! status_swarm
|
93
|
+
FloatingIP.arp_del!
|
94
|
+
Logger.notice 'Notifying the network about change'
|
95
|
+
FloatingIP.update_arp!
|
96
|
+
else
|
97
|
+
Logger.debug 'No FloatingIP here, that is OK'
|
98
|
+
end
|
99
|
+
# handle LoadBalancer on follower
|
100
|
+
if LoadBalancer.up?
|
101
|
+
Logger.notice 'LoadBalancer is up, stopping'
|
102
|
+
LoadBalancer.stop!
|
103
|
+
else
|
104
|
+
Logger.debug 'LoadBalancer is down, that is OK'
|
105
|
+
end
|
106
|
+
end # logic end
|
107
|
+
end
|
108
|
+
|
109
|
+
thr_wait
|
110
|
+
|
111
|
+
# remove FloatingIP on shutdown
|
112
|
+
if FloatingIP.up?
|
113
|
+
Logger.notice 'Removing FloatingIP'
|
114
|
+
FloatingIP.handle! false
|
115
|
+
FloatingIP.update_arp!
|
116
|
+
end
|
117
|
+
|
118
|
+
# stop LB on shutdown
|
119
|
+
if LoadBalancer.up?
|
120
|
+
Logger.notice 'Stopping LoadBalancer'
|
121
|
+
LoadBalancer.stop!
|
122
|
+
end
|
123
|
+
|
124
|
+
Logger.notice 'Exiting...'
|
125
|
+
exit 0
|
126
|
+
end
|
127
|
+
|
128
|
+
def my_ip
|
129
|
+
@my_ip ||= Socket.ip_address_list.detect(&:ipv4_private?).ip_address
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'etcd-tools/mixins/hash'
|
2
|
+
|
3
|
+
module PortAuthority
|
4
|
+
module Config
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
attr_reader :_cfg
|
9
|
+
|
10
|
+
def method_missing(name, *_args, &_block)
|
11
|
+
return @_cfg[name.to_sym] if @_cfg[name.to_sym] != nil
|
12
|
+
fail(NoMethodError, "unknown configuration section #{name}", caller)
|
13
|
+
end
|
14
|
+
|
15
|
+
def load!
|
16
|
+
@_cfg = {
|
17
|
+
debug: false,
|
18
|
+
syslog: false,
|
19
|
+
daemonize: false,
|
20
|
+
etcd: {
|
21
|
+
endpoints: ['http://localhost:2379'],
|
22
|
+
timeout: 5
|
23
|
+
},
|
24
|
+
commands: {
|
25
|
+
arping: `which arping`.chomp,
|
26
|
+
arp: `which arp`.chomp,
|
27
|
+
iproute: `which ip`.chomp
|
28
|
+
}
|
29
|
+
}
|
30
|
+
files = ['/etc/port-authority.yaml', './etc/port-authority.yaml'].delete_if {|f| !File.exists?(f)}
|
31
|
+
dir_files = Dir['/etc/port-authority.d/**.yaml'] + Dir['./etc/port-authority.d/**.yaml']
|
32
|
+
files += dir_files
|
33
|
+
return false if files.empty?
|
34
|
+
files.each do |f|
|
35
|
+
@_cfg = @_cfg.deep_merge(YAML.load_file(f))
|
36
|
+
end
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def dump
|
41
|
+
self._cfg
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_yaml
|
45
|
+
self._cfg.to_yaml.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'etcd-tools'
|
2
|
+
require 'etcd-tools/mixins'
|
3
|
+
|
4
|
+
module PortAuthority
|
5
|
+
class Etcd < Etcd::Client
|
6
|
+
|
7
|
+
def self.cluster_connect(config)
|
8
|
+
endpoints = config[:endpoints].map {|ep| Hash[[:host, :port].zip(ep.match(/^(?:https?:\/\/)?([0-9a-zA-Z\.-_]+):([0-9]+)\/?/).captures)]}
|
9
|
+
timeout = config[:timeout].to_i
|
10
|
+
PortAuthority::Etcd.new(cluster: endpoints, read_timeout: timeout)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.shell_cluster_connect(env, timeout = 2)
|
14
|
+
env.split(',').each do |u|
|
15
|
+
(host, port) = u.gsub(/^https?:\/\//, '').gsub(/\/$/, '').split(':')
|
16
|
+
etcd = PortAuthority::Etcd.new(cluster: [{ host: host, port: port }], read_timeout: timeout)
|
17
|
+
return etcd if etcd.healthy?
|
18
|
+
next
|
19
|
+
end
|
20
|
+
raise Etcd::ClusterConnectError
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def swarm_leader
|
25
|
+
get('/_pa/docker/swarm/leader').value.split(':').first
|
26
|
+
end
|
27
|
+
|
28
|
+
def am_i_swarm_leader?
|
29
|
+
Socket.ip_address_list.map(&:ip_address).member?(swarm_leader)
|
30
|
+
end
|
31
|
+
|
32
|
+
def swarm_overlay_id(name)
|
33
|
+
get_hash('/_pa/docker/network/v1.0/network').each_value do |network|
|
34
|
+
return network['id'] if network['name'] == name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def swarm_list_services(network, service_name='.*')
|
39
|
+
services = {}
|
40
|
+
self.get_hash("/_pa/docker/network/v1.0/endpoint/#{swarm_overlay_id(network)}").each_value do |container|
|
41
|
+
next unless Regexp.new(service_name).match container['name']
|
42
|
+
services = { container['name'] => { 'id' => container['id'], 'ip' => container['ep_iface']['addr'].sub(/\/[0-9]+$/, ''), 'ports' => container['exposed_ports'].map { |port| port['Port'] } } }
|
43
|
+
end
|
44
|
+
services
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# rubocop:disable Metrics/LineLength, Metrics/AbcSize, Metrics/MethodLength
|
2
|
+
require 'syslog'
|
3
|
+
|
4
|
+
module PortAuthority
|
5
|
+
module Logger
|
6
|
+
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def init!(s)
|
10
|
+
@_s = s
|
11
|
+
@debug = Config.debug
|
12
|
+
Syslog.open($0, Syslog::LOG_PID, Syslog::LOG_DAEMON) if Config.syslog
|
13
|
+
end
|
14
|
+
|
15
|
+
def debug!
|
16
|
+
@debug = !@debug
|
17
|
+
end
|
18
|
+
|
19
|
+
def debug(message)
|
20
|
+
log :debug, message if @debug
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(name, *args, &_block)
|
24
|
+
if name
|
25
|
+
return log(name.to_sym, args[0])
|
26
|
+
else
|
27
|
+
fail(NoMethodError, "Unknown Logger method '#{name}'", caller)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def log(lvl, msg)
|
32
|
+
if Config.syslog
|
33
|
+
case lvl
|
34
|
+
when :debug
|
35
|
+
l = Syslog::LOG_DEBUG
|
36
|
+
when :info
|
37
|
+
l = Syslog::LOG_INFO
|
38
|
+
when :notice
|
39
|
+
l = Syslog::LOG_NOTICE
|
40
|
+
when :error
|
41
|
+
l = Syslog::LOG_ERR
|
42
|
+
when :alert
|
43
|
+
l = Syslog::LOG_ALERT
|
44
|
+
end
|
45
|
+
@_s.synchronize do
|
46
|
+
Syslog.log(l, "(%s) %s", Thread.current[:name], msg.to_s)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
@_s.synchronize do
|
50
|
+
$stdout.puts [Time.now.to_s, sprintf('%-6.6s', lvl.to_s.upcase), "(#{Thread.current[:name]})", msg.to_s].join(' ')
|
51
|
+
$stdout.flush
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|