betterlog 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/betterlog CHANGED
@@ -2,239 +2,240 @@
2
2
  # vim: set ft=ruby et sw=2 ts=2:
3
3
 
4
4
  require 'betterlog'
5
- require 'tins/go'
6
- require 'file-tail'
7
5
  require 'complex_config/rude'
8
6
  require 'zlib'
7
+ require 'file/tail'
8
+
9
+ module Betterlog
10
+ class App
11
+ def initialize(args = ARGV.dup)
12
+ STDOUT.sync = true
13
+ @args = args
14
+ @opts = Tins::GO.go 'cfhp:e:s:S:n:F:', @args, defaults: { ?c => true, ?p => ?d }
15
+ filter_severities
16
+ @opts[?h] and usage
17
+ end
9
18
 
10
- class Betterlog
11
- def initialize(args = ARGV.dup)
12
- STDOUT.sync = true
13
- @args = args
14
- @opts = Tins::GO.go 'cfhp:e:s:S:n:F:', @args, defaults: { ?c => true, ?p => ?d }
15
- filter_severities
16
- @opts[?h] and usage
17
- end
18
-
19
- def usage
20
- puts <<~end
21
- Usage: #{prog} [OPTIONS] [LOGFILES]
19
+ def usage
20
+ puts <<~end
21
+ Usage: #{prog} [OPTIONS] [LOGFILES]
22
22
 
23
- Options are
23
+ Options are
24
24
 
25
- -c to enable colors during pretty printing
26
- -f to follow the log files
27
- -h to display this help
28
- -p FORMAT to pretty print the log file if possible
29
- -e EMITTER only output events from these emitters
30
- -s MATCH only display events matching this search string
31
- -S SEVERITY only output events with severity, e. g. -S '>=warn'
32
- -n NUMBER rewind this many lines backwards before tailing log file
33
- -F SHORTCUT to open the config files with SHORTCUT
25
+ -c to enable colors during pretty printing
26
+ -f to follow the log files
27
+ -h to display this help
28
+ -p FORMAT to pretty print the log file if possible
29
+ -e EMITTER only output events from these emitters
30
+ -s MATCH only display events matching this search string
31
+ -S SEVERITY only output events with severity, e. g. -S '>=warn'
32
+ -n NUMBER rewind this many lines backwards before tailing log file
33
+ -F SHORTCUT to open the config files with SHORTCUT
34
34
 
35
- FORMAT values are: #{Array(cc.log.formats?&.attribute_names) * ?,}
35
+ FORMAT values are: #{Array(cc.log.formats?&.attribute_names) * ?,}
36
36
 
37
- SEVERITY values are: #{Log::Severity.all * ?|}
37
+ SEVERITY values are: #{Log::Severity.all * ?|}
38
38
 
39
- Config file SHORTCUTs are: #{Array(cc.log.config_files?&.attribute_names) * ?,}
39
+ Config file SHORTCUTs are: #{Array(cc.log.config_files?&.attribute_names) * ?,}
40
40
 
41
- Note, that you can use multiple SHORTCUTs via "-F foo -F bar".
41
+ Note, that you can use multiple SHORTCUTs via "-F foo -F bar".
42
42
 
43
- Examples:
43
+ Examples:
44
44
 
45
- - Follow rails log in long format with colors for errors or greater:
45
+ - Follow rails log in long format with colors for errors or greater:
46
46
 
47
- $ betterlog -f -F rails -p long -c -S ">=error"
47
+ $ betterlog -f -F rails -p long -c -S ">=error"
48
48
 
49
- - Follow rails AND redis logs with default format in colors
50
- including the last 10 lines:
49
+ - Follow rails AND redis logs with default format in colors
50
+ including the last 10 lines:
51
51
 
52
- $ betterlog -f -F rails -F redis -pd -c -n 10
52
+ $ betterlog -f -F rails -F redis -pd -c -n 10
53
53
 
54
- - Filter stdin from file unicorn.log with default format in color:
54
+ - Filter stdin from file unicorn.log with default format in color:
55
55
 
56
- $ betterlog -pd -c <unicorn.log
56
+ $ betterlog -pd -c <unicorn.log
57
57
 
58
- - Filter the last 10 lines of file unicorn.log with default format
59
- in color:
58
+ - Filter the last 10 lines of file unicorn.log with default format
59
+ in color:
60
60
 
61
- $ betterlog -c -pd -n 10 unicorn.log
61
+ $ betterlog -c -pd -n 10 unicorn.log
62
62
 
63
- - Filter the last 10 lines of file unicorn.log as JSON events:
63
+ - Filter the last 10 lines of file unicorn.log as JSON events:
64
64
 
65
- $ betterlog -n 10 unicorn.log
65
+ $ betterlog -n 10 unicorn.log
66
66
 
67
+ end
68
+ exit(0)
67
69
  end
68
- exit(0)
69
- end
70
70
 
71
- private\
72
- def filter_severities
73
- @severities = Log::Severity.all
74
- if severity = @opts[?S]
75
- severity.each do |s|
76
- if s =~ /\A(>=?|<=?)(.+)/
77
- gs = Log::Severity.new($2)
78
- @severities.select! { |x| x.send($1, gs) }
79
- else
80
- gs = Log::Severity.new(s)
81
- @severities.select! { |x| x == gs }
71
+ private\
72
+ def filter_severities
73
+ @severities = Log::Severity.all
74
+ if severity = @opts[?S]
75
+ severity.each do |s|
76
+ if s =~ /\A(>=?|<=?)(.+)/
77
+ gs = Log::Severity.new($2)
78
+ @severities.select! { |x| x.send($1, gs) }
79
+ else
80
+ gs = Log::Severity.new(s)
81
+ @severities.select! { |x| x == gs }
82
+ end
82
83
  end
83
84
  end
84
85
  end
85
- end
86
-
87
- def prog
88
- File.basename($0)
89
- end
90
86
 
91
- def emitters
92
- Array(@opts[?e])
93
- end
87
+ def prog
88
+ File.basename($0)
89
+ end
94
90
 
95
- def search_matched?(event)
96
- case @opts[?s]
97
- when /:\?\z/
98
- event[$`].present?
99
- when /:([^:]+)\z/
100
- event[$`].full?(:include?, $1)
101
- when String
102
- event.to_json.include?(@opts[?s])
103
- else
104
- return true
91
+ def emitters
92
+ Array(@opts[?e])
105
93
  end
106
- end
107
94
 
108
- def output_log_event(prefix, event)
109
- return unless @severities.include?(event.severity)
110
- return if emitters.full? && !emitters.include?(event.emitter)
111
- search_matched?(event) or return
112
- if format = @opts[?p]
113
- puts event.format(pretty: :format, color: @opts[?c], format: format)
114
- else
115
- puts "#{prefix}#{event}"
95
+ def search_matched?(event)
96
+ case @opts[?s]
97
+ when /:\?\z/
98
+ event[$`].present?
99
+ when /:([^:]+)\z/
100
+ event[$`].full?(:include?, $1)
101
+ when String
102
+ event.to_json.include?(@opts[?s])
103
+ else
104
+ return true
105
+ end
116
106
  end
117
- end
118
107
 
119
- def output_log_line(l, filename)
120
- l.blank? and return
121
- prefix =
122
- if filename && @args.size > 1
123
- "#{filename}: "
108
+ def output_log_event(prefix, event)
109
+ return unless @severities.include?(event.severity)
110
+ return if emitters.full? && !emitters.include?(event.emitter)
111
+ search_matched?(event) or return
112
+ if format = @opts[?p]
113
+ puts event.format(pretty: :format, color: @opts[?c], format: format)
114
+ else
115
+ puts "#{prefix}#{event}"
124
116
  end
125
- if event = Log::Event.parse(l)
126
- filename and event[:file] = filename
127
- output_log_event(prefix, event)
128
- elsif l =~ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3})\d* (.*)/
129
- event = Log::Event.new(
130
- timestamp: $1,
131
- message: Term::ANSIColor.uncolor($2),
132
- type: 'isoprefix',
133
- )
134
- filename and event[:file] = filename
135
- output_log_event(prefix, event)
136
- else
137
- @opts[?e] or puts "#{prefix}#{l}"
138
117
  end
139
- rescue
140
- @opts[?e] or puts "#{prefix}#{l}"
141
- end
142
118
 
143
- def query_config_file_configuration
144
- if @opts[?F]
145
- if cfs = cc.log.config_files?
146
- @opts[?F].each do |f|
147
- @args.concat cfs[f]
119
+ def output_log_line(l, filename)
120
+ l.blank? and return
121
+ prefix =
122
+ if filename && @args.size > 1
123
+ "#{filename}: "
148
124
  end
125
+ if event = Log::Event.parse(l)
126
+ filename and event[:file] = filename
127
+ output_log_event(prefix, event)
128
+ elsif l =~ /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3})\d* (.*)/
129
+ event = Log::Event.new(
130
+ timestamp: $1,
131
+ message: Term::ANSIColor.uncolor($2),
132
+ type: 'isoprefix',
133
+ )
134
+ filename and event[:file] = filename
135
+ output_log_event(prefix, event)
149
136
  else
150
- fail "no config files for #{@opts[?F]} defined"
151
- end
152
- else
153
- if @args.empty? and r = cc.log.config_files?&.rails?
154
- @args.concat r
155
- end
156
- if @args.empty?
157
- fail "filenames to follow needed"
137
+ @opts[?e] or puts "#{prefix}#{l}"
158
138
  end
139
+ rescue
140
+ @opts[?e] or puts "#{prefix}#{l}"
159
141
  end
160
- @args.uniq!
161
- end
162
142
 
163
- def follow_files
164
- group = File::Tail::Group.new
165
- @args.each do |f|
166
- if File.exist?(f)
167
- group.add_filename f, @opts[?n].to_i
143
+ def query_config_file_configuration
144
+ if @opts[?F]
145
+ if cfs = cc.log.config_files?
146
+ @opts[?F].each do |f|
147
+ @args.concat cfs[f]
148
+ end
149
+ else
150
+ fail "no config files for #{@opts[?F]} defined"
151
+ end
168
152
  else
169
- STDERR.puts "file #{f.inspect} does not exist, skip it!"
153
+ if @args.empty? and r = cc.log.config_files?&.rails?
154
+ @args.concat r
155
+ end
156
+ if @args.empty?
157
+ fail "filenames to follow needed"
158
+ end
170
159
  end
160
+ @args.uniq!
171
161
  end
172
- group.each_file { |f| f.max_interval = 1 }
173
- t = Thread.new do
174
- group.tail { |l| output_log_line(l, l.file.path) }
175
- end
176
- t.join
177
- rescue Interrupt
178
- end
179
162
 
180
- def filter_argv
181
- for fn in @args
182
- unless File.exist?(fn)
183
- STDERR.puts "file #{fn.inspect} does not exist, skip it!"
184
- next
163
+ def follow_files
164
+ group = File::Tail::Group.new
165
+ @args.each do |f|
166
+ if File.exist?(f)
167
+ group.add_filename f, @opts[?n].to_i
168
+ else
169
+ STDERR.puts "file #{f.inspect} does not exist, skip it!"
170
+ end
185
171
  end
186
- if fn.end_with?('.gz')
187
- Zlib::GzipReader.open(fn) do |f|
188
- f.extend(File::Tail)
189
- f.each_line do |l|
190
- output_log_line(l, fn)
191
- end
172
+ group.each_file { |f| f.max_interval = 1 }
173
+ t = Thread.new do
174
+ group.tail { |l| output_log_line(l, l.file.path) }
175
+ end
176
+ t.join
177
+ rescue Interrupt
178
+ end
179
+
180
+ def filter_argv
181
+ for fn in @args
182
+ unless File.exist?(fn)
183
+ STDERR.puts "file #{fn.inspect} does not exist, skip it!"
184
+ next
192
185
  end
193
- else
194
- File::Tail::Logfile.open(fn, backward: @opts[?n].to_i) do |f|
195
- f.each_line do |l|
196
- output_log_line(l, fn)
186
+ if fn.end_with?('.gz')
187
+ Zlib::GzipReader.open(fn) do |f|
188
+ f.extend(File::Tail)
189
+ f.each_line do |l|
190
+ output_log_line(l, fn)
191
+ end
192
+ end
193
+ else
194
+ File::Tail::Logfile.open(fn, backward: @opts[?n].to_i) do |f|
195
+ f.each_line do |l|
196
+ output_log_line(l, fn)
197
+ end
197
198
  end
198
199
  end
199
200
  end
200
201
  end
201
- end
202
202
 
203
- def filter_stdin
204
- STDIN.each_line do |l|
205
- output_log_line(l, nil)
203
+ def filter_stdin
204
+ STDIN.each_line do |l|
205
+ output_log_line(l, nil)
206
+ end
206
207
  end
207
- end
208
208
 
209
- def output_log_sources
210
- if @args.empty?
211
- STDERR.puts "#{prog} tracking stdin\nseverities: #{@severities * ?|}"
212
- else
213
- STDERR.puts "#{prog} tracking files:\n"\
214
- "#{@args.map { |a| ' ' + a.inspect }.join(' ')}\n"\
215
- "severities: #{@severities * ?|}\n"
209
+ def output_log_sources
210
+ if @args.empty?
211
+ STDERR.puts "#{prog} tracking stdin\nseverities: #{@severities * ?|}"
212
+ else
213
+ STDERR.puts "#{prog} tracking files:\n"\
214
+ "#{@args.map { |a| ' ' + a.inspect }.join(' ')}\n"\
215
+ "severities: #{@severities * ?|}\n"
216
+ end
216
217
  end
217
- end
218
218
 
219
- def run
220
- if @opts[?f]
221
- query_config_file_configuration
222
- output_log_sources
223
- follow_files
224
- elsif @opts[?F] && @args.empty?
225
- query_config_file_configuration
226
- output_log_sources
227
- filter_argv
228
- elsif !@args.empty?
229
- output_log_sources
230
- filter_argv
231
- else
232
- output_log_sources
233
- filter_stdin
219
+ def run
220
+ if @opts[?f]
221
+ query_config_file_configuration
222
+ output_log_sources
223
+ follow_files
224
+ elsif @opts[?F] && @args.empty?
225
+ query_config_file_configuration
226
+ output_log_sources
227
+ filter_argv
228
+ elsif !@args.empty?
229
+ output_log_sources
230
+ filter_argv
231
+ else
232
+ output_log_sources
233
+ filter_stdin
234
+ end
234
235
  end
235
236
  end
236
237
  end
237
238
 
238
239
  if File.basename($0) == File.basename(__FILE__)
239
- Betterlog.new(ARGV).run
240
+ Betterlog::App.new(ARGV).run
240
241
  end
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set ft=ruby et sw=2 ts=2:
3
+
4
+ require 'betterlog'
5
+ require 'excon'
6
+
7
+ lines = Integer(ENV.fetch('BETTERLOG_LINES', 1_000))
8
+ url = ENV.fetch('BETTERLOG_SERVER_URL')
9
+ name = ENV['BETTERLOG_NAME']
10
+ redis = Redis.new(url: ENV.fetch('REDIS_URL'))
11
+ logger = Betterlog::Logger.new(redis, name: name)
12
+
13
+ quit = false
14
+
15
+ [ :TERM, :INT, :QUIT ].each { |s| trap(s) { quit = true } }
16
+
17
+ STDOUT.sync = true
18
+ loop do
19
+ count = 0
20
+ logger.each_slice(lines).with_index do |batch, i|
21
+ count.zero? and print ?(
22
+ count += batch.sum(&:size)
23
+ attempt(attempts: 10, sleep: -60, reraise: true) do
24
+ print ?┄
25
+ Excon.post(url, body: batch.join)
26
+ end
27
+ end
28
+ quit and exit
29
+ if count.zero?
30
+ sleep 1
31
+ else
32
+ print "→%s)" % Tins::Unit.format(count, format: '%.2f %U', prefix: 1024, unit: ?b)
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ Copyright 2018 Florian Frank
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,165 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "io/ioutil"
7
+ "log"
8
+ "net/http"
9
+ "os"
10
+ "strings"
11
+
12
+ betterlog "github.com/betterplace/betterlog/betterlog"
13
+ "github.com/go-redis/redis"
14
+ "github.com/kelseyhightower/envconfig"
15
+ "github.com/labstack/echo"
16
+ "github.com/labstack/echo/middleware"
17
+ "golang.org/x/crypto/acme/autocert"
18
+ )
19
+
20
+ type Config struct {
21
+ PORT int `default:"5514"`
22
+ HEALTHZ_PORT int `default:"5513"`
23
+ HTTP_REALM string `default:"betterlog"`
24
+ HTTP_AUTH string
25
+ SSL bool
26
+ REDIS_PREFIX string
27
+ REDIS_URL string `default:"redis://localhost:6379"`
28
+ }
29
+
30
+ type RedisCertCache struct {
31
+ Redis *redis.Client
32
+ PREFIX string
33
+ }
34
+
35
+ // Get reads certificate data from the specified key name.
36
+ func (cache RedisCertCache) Get(ctx context.Context, name string) ([]byte, error) {
37
+ name = strings.Join([]string{cache.PREFIX, name}, "/")
38
+ done := make(chan struct{})
39
+ var (
40
+ err error
41
+ data string
42
+ )
43
+ go func() {
44
+ defer close(done)
45
+ result := cache.Redis.Get(name)
46
+ err = result.Err()
47
+ if err == nil {
48
+ data, err = result.Result()
49
+ }
50
+ }()
51
+ select {
52
+ case <-ctx.Done():
53
+ return nil, ctx.Err()
54
+ case <-done:
55
+ }
56
+ if err == redis.Nil {
57
+ return nil, autocert.ErrCacheMiss
58
+ }
59
+ return []byte(data), err
60
+ }
61
+
62
+ // Put writes the certificate data to the specified redis key name.
63
+ func (cache RedisCertCache) Put(ctx context.Context, name string, data []byte) error {
64
+ name = strings.Join([]string{cache.PREFIX, name}, "/")
65
+ done := make(chan struct{})
66
+ var err error
67
+ go func() {
68
+ defer close(done)
69
+ select {
70
+ case <-ctx.Done():
71
+ // Don't overwrite the key if the context was canceled.
72
+ default:
73
+ result := cache.Redis.Set(name, string(data), 0)
74
+ err = result.Err()
75
+ }
76
+ }()
77
+ select {
78
+ case <-ctx.Done():
79
+ return ctx.Err()
80
+ case <-done:
81
+ }
82
+ return err
83
+ }
84
+
85
+ // Delete removes the specified key name.
86
+ func (cache RedisCertCache) Delete(ctx context.Context, name string) error {
87
+ name = strings.Join([]string{cache.PREFIX, name}, "/")
88
+ var (
89
+ err error
90
+ done = make(chan struct{})
91
+ )
92
+ go func() {
93
+ defer close(done)
94
+ err = cache.Redis.Del(name).Err()
95
+ }()
96
+ select {
97
+ case <-ctx.Done():
98
+ return ctx.Err()
99
+ case <-done:
100
+ }
101
+ return err
102
+ }
103
+
104
+ func postLogHandler(c echo.Context) error {
105
+ body := c.Request().Body
106
+ data, err := ioutil.ReadAll(body)
107
+ if err == nil {
108
+ defer body.Close()
109
+ os.Stdout.Write(data)
110
+ return c.NoContent(http.StatusOK)
111
+ } else {
112
+ return c.String(http.StatusInternalServerError, err.Error())
113
+ }
114
+ }
115
+
116
+ func basicAuthConfig(config Config) middleware.BasicAuthConfig {
117
+ return middleware.BasicAuthConfig{
118
+ Realm: config.HTTP_REALM,
119
+ Validator: func(username, password string, c echo.Context) (bool, error) {
120
+ httpAuth := strings.Split(config.HTTP_AUTH, ":")
121
+ if username == httpAuth[0] && password == httpAuth[1] {
122
+ return true, nil
123
+ }
124
+ return false, nil
125
+ },
126
+ }
127
+ }
128
+
129
+ func initializeRedis(config Config) *redis.Client {
130
+ options, err := redis.ParseURL(config.REDIS_URL)
131
+ if err != nil {
132
+ log.Panic(err)
133
+ }
134
+ options.MaxRetries = 3
135
+ return redis.NewClient(options)
136
+ }
137
+
138
+ func main() {
139
+ var config Config
140
+ err := envconfig.Process("", &config)
141
+ if err != nil {
142
+ log.Fatal(err)
143
+ }
144
+ e := echo.New()
145
+ if config.HTTP_AUTH != "" {
146
+ fmt.Println("info: Configuring HTTP Auth access control")
147
+ e.Use(middleware.BasicAuthWithConfig(basicAuthConfig(config)))
148
+ }
149
+ e.POST("/log", postLogHandler)
150
+ if config.SSL {
151
+ log.Println("Starting SSL AutoTLS service.")
152
+ redis := initializeRedis(config)
153
+ e.AutoTLSManager.Cache = RedisCertCache{
154
+ Redis: redis,
155
+ PREFIX: config.REDIS_PREFIX,
156
+ }
157
+ go betterlog.StartHealthzEcho(
158
+ betterlog.Health{
159
+ PORT: config.HEALTHZ_PORT,
160
+ })
161
+ e.Logger.Fatal(e.StartAutoTLS(fmt.Sprintf(":%d", config.PORT)))
162
+ } else {
163
+ e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", config.PORT)))
164
+ }
165
+ }
@@ -1,4 +1,4 @@
1
- default: &default
1
+ development: &development
2
2
  styles:
3
3
  'timestamp': [ yellow, bold ]
4
4
  'file': [ blue, bold ]
@@ -32,9 +32,6 @@ default: &default
32
32
  {program}: {message}
33
33
  metric: >
34
34
  {%ft%timestamp} {metric} {value} {type}
35
-
36
- development:
37
- <<: *default
38
35
  config_files:
39
36
  rails:
40
37
  - log/development.log
@@ -43,35 +40,8 @@ development:
43
40
  redis:
44
41
  - /usr/local/var/log/redis.log
45
42
  elasticsearch:
46
- - /usr/local/var/log/elasticsearch@1.7.log
47
- nginx:
48
- - /usr/local/var/log/nginx/access.log
49
- - /usr/local/var/log/nginx/error.log
50
- legacy_supported: <%= ENV['LOG_LEGACY_SUPPORTED'].to_i == 1 %>
51
-
52
- test:
53
- <<: *default
54
- config_files:
55
- test:
56
- - log/test.log
57
- legacy_supported: <%= ENV['LOG_LEGACY_SUPPORTED'].to_i == 1 %>
58
-
59
- production:
60
- <<: *default
61
- config_files:
62
- rails:
63
- - /var/apps/betterplace/current/log/production.log
64
- cron:
65
- - /var/apps/betterplace/current/log/cron.log
66
- nginx:
67
- - /var/apps/betterplace/current/log/nginx.assets.access.log
68
- - /var/apps/betterplace/current/log/nginx.assets.error.log
69
- - /var/apps/betterplace/current/log/nginx.betterplace.access.log
70
- - /var/apps/betterplace/current/log/nginx.betterplace.error.log
71
- - /var/apps/betterplace/current/log/nginx.default.access.log
72
- - /var/apps/betterplace/current/log/nginx.default.error.log
73
- memosig:
74
- - /var/log/memosig/current
75
- unicorn:
76
- - /var/log/unicorn/current
43
+ - /usr/local/var/log/elasticsearch.log
77
44
  legacy_supported: yes
45
+ test: *development
46
+ staging: *development
47
+ production: *development