about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--.olddoc.yml21
-rwxr-xr-xGIT-VERSION-GEN2
-rw-r--r--GNUmakefile2
-rw-r--r--README46
-rw-r--r--archive/rfmig.rb32
-rw-r--r--clogger.gemspec11
-rw-r--r--ext/clogger_ext/clogger.c96
-rw-r--r--lib/clogger.rb11
-rw-r--r--lib/clogger/pure.rb31
-rw-r--r--pkg.mk5
-rw-r--r--test/test_clogger.rb102
-rw-r--r--test/test_clogger_to_path.rb14
12 files changed, 248 insertions, 125 deletions
diff --git a/.olddoc.yml b/.olddoc.yml
index fd7b52d..2ed15c3 100644
--- a/.olddoc.yml
+++ b/.olddoc.yml
@@ -1,12 +1,15 @@
 ---
-rdoc_url: https://bogomips.org/clogger/
-cgit_url: https://bogomips.org/clogger.git
-git_url: git://bogomips.org/clogger.git
-public_email: clogger-public@bogomips.org
+rdoc_url: https://YHBT.net/clogger/
+cgit_url: https://YHBT.net/clogger.git
+git_url: https://YHBT.net/clogger.git
+public_email: clogger-public@YHBT.net
 ml_url:
-- https://bogomips.org/clogger-public/
-- http://ou63pmih66umazou.onion/clogger-public/
-- nntp://news.public-inbox.org/inbox.comp.lang.ruby.clogger
+- https://YHBT.net/clogger-public/
+- http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/clogger-public/
+- imaps://YHBT.net/inbox.comp.lang.ruby.clogger.0
+- imap://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.clogger.0
+- nntps://news.public-inbox.org/inbox.comp.lang.ruby.clogger
+- nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.clogger
 source_code:
-- git clone git://bogomips.org/clogger.git
-- git clone https://bogomips.org/clogger.git
+- git clone https://YHBT.net/clogger.git
+- torsocks git clone http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/clogger.git
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 49932c7..8fc5dc1 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 GVF=GIT-VERSION-FILE
-DEF_VER=v2.1.0
+DEF_VER=v2.4.0
 
 LF='
 '
diff --git a/GNUmakefile b/GNUmakefile
index ab8b4fe..b75e10c 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -1,5 +1,5 @@
 all::
-RSYNC_DEST := bogomips.org:/srv/bogomips/clogger/
+RSYNC_DEST := YHBT.net:/srv/yhbt/clogger/
 rfpackage := clogger
 include pkg.mk
 test-ext:
diff --git a/README b/README
index 890ba78..2bbe9e7 100644
--- a/README
+++ b/README
@@ -69,10 +69,12 @@ that receives a "<<" method:
 * $request_uri - the URI requested ($path_info?$query_string)
 * $request - the first line of the HTTP request
   ($request_method $request_uri $http_version)
-* $request_time, $request_time{PRECISION} - time taken for request
-  (including response body iteration).  PRECISION defaults to 3
-  (milliseconds) if not specified but may be specified anywhere from
-  0(seconds) to 6(microseconds).
+* $request_time, $request_time{PRECISION}, $request_time{POWER,PRECISION} -
+  time taken for request (including response body iteration). PRECISION
+  defaults to 3 (milliseconds) if not specified but may be specified
+  anywhere from 0(seconds) to 6(microseconds). POWER will raise the time to
+  the provided power of 10, useful for converting to micro- or nanoseconds.
+  POWER defaults to 0 if not specified but may be specified any from 0 to 9
 * $time_iso8601 - current local time in ISO 8601 format,
   e.g. "1970-01-01T00:00:00+00:00"
 * $time_local - current local time in Apache log format,
@@ -87,7 +89,7 @@ that receives a "<<" method:
 * $ip - X-Forwarded-For request header if available, $remote_addr if not
 * $pid - process ID of the current process
 * $e{Thread.current} - Thread processing the request
-* $e{Actor.current} - Actor processing the request (Revactor or Rubinius)
+* $e{Fiber.current} - Fiber processing the request
 * $env{variable_name} - any Rack environment variable (e.g. rack.url_scheme)
 
 == REQUIREMENTS
@@ -98,28 +100,40 @@ that receives a "<<" method:
 
 The latest development happens in git and is published to the following:
 
-   git clone git://bogomips.org/clogger.git
-   git clone git://repo.or.cz/clogger.git
+   git clone https://YHBT.net/clogger.git
+   git clone https://repo.or.cz/clogger.git
 
 You may also browse and download snapshot tarballs:
 
-* https://bogomips.org/clogger.git
-* http://repo.or.cz/w/clogger.git (gitweb)
+* https://YHBT.net/clogger.git
+* https://repo.or.cz/w/clogger.git (gitweb)
 
-The mailing list (see below) is central for coordination and
-development.  Patches should always be sent inline
-(git format-patch -M + git send-email) so we can reply to them inline.
+We use email for coordination and development, see below:
 
 == CONTACT
 
 All feedback (bug reports, user/development discussion, patches, pull
-requests) go to the public mailing list.
+requests) is done via publicly-archived email:
 
-* mailto:clogger-public@bogomips.org
+* https://YHBT.net/clogger-public/
+* imaps://YHBT.net/inbox.comp.lang.ruby.clogger.0
+* nntps://news.public-inbox.org/inbox.comp.lang.ruby.clogger
 
-Do not send HTML mail or attachments.  Do not top post.
+Tor users may also access HTTP, IMAP, and NNTP archives via .onion:
 
-Homepage: https://bogomips.org/clogger/
+* http://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/clogger-public/
+* imap://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.clogger.0
+* nntp://7fh6tueqddpjyxjmgtdiueylzoqt6pt7hec3pukyptlmohoowvhde4yd.onion/inbox.comp.lang.ruby.clogger
+
+AUTH=ANONYMOUS is supported for IMAP and IMAPS, and any
+username + password will work.
+
+No subscription or real names will ever be required to email us.
+Do not send HTML email, do not top post.
+
+* mailto:clogger-public@YHBT.net
+
+Homepage: https://YHBT.net/clogger/
 
 == INSTALL
 
diff --git a/archive/rfmig.rb b/archive/rfmig.rb
deleted file mode 100644
index 033051a..0000000
--- a/archive/rfmig.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env ruby
-require 'find'
-require 'fileutils'
-rfdir = 'rubyforge.org:/var/www/gforge-projects/clogger/'
-newbase = 'http://clogger.bogomips.org/'
-refresh = '<meta http-equiv="refresh" content="0; url=%s" />'
-old = 'rf.old'
-new = 'rf.new'
-cmd = %W(rsync -av #{rfdir} #{old}/)
-unless File.directory?(old)
-  system(*cmd) or abort "#{cmd.inspect} failed: #$?"
-end
-
-Find.find(old) do |path|
-  path =~ /\.html\z/ or next
-  data = File.read(path)
-  tmp = path.split(%r{/})
-  tmp.shift == old or abort "BUG"
-  dst = "#{new}/#{tmp.join('/')}"
-
-  tmp[-1] = '' if tmp[-1] == "index.html"
-  url = "#{newbase}#{tmp.join('/')}"
-  meta = sprintf(refresh, url)
-  data.sub!(/(<head[^>]*>)/i, "#$1#{meta}")
-  data.sub!(/(<body[^>]*>)/i,
-            "#{$1}Redirecting to <a href=\"#{url}\">#{url}</a> ...<br/>")
-  FileUtils.mkdir_p(File.dirname(dst))
-  File.open(dst, "w") { |fp| fp.write(data) }
-end
-
-print "Verify results in #{new}/, then run:\n  "
-puts %W(rsync -av #{new}/ #{rfdir}).join(' ')
diff --git a/clogger.gemspec b/clogger.gemspec
index ebc0baf..7a08dc5 100644
--- a/clogger.gemspec
+++ b/clogger.gemspec
@@ -3,12 +3,12 @@ manifest = File.exist?('.manifest') ?
 
 Gem::Specification.new do |s|
   s.name = %q{clogger}
-  s.version = (ENV['VERSION'] || '2.1.0').dup
-  s.homepage = 'https://bogomips.org/clogger/'
+  s.version = (ENV['VERSION'] || '2.4.0').dup
+  s.homepage = 'https://YHBT.net/clogger/'
   s.authors = ["cloggers"]
   s.summary = 'configurable request logging for Rack'
   s.description = File.read('README').split("\n\n")[1]
-  s.email = %q{clogger-public@bogomips.org}
+  s.email = %q{clogger-public@YHBT.net}
   s.extra_rdoc_files = IO.readlines('.document').map!(&:chomp!).keep_if do |f|
     File.exist?(f)
   end
@@ -16,8 +16,9 @@ Gem::Specification.new do |s|
   s.test_files = %w(test/test_clogger.rb test/test_clogger_to_path.rb)
 
   # HeaderHash wasn't case-insensitive in old versions
-  s.add_dependency(%q<rack>, ['>= 1.0', '< 3.0'])
+  s.add_dependency(%q<rack>, ['>= 1.0', '< 4.0'])
   s.add_development_dependency('test-unit', '~> 3.0')
+  s.add_development_dependency('rackup', '~> 2.0')
   s.extensions = %w(ext/clogger_ext/extconf.rb)
-  s.licenses = %w(LGPL-2.1+)
+  s.licenses = %w(LGPL-2.1-or-later)
 end
diff --git a/ext/clogger_ext/clogger.c b/ext/clogger_ext/clogger.c
index fdc23e3..cea4072 100644
--- a/ext/clogger_ext/clogger.c
+++ b/ext/clogger_ext/clogger.c
@@ -1,3 +1,6 @@
+/* quiet deprecation for _BSD_SOURCE and _SVID_SOURCE: */
+#define _DEFAULT_SOURCE 1
+
 #include <ruby.h>
 #ifdef HAVE_RUBY_IO_H
 #  include <ruby/io.h>
@@ -14,7 +17,7 @@
 #  include <fcntl.h>
 #endif
 #ifndef _POSIX_C_SOURCE
-#  define _POSIX_C_SOURCE 200112L
+#  define _POSIX_C_SOURCE 200809L
 #endif
 #include <time.h>
 #include <stdlib.h>
@@ -133,7 +136,7 @@ static ID to_path_id;
 static ID respond_to_id;
 static VALUE cClogger;
 static VALUE mFormat;
-static VALUE cHeaderHash;
+static VALUE cRackHeaders;
 
 /* common hash lookup keys */
 static VALUE g_HTTP_X_FORWARDED_FOR;
@@ -169,6 +172,10 @@ static VALUE byte_xs(VALUE obj)
 {
         static const char esc[] = "0123456789ABCDEF";
         unsigned char *new_ptr;
+        if (rb_obj_is_kind_of(obj, rb_cArray)) {
+                // Rack 3
+                obj = rb_ary_join(obj, rb_str_new2("\n"));
+        }
         VALUE from = rb_obj_as_string(obj);
         const unsigned char *ptr = (const unsigned char *)RSTRING_PTR(from);
         long len = RSTRING_LEN(from);
@@ -222,18 +229,28 @@ static void clogger_mark(void *ptr)
         rb_gc_mark(c->body);
 }
 
+static size_t memsize(const void *ptr)
+{
+        return sizeof(struct clogger);
+}
+
+static const rb_data_type_t clogger_type = {
+        "clogger",
+        { clogger_mark, RUBY_TYPED_DEFAULT_FREE, memsize, /* reserved */ },
+};
+
 static VALUE clogger_alloc(VALUE klass)
 {
         struct clogger *c;
 
-        return Data_Make_Struct(klass, struct clogger, clogger_mark, -1, c);
+        return TypedData_Make_Struct(klass, struct clogger, &clogger_type, c);
 }
 
 static struct clogger *clogger_get(VALUE self)
 {
         struct clogger *c;
 
-        Data_Get_Struct(self, struct clogger, c);
+        TypedData_Get_Struct(self, struct clogger, &clogger_type, c);
         assert(c);
         return c;
 }
@@ -374,12 +391,39 @@ static void append_ts(struct clogger *c, VALUE op, struct timespec *ts)
         rb_str_buf_cat(c->log_buf, buf, nr);
 }
 
+#define NANO_PER_SEC (1000 * 1000 * 1000)
 static void append_request_time_fmt(struct clogger *c, VALUE op)
 {
         struct timespec now;
+        unsigned long ipow = NUM2ULONG(rb_ary_entry(op, 3));
 
         clock_gettime(hopefully_CLOCK_MONOTONIC, &now);
         clock_diff(&now, &c->ts_start);
+        if (ipow) {
+                unsigned long adj = 1;
+                int64_t now_sec = now.tv_sec, now_nsec = now.tv_nsec,
+                        prev_sec = now.tv_sec;
+
+                do { adj *= 10; } while (--ipow);
+                now_sec *= adj;
+                now_nsec *= adj;
+                if (now_nsec >= NANO_PER_SEC) {
+                        int64_t add = now_nsec / NANO_PER_SEC;
+                        now_sec += add;
+                        now_nsec %= NANO_PER_SEC;
+                }
+                if (now_sec < prev_sec) { /* overflowed */
+                        now_nsec = NANO_PER_SEC - 1;
+                        /*
+                          * some platforms may use unsigned .tv_sec, but
+                          * they're not worth supporting, so keep unsigned:
+                          */
+                        now_sec = (time_t)(sizeof(now.tv_sec) == 4 ?
+                                     INT_MAX : LONG_MAX);
+                }
+                now.tv_sec = now_sec;
+                now.tv_nsec = now_nsec;
+        }
         append_ts(c, op, &now);
 }
 
@@ -416,10 +460,11 @@ static void append_request(struct clogger *c)
 {
         VALUE tmp;
 
-        /* REQUEST_METHOD doesn't need escaping, Rack::Lint governs it */
         tmp = rb_hash_aref(c->env, g_REQUEST_METHOD);
-        if (!NIL_P(tmp))
+        if (!NIL_P(tmp)) {
+                tmp = byte_xs(tmp);
                 rb_str_buf_append(c->log_buf, tmp);
+        }
 
         rb_str_buf_append(c->log_buf, g_space);
 
@@ -444,9 +489,21 @@ static void append_request_length(struct clogger *c)
         }
 }
 
+/*
+ * time(2) may slip backwards, so use CLOCK_REALTIME for accuracy
+ * https://lore.kernel.org/git/20230319064353.686226-3-eggert@cs.ucla.edu/T/
+ */
+static time_t cur_time(void)
+{
+        struct timespec now;
+
+        (void)clock_gettime(CLOCK_REALTIME, &now);
+        return now.tv_sec;
+}
+
 static long local_gmtoffset(struct tm *tm)
 {
-        time_t t = time(NULL);
+        time_t t = cur_time();
 
         tzset();
         localtime_r(&t, tm);
@@ -506,7 +563,7 @@ static void append_time_utc(struct clogger *c)
         char buf[sizeof("01/Jan/1970:00:00:00 +0000")];
         struct tm tm;
         int nr;
-        time_t t = time(NULL);
+        time_t t = cur_time();
 
         gmtime_r(&t, &tm);
         nr = snprintf(buf, sizeof(buf),
@@ -525,7 +582,7 @@ append_time(struct clogger *c, enum clogger_opcode op, VALUE fmt, VALUE buf)
         size_t buf_size = RSTRING_LEN(buf) + 1; /* "\0" */
         size_t nr;
         struct tm tmp;
-        time_t t = time(NULL);
+        time_t t = cur_time();
 
         if (op == CL_OP_TIME_LOCAL)
                 localtime_r(&t, &tmp);
@@ -585,7 +642,8 @@ static void append_response(struct clogger *c, VALUE key)
 {
         VALUE v;
 
-        assert(rb_obj_is_kind_of(c->headers, cHeaderHash) && "not HeaderHash");
+        assert(rb_obj_is_kind_of(c->headers, cRackHeaders)
+                && "not Rack::Headers");
 
         v = rb_funcall(c->headers, sq_brace_id, 1, key);
         v = NIL_P(v) ? g_dash : byte_xs(v);
@@ -857,10 +915,9 @@ static VALUE ccall(struct clogger *c, VALUE env)
 
                 rv = rb_ary_dup(rv);
                 if (c->need_resp &&
-                    ! rb_obj_is_kind_of(c->headers, cHeaderHash)) {
-                        c->headers = rb_funcall(cHeaderHash, new_id, 1,
-                                                c->headers);
-                        rb_ary_store(rv, 1, c->headers);
+                    ! rb_obj_is_kind_of(c->headers, cRackHeaders)) {
+                        c->headers = rb_funcall(cRackHeaders, sq_brace_id,
+                                                1, c->headers);
                 }
         } else {
                 VALUE tmp = rb_inspect(rv);
@@ -1071,8 +1128,15 @@ void Init_clogger_ext(void)
         CONST_GLOBAL_STR2(rack_request_cookie_hash, "rack.request.cookie_hash");
 
         tmp = rb_const_get(rb_cObject, rb_intern("Rack"));
-        tmp = rb_const_get(tmp, rb_intern("Utils"));
-        cHeaderHash = rb_const_get(tmp, rb_intern("HeaderHash"));
+        if (rb_const_defined(tmp, rb_intern("Headers"))) {
+                // Rack >= 3.0
+                cRackHeaders = rb_const_get(tmp, rb_intern("Headers"));
+        } else {
+                // Rack < 3.0
+                tmp = rb_const_get(tmp, rb_intern("Utils"));
+                cRackHeaders = rb_const_get(tmp, rb_intern("HeaderHash"));
+        }
+        rb_ary_push(mark_ary, cRackHeaders);
 
         rb_obj_freeze(mark_ary);
 }
diff --git a/lib/clogger.rb b/lib/clogger.rb
index be1bdce..7ce2b24 100644
--- a/lib/clogger.rb
+++ b/lib/clogger.rb
@@ -51,7 +51,7 @@ private
 
   SCAN = /([^$]*)(\$+(?:env\{\w+(?:\.[\w\.]+)?\}|
                         e\{[^\}]+\}|
-                        (?:request_)?time\{\d+\}|
+                        (?:request_)?time\{\d+(?:,\d+)?\}|
                         time_(?:utc|local)\{[^\}]+\}|
                         \w*))?([^$]*)/x
 
@@ -92,7 +92,14 @@ private
         when /\A\$time\{(\d+)\}\z/
           rv << [ OP_TIME, *usec_conv_pair(tok, $1.to_i) ]
         when /\A\$request_time\{(\d+)\}\z/
-          rv << [ OP_REQUEST_TIME, *usec_conv_pair(tok, $1.to_i) ]
+          rv << [ OP_REQUEST_TIME, *usec_conv_pair(tok, $1.to_i), 0 ]
+        when /\A\$request_time\{(\d+),(\d+)\}\z/
+          ipow = $1.to_i
+          prec = $2.to_i
+          if ipow > 9 # nanosecond precision is the highest POSIX goes
+            raise ArgumentError, "#{tok}: too big: #{ipow} (max=9)"
+          end
+          rv << [ OP_REQUEST_TIME, *usec_conv_pair(tok, prec), ipow ]
         else
           tok_sym = tok[1..-1].to_sym
           if special_code = SPECIAL_VARS[tok_sym]
diff --git a/lib/clogger/pure.rb b/lib/clogger/pure.rb
index fddfe79..156f11e 100644
--- a/lib/clogger/pure.rb
+++ b/lib/clogger/pure.rb
@@ -8,10 +8,15 @@ class Clogger
   attr_accessor :env, :status, :headers, :body
   attr_writer :body_bytes_sent, :start
 
-  def initialize(app, opts = {})
-    # trigger autoload to avoid thread-safety issues later on
+  RackHeaders = if Object.const_defined?("Rack::Headers")
+    # Rack >= 3.0
+    Rack::Headers
+  else
+    # Rack < 3.0
     Rack::Utils::HeaderHash
+  end
 
+  def initialize(app, opts = {})
     @app = app
     @logger = opts[:logger]
     path = opts[:path]
@@ -35,7 +40,7 @@ class Clogger
       raise TypeError, "app response not a 3 element Array: #{resp.inspect}"
     end
     status, headers, body = resp
-    headers = Rack::Utils::HeaderHash.new(headers) if @need_resp
+    headers = RackHeaders[headers] if @need_resp
     if @wrap_body
       @reentrant = env['rack.multithread'] if @reentrant.nil?
       wbody = @reentrant ? self.dup : self
@@ -91,6 +96,9 @@ private
 
   def byte_xs(s)
     s = s.dup
+    if s.is_a?(Array)
+      s = s.join("\n")
+    end
     s.force_encoding(Encoding::BINARY) if defined?(Encoding::BINARY)
     s.gsub!(/(['"\x00-\x1f\x7f-\xff])/) do |x|
       "\\x#{$1.unpack('H2').first.upcase}"
@@ -118,8 +126,7 @@ private
       version = env['HTTP_VERSION'] and version = " #{byte_xs(version)}"
       qs = env['QUERY_STRING']
       qs.empty? or qs = "?#{byte_xs(qs)}"
-      "#{env['REQUEST_METHOD']} " \
-        "#{request_uri(env)}#{version}"
+      "#{byte_xs(env['REQUEST_METHOD'] || '')} #{request_uri(env)}#{version}"
     when :request_uri
       request_uri(env)
     when :request_length
@@ -134,17 +141,10 @@ private
     when :time_iso8601
       Time.now.iso8601
     when :time_local
-      t = Time.now
-      off = t.utc_offset
-      sign = off < 0 ? '-' : '+'
-      sprintf("%02d/%s/%d:%02d:%02d:%02d #{sign}%02d%02d",
-              t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
-              t.year, t.hour, t.min, t.sec, *(off.abs / 60).divmod(60))
+      # %b in Ruby is locale-independent, unlike strftime(3) in C
+      Time.now.strftime('%d/%b/%Y:%H:%M:%S %z')
     when :time_utc
-      t = Time.now.utc
-      sprintf("%02d/%s/%d:%02d:%02d:%02d +0000",
-              t.mday, Time::RFC2822_MONTH_NAME[t.mon - 1],
-              t.year, t.hour, t.min, t.sec)
+      Time.now.utc.strftime('%d/%b/%Y:%H:%M:%S %z')
     else
       raise "EDOOFUS #{special_nr}"
     end
@@ -166,6 +166,7 @@ private
       when OP_TIME_UTC; Time.now.utc.strftime(op[1])
       when OP_REQUEST_TIME
         t = mono_now - start
+        t = t * (10 ** op[3])
         time_format(t.to_i, (t - t.to_i) * 1000000, op[1], op[2])
       when OP_TIME
         t = Time.now
diff --git a/pkg.mk b/pkg.mk
index 1e40915..1f1b8ed 100644
--- a/pkg.mk
+++ b/pkg.mk
@@ -60,7 +60,7 @@ doc:: .document .olddoc.yml $(pkg_extra) $(PLACEHOLDERS)
         -find lib -type f -name '*.rbc' -exec rm -f '{}' ';'
         -find ext -type f -name '*.rbc' -exec rm -f '{}' ';'
         $(RM) -r doc
-        $(RDOC) -f oldweb
+        $(RDOC) -f dark216
         $(OLDDOC) merge
         install -m644 COPYING doc/COPYING
         install -m644 NEWS doc/NEWS
@@ -127,7 +127,8 @@ publish_doc:
         -git set-file-times
         $(MAKE) doc
         $(MAKE) doc_gz
-        $(RSYNC) -av doc/ $(RSYNC_DEST)/
+        $(RSYNC) -av doc/ $(RSYNC_DEST)/ \
+                --exclude index.html* --exclude created.rid*
         git ls-files | xargs touch
 endif
 
diff --git a/test/test_clogger.rb b/test/test_clogger.rb
index ca3bd55..5646025 100644
--- a/test/test_clogger.rb
+++ b/test/test_clogger.rb
@@ -10,6 +10,16 @@ require "rack"
 
 require "clogger"
 
+IS_RACK3 = Gem::Version.new(Rack.release) >= Gem::Version.new('3.0.0')
+
+RackHeaders = if Object.const_defined?("Rack::Headers")
+  # Rack >= 3.0
+  Rack::Headers
+else
+  # Rack < 3.0
+  Rack::Utils::HeaderHash
+end
+
 # used to test subclasses
 class FooString < String
 end
@@ -22,6 +32,7 @@ class TestClogger < Test::Unit::TestCase
     @nginx_fmt = "%d/%b/%Y:%H:%M:%S %z"
     @req = {
       "REQUEST_METHOD" => "GET",
+      "SERVER_PROTOCOL" => "HTTP/1.0",
       "HTTP_VERSION" => "HTTP/1.0",
       "HTTP_USER_AGENT" => 'echo and socat \o/',
       "PATH_INFO" => "/hello",
@@ -85,13 +96,13 @@ class TestClogger < Test::Unit::TestCase
   def test_clen_stringio
     start = DateTime.now - 1
     str = StringIO.new
-    app = lambda { |env| [ 301, {'Content-Length' => '5'}, ['abcde'] ] }
+    app = lambda { |env| [ 301, {'content-length' => '5'}, ['abcde'] ] }
     format = Common.dup
     assert format.gsub!(/response_length/, 'sent_http_content_length')
     cl = Clogger.new(app, :logger => str, :format => format)
     status, headers, body = cl.call(@req)
     assert_equal(301, status)
-    assert_equal({'Content-Length' => '5'}, headers)
+    assert_equal({'content-length' => '5'}, headers)
     body.each { |part| assert_equal('abcde', part) }
     str = str.string
     r = %r{\Ahome - - \[[^\]]+\] "GET /hello\?goodbye=true HTTP/1.0" 301 5\n\z}
@@ -167,10 +178,11 @@ class TestClogger < Test::Unit::TestCase
       ary = compile_format(
         '$remote_addr - $remote_user [$time_local] ' \
         '"$request" $status $body_bytes_sent "$http_referer" ' \
-        '"$http_user_agent" "$http_cookie" $request_time ' \
+        '"$http_user_agent" "$http_cookie" $request_time $request_time{6,0} ' \
         '$env{rack.url_scheme}' \
         "\n")
     }
+
     expect = [
       [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
       [ Clogger::OP_LITERAL, " - " ],
@@ -190,7 +202,9 @@ class TestClogger < Test::Unit::TestCase
       [ Clogger::OP_LITERAL, "\" \"" ],
       [ Clogger::OP_REQUEST, "HTTP_COOKIE" ],
       [ Clogger::OP_LITERAL, "\" " ],
-      [ Clogger::OP_REQUEST_TIME, '%d.%03d', 1000 ],
+      [ Clogger::OP_REQUEST_TIME, '%d.%03d', 1000, 0],
+      [ Clogger::OP_LITERAL, " " ],
+      [ Clogger::OP_REQUEST_TIME, '%d', 1, 6],
       [ Clogger::OP_LITERAL, " " ],
       [ Clogger::OP_REQUEST, "rack.url_scheme" ],
       [ Clogger::OP_LITERAL, "\n" ],
@@ -209,6 +223,22 @@ class TestClogger < Test::Unit::TestCase
     assert_equal "-#{current}-\n", str.string
   end
 
+  def test_fiber
+    begin
+      current = Fiber.current.to_s
+    rescue NameError => e
+      warn "your Ruby does not support fibers #{e}"
+      return
+    end
+    str = StringIO.new
+    app = lambda { |env| [ 302, {}, [] ] }
+    cl = Clogger.new(app,
+                    :logger => str,
+                    :format => "-$e{Fiber.current}-\n")
+    status, headers, body = cl.call(@req)
+    assert_equal "-#{current}-\n", str.string
+  end
+
   def test_pid
     str = StringIO.new
     app = lambda { |env| [ 302, {}, [] ] }
@@ -234,7 +264,7 @@ class TestClogger < Test::Unit::TestCase
   def test_rack_1_0
     start = DateTime.now - 1
     str = StringIO.new
-    app = lambda { |env| [ 200, {'Content-Length'=>'0'}, %w(a b c)] }
+    app = lambda { |env| [ 200, {'content-length'=>'0'}, %w(a b c)] }
     cl = Clogger.new(app, :logger => str, :format => Rack_1_0)
     status, headers, body = cl.call(@req)
     tmp = []
@@ -325,7 +355,7 @@ class TestClogger < Test::Unit::TestCase
   def test_combined
     start = DateTime.now - 1
     str = StringIO.new
-    app = lambda { |env| [ 200, {'Content-Length'=>'3'}, %w(a b c)] }
+    app = lambda { |env| [ 200, {'content-length'=>'3'}, %w(a b c)] }
     cl = Clogger.new(app, :logger => str, :format => Combined)
     status, headers, body = cl.call(@req)
     tmp = []
@@ -346,7 +376,7 @@ class TestClogger < Test::Unit::TestCase
 
   def test_rack_errors_fallback
     err = StringIO.new
-    app = lambda { |env| [ 200, {'Content-Length'=>'3'}, %w(a b c)] }
+    app = lambda { |env| [ 200, {'content-length'=>'3'}, %w(a b c)] }
     cl = Clogger.new(app, :format => '$pid')
     req = @req.merge('rack.errors' => err)
     status, headers, body = cl.call(req)
@@ -355,7 +385,7 @@ class TestClogger < Test::Unit::TestCase
 
   def test_body_close
     s_body = StringIO.new(%w(a b c).join("\n"))
-    app = lambda { |env| [ 200, {'Content-Length'=>'5'}, s_body] }
+    app = lambda { |env| [ 200, {'content-length'=>'5'}, s_body] }
     cl = Clogger.new(app, :logger => [], :format => '$pid')
     status, headers, body = cl.call(@req)
     assert ! s_body.closed?
@@ -365,7 +395,7 @@ class TestClogger < Test::Unit::TestCase
 
   def test_escape
     str = StringIO.new
-    app = lambda { |env| [ 200, {'Content-Length'=>'5'}, [] ] }
+    app = lambda { |env| [ 200, {'content-length'=>'5'}, [] ] }
     cl = Clogger.new(app,
       :logger => str,
       :format => '$http_user_agent "$request"')
@@ -379,13 +409,21 @@ class TestClogger < Test::Unit::TestCase
     assert_equal expect, str.string
   end
 
-  # rack allows repeated headers with "\n":
-  # { 'Set-Cookie' => "a\nb" } =>
-  #   Set-Cookie: a
-  #   Set-Cookie: b
-  def test_escape_header_newlines
-    str = StringIO.new
-    app = lambda { |env| [302, { 'Set-Cookie' => "a\nb" }, [] ] }
+  # rack >= 3 allows repeated headers with array:
+  # { 'set-cookie' => ["a","b"] } =>
+  #   set-cookie: a
+  #   set-cookie: b
+  # rack < 3 allows repeated headers with "\n":
+  # { 'set-cookie' => "a\nb" } =>
+  #   set-cookie: a
+  #   set-cookie: b
+  def test_multiheader
+    str = StringIO.new
+    if IS_RACK3 then
+      app = lambda { |env| [302, { 'set-cookie' => ["a","b"] }, [] ] }
+    else
+      app = lambda { |env| [302, { 'set-cookie' => "a\nb" }, [] ] }
+    end
     cl = Clogger.new(app, :logger => str, :format => '$sent_http_set_cookie')
     cl.call(@req)
     assert_equal "a\\x0Ab\n", str.string
@@ -444,12 +482,16 @@ class TestClogger < Test::Unit::TestCase
     str = StringIO.new
     app = lambda { |env| [302, [ %w(a) ], []] }
     cl = Clogger.new(app, :logger => str, :format => '$sent_http_set_cookie')
-    assert_nothing_raised { cl.call(@req) }
+    if IS_RACK3 then
+      assert_raise(ArgumentError) { cl.call(@req) }
+    else
+      assert_nothing_raised { cl.call(@req) }
+    end
   end
 
   def test_subclass_hash
     str = StringIO.new
-    req = Rack::Utils::HeaderHash.new(@req)
+    req = RackHeaders[@req]
     app = lambda { |env| [302, [ %w(a) ], []] }
     cl = Clogger.new(app, :logger => str, :format => Rack_1_0)
     assert_nothing_raised { cl.call(req).last.each {}.close }
@@ -721,6 +763,24 @@ class TestClogger < Test::Unit::TestCase
     assert s[-1].to_f <= 0.110
   end
 
+  def test_request_time_with_multiplier
+    s = []
+    app = lambda { |env| sleep(0.1) ; [302, [], [] ] }
+    cl = Clogger.new(app, :logger => s, :format => "$request_time{9,0}")
+    status, headers, body = cl.call(@req)
+    assert_nothing_raised { body.each { |x| } ; body.close }
+    assert s[-1].to_f >= 100000000
+    assert s[-1].to_f <= 300000000
+  end
+
+  def test_request_time_with_invalid_multiplier
+    s = []
+    app = lambda { |env| sleep(0.1) ; [302, [], [] ] }
+    assert_raise(ArgumentError) {
+      cl = Clogger.new(app, :logger => s, :format => "$request_time{10,0}")
+    }
+  end
+
   def test_insanely_long_time_format
     s = []
     app = lambda { |env| [200, [], [] ] }
@@ -840,19 +900,21 @@ class TestClogger < Test::Unit::TestCase
   end
 
   def test_lint_error_wrapper
-    require 'rack/lobster'
+    require 'rackup/lobster'
     @req["SERVER_NAME"] = "FOO"
     @req["SERVER_PORT"] = "666"
     @req["rack.version"] = [1,1]
     @req["rack.multithread"] = true
     @req["rack.multiprocess"] = true
     @req["rack.run_once"] = false
-    app = Rack::ContentLength.new(Rack::ContentType.new(Rack::Lobster.new))
+    app = Rack::ContentLength.new(Rack::ContentType.new(Rackup::Lobster.new))
     cl = Clogger.new(app, :format => :Combined)
     @req["rack.errors"] = err = StringIO.new
     status, headers, body = r = Rack::Lint.new(cl).call(@req)
     body.each { |x| assert_kind_of String, x.to_str }
     body.close # might raise here
     assert_match(%r{GET /hello}, err.string)
+  rescue LoadError, Gem::ConflictError
+    # This test only works on Rack >= 3.0
   end
 end
diff --git a/test/test_clogger_to_path.rb b/test/test_clogger_to_path.rb
index f74b991..4cc1738 100644
--- a/test/test_clogger_to_path.rb
+++ b/test/test_clogger_to_path.rb
@@ -3,6 +3,7 @@ $stderr.sync = $stdout.sync = true
 require "test/unit"
 require "date"
 require "stringio"
+require "tempfile"
 require "rack"
 require "clogger"
 
@@ -26,6 +27,7 @@ class TestCloggerToPath < Test::Unit::TestCase
     @req = {
       "REQUEST_METHOD" => "GET",
       "HTTP_VERSION" => "HTTP/1.0",
+      "SERVER_PROTOCOL" => "HTTP/1.0",
       "HTTP_USER_AGENT" => 'echo and socat \o/',
       "PATH_INFO" => "/",
       "QUERY_STRING" => "",
@@ -50,8 +52,8 @@ class TestCloggerToPath < Test::Unit::TestCase
     app = Rack::Builder.new do
       tmp.syswrite(' ' * 365)
       h = {
-        'Content-Length' => '0',
-        'Content-Type' => 'text/plain',
+        'content-length' => '0',
+        'content-type' => 'text/plain',
       }
       use Clogger,
         :logger => logger,
@@ -82,8 +84,8 @@ class TestCloggerToPath < Test::Unit::TestCase
     app = Rack::Builder.new do
       tmp.syswrite(' ' * 365)
       h = {
-        'Content-Length' => '0',
-        'Content-Type' => 'text/plain',
+        'content-length' => '0',
+        'content-type' => 'text/plain',
       }
       use Clogger,
         :logger => logger,
@@ -106,8 +108,8 @@ class TestCloggerToPath < Test::Unit::TestCase
     logger = StringIO.new
     app = Rack::Builder.new do
       h = {
-        'Content-Length' => '3',
-        'Content-Type' => 'text/plain',
+        'content-length' => '3',
+        'content-type' => 'text/plain',
       }
       use Clogger,
         :logger => logger,