tp2 0.13.4 → 0.14.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8df38416f003b41f8a768a46f6d6177a479627e57b741a571f4c32333babb61a
4
- data.tar.gz: abfc514214ca7b053d023dc40d182cf4f608ed155a66e8812c53d113c4c6f3cd
3
+ metadata.gz: 3c99469ae66a0fa3a184c3196d2e7978a5ae1cdac9b55820a4badc176030033a
4
+ data.tar.gz: 4e4f7646de8efdc89c23af826f2502c0aabc5ef8e7773272e4596ccf1c106822
5
5
  SHA512:
6
- metadata.gz: 0e37cab256b895605140bc727997a5f93fe821566525bfd4cbd9b2fc431be1470084ebed6ac235917105fffadd28a54c02638c304606464f83bcca16468c236d
7
- data.tar.gz: 5c55ab0d48a4a227f1ebc7aee1b14a54922f7d148b54f14e340f97d3d52b7811d530ef7c00ff6890d3879c383f2654ab0cde8257b3ba46d185ede6fd247516d9
6
+ metadata.gz: 3dc15dc21f2d199cf3d807cc99e82d2f624269862e9adbf49e0d1bcd29bf00046f6c90bdb9523decfa9ad78377278e2e56a0cb8b6cc30f990273aa9349fe5bed
7
+ data.tar.gz: c236ef22298ffca5c23a432dfa31e83c243821b0c95d1705eeb109d0fd9312669e43551a9ca64d90e5f16abd9ec593f77416aa04306ab86d675e06a6890effb5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 0.14.1 2025-07-08
2
+
3
+ - Add request elapsed time to log
4
+
5
+ # Version 0.14 2025-07-08
6
+
7
+ - Finish new logger
8
+
1
9
  # Version 0.13.4 2025-07-06
2
10
 
3
11
  - Improve logging for protocol errors, include more info
data/TODO.md CHANGED
@@ -14,6 +14,4 @@
14
14
  HEADER_RE = /^\s([^\s^\:]{1, #{MAX_HEADER_KEY_BYTES}}).../
15
15
  ```
16
16
 
17
- - Benchmark parsing with UM::Stream (current approach), against parsing with strscan
18
-
19
17
  - Add pluggable logging. Move default logging format outside and pass a callable.
data/examples/app.rb CHANGED
@@ -5,7 +5,7 @@ require 'tp2'
5
5
  require 'json'
6
6
 
7
7
  TP2.config do |req|
8
- raise 'foo'
8
+ # raise 'foo'
9
9
  body = req.headers.to_json
10
10
  req.respond(body, 'Content-Type' => 'application/json')
11
11
  end
@@ -5,7 +5,7 @@ require 'stringio'
5
5
 
6
6
  module TP2
7
7
  class HTTP1Connection
8
- attr_reader :fd
8
+ attr_reader :fd, :response_headers
9
9
 
10
10
  def initialize(machine, fd, opts, &app)
11
11
  @machine = machine
@@ -29,7 +29,10 @@ module TP2
29
29
  rescue UM::Terminate
30
30
  # server is terminated, do nothing
31
31
  rescue StandardError => e
32
- @logger&.call(e)
32
+ @logger&.error(
33
+ message: 'Uncaught error while running connection',
34
+ error: e
35
+ )
33
36
  ensure
34
37
  @machine.close_async(@fd)
35
38
  end
@@ -40,19 +43,27 @@ module TP2
40
43
  return false if !headers
41
44
 
42
45
  request = Qeweney::Request.new(headers, self)
46
+ request.start_stamp = monotonic_clock
43
47
  @app.call(request)
44
48
  persist_connection?(headers)
45
49
  rescue ProtocolError => e
46
- msg = "Protocol error: #{e.message}. Closing connection..."
47
- @logger&.call(msg)
50
+ @logger&.error(
51
+ message: 'Protocol error, closing connection',
52
+ error: e
53
+ )
48
54
  false
49
55
  rescue SystemCallError => e
50
- msg = "I/O error: #{e.class} #{e.message}. Closing connection..."
51
- @logger&.call(msg)
56
+ @logger&.error(
57
+ message: 'I/O error, closing connection',
58
+ error: e
59
+ )
52
60
  false
53
61
  rescue StandardError => e
54
- msg = "Internal error while serving request: #{e.class} #{e.message} (#{e.backtrace.inspect}). Abandoning connection..."
55
- @logger&.call(msg)
62
+ @logger&.error(
63
+ message: 'Internal error while serving request, abandoning connection',
64
+ error: e
65
+ )
66
+
56
67
  if request && !@done
57
68
  respond(request, 'Internal server error', ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
58
69
  end
@@ -116,7 +127,7 @@ module TP2
116
127
  else
117
128
  @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
118
129
  end
119
- @logger&.call(request, headers)
130
+ @logger&.info(request: request, response_headers: headers)
120
131
  @done = true
121
132
  @response_headers = headers
122
133
  end
@@ -155,7 +166,7 @@ module TP2
155
166
  @machine.send(@fd, data, data.bytesize, SEND_FLAGS)
156
167
  return if @done || !done
157
168
 
158
- @logger&.call(request, @response_headers)
169
+ @logger&.info(request: request, response_headers: @response_headers)
159
170
  @done = true
160
171
  end
161
172
 
@@ -167,7 +178,7 @@ module TP2
167
178
  @machine.send(@fd, EMPTY_CHUNK, EMPTY_CHUNK_LEN, SEND_FLAGS)
168
179
  return if @done
169
180
 
170
- @logger&.call(request, @response_headers)
181
+ @logger&.info(request, request, response_headers: @response_headers)
171
182
  @done = true
172
183
  end
173
184
 
@@ -207,6 +218,10 @@ module TP2
207
218
  @machine.close_async(@fd)
208
219
  end
209
220
 
221
+ def monotonic_clock
222
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
223
+ end
224
+
210
225
  private
211
226
 
212
227
  RE_REQUEST_LINE = %r{^([a-z]+)\s+([^\s]+)\s+(http/[0-9.]{1,3})}i
data/lib/tp2/logger.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module TP2
4
6
  class Logger
5
7
  def initialize(machine, fd = $stdout.fileno, **opts)
@@ -8,56 +10,85 @@ module TP2
8
10
  @opts = opts
9
11
  end
10
12
 
11
- # @param o <Qeweney::Request> request
12
- # @param h <Hash, nil> response headers
13
- def call(o, h = nil)
14
- log(format_log_line(o, h))
13
+ def info(o)
14
+ call(:INFO, o)
15
+ end
16
+
17
+ def warn(o)
18
+ call(:WARN, o)
19
+ end
20
+
21
+ def error(o)
22
+ call(:ERROR, o)
23
+ end
24
+
25
+ private
26
+
27
+ # @param level <Symbol> log level
28
+ # @param o <Hash> hash
29
+ def call(level, o)
30
+ emit(make_entry(level, o))
15
31
  rescue StandardError => e
32
+ puts 'Uncaught error while emitting log entry:'
16
33
  p e: e
17
34
  p e.backtrace
18
35
  exit
19
36
  end
20
37
 
21
- private
22
-
23
- def log(str)
24
- str = format(
25
- "%<stamp>s %<msg>s\n",
26
- stamp: Time.now.strftime('%Y-%m-%d %H:%M:%S.%3N'),
27
- msg: str
28
- )
29
- @machine.write_async(@fd, str)
38
+ def emit(entry)
39
+ @machine.write_async(@fd, "#{entry.to_json}\n")
30
40
  end
31
41
 
32
- def format_log_line(o, h)
33
- case o
34
- when Exception
35
- format_error_log_line(o)
36
- when Qeweney::Request
37
- format_request_log_line(o, h)
38
- when String
39
- o
42
+ def make_entry(level, o)
43
+ if o[:request]
44
+ make_request_entry(level, o)
45
+ elsif o[:error]
46
+ make_error_entry(level, o)
40
47
  else
41
- o.to_s
48
+ make_hash_entry(level, o)
42
49
  end
43
50
  end
44
51
 
45
- def format_error_log_line(err)
46
- "Error: #{err.inspect}: #{err.backtrace.inspect}"
52
+ def make_error_entry(level, o)
53
+ err = o[:error]
54
+ {
55
+ level: level.to_s,
56
+ ts: (t = Time.now; t.to_i),
57
+ ts_s: t.iso8601
58
+ }
59
+ .merge(o)
60
+ .merge(
61
+ error: "#{err.class}: #{err.message}",
62
+ backtrace: err.backtrace
63
+ )
47
64
  end
48
65
 
49
- def format_request_log_line(request, response_headers)
66
+ def make_request_entry(level, o)
67
+ request = o[:request]
50
68
  request_headers = request.headers
51
- uri = full_uri(request_headers)
52
- status = response_headers[':status'] || '200'
53
- format(
54
- '%<client_ip>s %<method>s %<uri>s %<status>s %<tx>d',
55
- client_ip: request.forwarded_for || '?',
56
- method: request_headers[':method'].upcase,
57
- uri: uri,
58
- status: status,
59
- tx: request_headers[':tx']
60
- )
69
+ response_headers = o[:response_headers]
70
+ elapsed = request.adapter.monotonic_clock - request.start_stamp
71
+ {
72
+ level: level.to_s,
73
+ ts: (t = Time.now; t.to_i),
74
+ ts_s: t.iso8601,
75
+ message: o[:message] || 'HTTP request done',
76
+ client_ip: request.forwarded_for || '?',
77
+ http_method: request_headers[':method'].upcase,
78
+ user_agent: request_headers['user-agent'],
79
+ uri: full_uri(request_headers),
80
+ status: response_headers[':status'] || '200',
81
+ elapsed: elapsed
82
+ }
83
+ end
84
+
85
+ def make_hash_entry(level, hash)
86
+ {
87
+ level: level.to_s,
88
+ ts: (t = Time.now; t.to_i),
89
+ ts_s: t.iso8601
90
+ }
91
+ .merge(hash)
61
92
  end
62
93
 
63
94
  def full_uri(headers)
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Qeweney::Request
4
+ attr_accessor :start_stamp
5
+
4
6
  def respond_with_static_file(path, etag, last_modified, opts)
5
7
  cache_headers = (etag || last_modified) ? {
6
8
  'etag' => etag,
7
- 'last-modified' => last_modified,
9
+ 'last-modified' => last_modified
8
10
  } : {}
9
11
 
10
12
  adapter.respond_with_static_file(self, path, opts, cache_headers)
data/lib/tp2/server.rb CHANGED
@@ -17,7 +17,7 @@ module TP2
17
17
 
18
18
  def self.tp2_app(_machine, opts)
19
19
  if opts[:app_location]
20
- opts[:logger]&.call("Loading app at #{opts[:app_location]}")
20
+ opts[:logger]&.info(message: 'Loading web app', location: opts[:app_location])
21
21
  require opts[:app_location]
22
22
 
23
23
  opts.merge!(TP2.config)
@@ -65,7 +65,7 @@ module TP2
65
65
  @accept_fibers << @machine.spin { accept_incoming(fd) }
66
66
  end
67
67
  bind_string = bind_info.map { it.join(':') }.join(', ')
68
- @opts[:logger]&.call("Listening on #{bind_string}")
68
+ @opts[:logger]&.info(message: "Listening on #{bind_string}")
69
69
 
70
70
  # map fibers
71
71
  @connection_fiber_map = {}
@@ -121,7 +121,7 @@ module TP2
121
121
  end
122
122
 
123
123
  def graceful_shutdown
124
- @opts[:logger]&.call('Shutting down gracefully...')
124
+ @opts[:logger]&.info(message: 'Shutting down gracefully...')
125
125
 
126
126
  # stop listening
127
127
  close_all_server_fds
data/lib/tp2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module TP2
2
- VERSION = '0.13.4'
2
+ VERSION = '0.14.1'
3
3
  end
data/lib/tp2.rb CHANGED
@@ -19,7 +19,7 @@ module TP2
19
19
  " o\n" +
20
20
  " \\|/ TP2 - a modern web server for Ruby apps\n" +
21
21
  " / \\ \n" +
22
- " / \\ https://github.com/noteflakes/tp2\n" +
22
+ " / x \\ https://github.com/noteflakes/tp2\n" +
23
23
  "⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺⎺\n"
24
24
  )
25
25
 
@@ -37,7 +37,7 @@ module TP2
37
37
  machine = opts[:machine] || UM.new
38
38
  machine.puts(opts[:banner]) if opts[:banner]
39
39
 
40
- opts[:logger]&.call("Running TP2 #{TP2::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
40
+ opts[:logger]&.info(message: "Running TP2 #{TP2::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
41
41
 
42
42
  server = Server.new(machine, opts, &app)
43
43
 
data/test/test_server.rb CHANGED
@@ -247,17 +247,37 @@ class ServerTest < Minitest::Test
247
247
  assert_equal 'barbaz', body
248
248
  end
249
249
 
250
+ class TestLogger
251
+ attr_reader :entries
252
+
253
+ def initialize
254
+ @entries = []
255
+ end
256
+
257
+ def info(o)
258
+ @entries << o.merge(level: :INFO)
259
+ end
260
+
261
+ def error(o)
262
+ @entries << o.merge(level: :ERROR)
263
+ end
264
+ end
265
+
250
266
  def test_logging
251
267
  skip
252
268
  reqs = []
253
- @opts[:logger] = ->(req, _h) { reqs << req }
254
- @app = ->(req) { req.respond('Hello, world!', {}) }
269
+ @opts[:logger] = TestLogger.new
270
+ @app = ->(req) { reqs << req; req.respond('Hello, world!', {}) }
255
271
 
256
272
  write_http_request "GET / HTTP/1.0\r\n\r\n"
257
273
  response = read_client_side
258
274
  expected = "HTTP/1.1 200\r\nContent-Length: 13\r\n\r\nHello, world!"
259
275
  assert_equal(expected, response)
260
276
 
277
+ entries = @opts[:logger].entries
278
+ assert_equal 1, entries.size
261
279
  assert_equal 1, reqs.size
280
+
281
+ assert_equal reqs.first, entries.first[:request]
262
282
  end
263
283
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tp2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.4
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner