betterlog 0.1.0 → 0.2.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.
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