It can be sometimes interesting to have a timestamp with a
resolution of less than a second.
It is currently painful to obtain this, because concatenation
of date and date_us lead to a shorter timestamp during first
100ms of a second, which is not parseable and needs ugly ACLs
in configuration to prepend 0s when needed.
To improve this, add an optional <unit> parameter to date sample
to report an integer with desired unit.
Also support this unit in http_date converter to report
a date string with sub-second precision.
---
 Changed in V2: add a verification function to check date unit argument

 Changed in V3:
 - fix a comment typo (s/milli/micro/)
 - Talk about "seconds" unit in doc

 Changed in V4:
 - offset is now considered to have the same unit than optional
   unit param.
 - set args[1].data.str.area to NULL after freeing it.
 - also support unit in http_date converter.
 - improve documentation

 doc/configuration.txt  | 33 +++++++++++++++++++--------
 include/proto/sample.h |  1 +
 src/http_conv.c        | 49 +++++++++++++++++++++++++++++++--------
 src/sample.c           | 52 +++++++++++++++++++++++++++++++++++++++---
 4 files changed, 113 insertions(+), 22 deletions(-)

diff --git a/doc/configuration.txt b/doc/configuration.txt
index 77f95572..1c49dde3 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -13233,13 +13233,17 @@ hex2i
   Converts a hex string containing two hex digits per input byte to an
   integer. If the input value cannot be converted, then zero is returned.
 
-http_date([<offset>])
+http_date([<offset, unit>])
   Converts an integer supposed to contain a date since epoch to a string
   representing this date in a format suitable for use in HTTP header fields. If
-  an offset value is specified, then it is a number of seconds that is added to
-  the date before the conversion is operated. This is particularly useful to
-  emit Date header fields, Expires values in responses when combined with a
-  positive offset, or Last-Modified values when the offset is negative.
+  an offset value is specified, then it is added to the date before the
+  conversion is operated. This is particularly useful to emit Date header 
fields,
+  Expires values in responses when combined with a positive offset, or
+  Last-Modified values when the offset is negative.
+  If a unit value is specified, then consider the timestamp as either
+  "s" for seconds (default behavior), "ms" for milliseconds, or "us" for
+  microseconds since epoch. Offset is assumed to have the same unit as
+  input timestamp.
 
 in_table(<table>)
   Uses the string representation of the input sample to perform a look up in
@@ -14050,18 +14054,29 @@ cpu_ns_tot : integer
   high cpu_calls count, for example when processing many HTTP chunks, and for
   this reason it is often preferred to log cpu_ns_avg instead.
 
-date([<offset>]) : integer
+date([<offset>, <unit>]) : integer
   Returns the current date as the epoch (number of seconds since 01/01/1970).
-  If an offset value is specified, then it is a number of seconds that is added
-  to the current date before returning the value. This is particularly useful
-  to compute relative dates, as both positive and negative offsets are allowed.
+
+  If an offset value is specified, then it is added to the current date before
+  returning the value. This is particularly useful to compute relative dates,
+  as both positive and negative offsets are allowed.
   It is useful combined with the http_date converter.
 
+  <unit> is facultative, and can be set to "s" for seconds (default behavior),
+  "ms" for  milliseconds or "us" for microseconds.
+  If unit is set, return value is an integer reflecting either seconds,
+  milliseconds or microseconds since epoch, plus offset.
+  It is useful when a time resolution of less than a second is needed.
+
   Example :
 
      # set an expires header to now+1 hour in every response
      http-response set-header Expires %[date(3600),http_date]
 
+     # set an expires header to now+1 hour in every response, with
+     # millisecond granularity
+     http-response set-header Expires %[date(3600000,ms),http_date(0,ms)]
+
 date_us : integer
   Return the microseconds part of the date (the "second" part is returned by
   date sample). This sample is coherent with the date sample as it is comes
diff --git a/include/proto/sample.h b/include/proto/sample.h
index 606dcb62..f0be3fd2 100644
--- a/include/proto/sample.h
+++ b/include/proto/sample.h
@@ -45,6 +45,7 @@ struct sample_fetch *find_sample_fetch(const char *kw, int 
len);
 struct sample_fetch *sample_fetch_getnext(struct sample_fetch *current, int 
*idx);
 struct sample_conv *sample_conv_getnext(struct sample_conv *current, int *idx);
 int smp_resolve_args(struct proxy *p);
+int smp_check_date_unit(struct arg *args, char **err);
 int smp_expr_output_type(struct sample_expr *expr);
 int c_none(struct sample *smp);
 int smp_dup(struct sample *smp);
diff --git a/src/http_conv.c b/src/http_conv.c
index 93b748c2..cd93aa96 100644
--- a/src/http_conv.c
+++ b/src/http_conv.c
@@ -33,10 +33,17 @@
 #include <proto/sample.h>
 #include <proto/stream.h>
 
+static int smp_check_http_date_unit(struct arg *args, struct sample_conv *conv,
+                                    const char *file, int line, char **err)
+{
+    return smp_check_date_unit(args, err);
+}
 
 /* takes an UINT value on input supposed to represent the time since EPOCH,
  * adds an optional offset found in args[0] and emits a string representing
- * the date in RFC-1123/5322 format.
+ * the date in RFC-1123/5322 format. If optional unit param in args[1] is
+ * provided, decode timestamp in milliseconds ("ms") or microseconds("us"),
+ * and use relevant output date format.
  */
 static int sample_conv_http_date(const struct arg *args, struct sample *smp, 
void *private)
 {
@@ -44,23 +51,45 @@ static int sample_conv_http_date(const struct arg *args, 
struct sample *smp, voi
        const char mon[12][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
        struct buffer *temp;
        struct tm *tm;
-       /* With high numbers, the date returned can be negative, the 55 bits 
mask prevent this. */
-       time_t curr_date = smp->data.u.sint & 0x007fffffffffffffLL;
+       int sec_frac = 0;
+       time_t curr_date;
 
        /* add offset */
        if (args && (args[0].type == ARGT_SINT))
-               curr_date += args[0].data.sint;
+               smp->data.u.sint += args[0].data.sint;
+
+        /* report in milliseconds */
+        if (args && args[1].type == ARGT_SINT && args[1].data.sint == 
TIME_UNIT_MS) {
+               sec_frac = smp->data.u.sint % 1000;
+                smp->data.u.sint /= 1000;
+        }
+        /* report in microseconds */
+        else if (args && args[1].type == ARGT_SINT && args[1].data.sint == 
TIME_UNIT_US) {
+               sec_frac = smp->data.u.sint % 1000000;
+                smp->data.u.sint /= 1000000;
+        }
+
+       /* With high numbers, the date returned can be negative, the 55 bits 
mask prevent this. */
+       curr_date = smp->data.u.sint & 0x007fffffffffffffLL;
 
        tm = gmtime(&curr_date);
        if (!tm)
                return 0;
 
        temp = get_trash_chunk();
-       temp->data = snprintf(temp->area, temp->size - temp->data,
-                             "%s, %02d %s %04d %02d:%02d:%02d GMT",
-                             day[tm->tm_wday], tm->tm_mday, mon[tm->tm_mon],
-                             1900+tm->tm_year,
-                             tm->tm_hour, tm->tm_min, tm->tm_sec);
+       if (args && args[1].type == ARGT_SINT && args[1].data.sint != 
TIME_UNIT_S) {
+           temp->data = snprintf(temp->area, temp->size - temp->data,
+                                 "%s, %02d %s %04d %02d:%02d:%02d.%d GMT",
+                                 day[tm->tm_wday], tm->tm_mday, 
mon[tm->tm_mon],
+                                 1900+tm->tm_year,
+                                 tm->tm_hour, tm->tm_min, tm->tm_sec, 
sec_frac);
+       } else {
+           temp->data = snprintf(temp->area, temp->size - temp->data,
+                                 "%s, %02d %s %04d %02d:%02d:%02d GMT",
+                                 day[tm->tm_wday], tm->tm_mday, 
mon[tm->tm_mon],
+                                 1900+tm->tm_year,
+                                 tm->tm_hour, tm->tm_min, tm->tm_sec);
+        }
 
        smp->data.u.str = *temp;
        smp->data.type = SMP_T_STR;
@@ -328,7 +357,7 @@ static int smp_conv_res_capture(const struct arg *args, 
struct sample *smp, void
 
 /* Note: must not be declared <const> as its list will be overwritten */
 static struct sample_conv_kw_list sample_conv_kws = {ILH, {
-       { "http_date",      sample_conv_http_date,    ARG1(0,SINT),     NULL,   
SMP_T_SINT, SMP_T_STR},
+       { "http_date",      sample_conv_http_date,    ARG2(0,SINT,STR),     
smp_check_http_date_unit,   SMP_T_SINT, SMP_T_STR},
        { "language",       sample_conv_q_preferred,  ARG2(1,STR,STR),  NULL,   
SMP_T_STR,  SMP_T_STR},
        { "capture-req",    smp_conv_req_capture,     ARG1(1,SINT),     NULL,   
SMP_T_STR,  SMP_T_STR},
        { "capture-res",    smp_conv_res_capture,     ARG1(1,SINT),     NULL,   
SMP_T_STR,  SMP_T_STR},
diff --git a/src/sample.c b/src/sample.c
index 98b5d573..1e4039d2 100644
--- a/src/sample.c
+++ b/src/sample.c
@@ -2940,14 +2940,60 @@ smp_fetch_env(const struct arg *args, struct sample 
*smp, const char *kw, void *
        return 1;
 }
 
-/* retrieve the current local date in epoch time, and applies an optional 
offset
- * of args[0] seconds.
+/* Validates the data unit argument passed to "date" fetch. Argument 1 support 
an
+ * optional string representing the unit of the result: "s" for seconds, "ms" 
for
+ * milliseconds and "us" for microseconds.
+ * Returns 0 on error and non-zero if OK.
+ */
+int smp_check_date_unit(struct arg *args, char **err)
+{
+        if (args[1].type == ARGT_STR) {
+                if (strcmp(args[1].data.str.area, "s") == 0) {
+                        args[1].data.sint = TIME_UNIT_S;
+                }
+                else if (strcmp(args[1].data.str.area, "ms") == 0) {
+                        args[1].data.sint = TIME_UNIT_MS;
+                }
+                else if (strcmp(args[1].data.str.area, "us") == 0) {
+                        args[1].data.sint = TIME_UNIT_US;
+                }
+                else {
+                        memprintf(err, "expects 's', 'ms' or 'us', got '%s'",
+                                  args[1].data.str.area);
+                        return 0;
+                }
+                free(args[1].data.str.area);
+                args[1].data.str.area = NULL;
+                args[1].type = ARGT_SINT;
+        }
+        else if (args[1].type != ARGT_STOP) {
+                memprintf(err, "Unexpected arg type");
+                return 0;
+        }
+
+        return 1;
+}
+
+/* retrieve the current local date in epoch time, converts it to milliseconds
+ * or microseconds if asked to in optional args[1] unit param, and applies an
+ * optional args[0] offset.
  */
 static int
 smp_fetch_date(const struct arg *args, struct sample *smp, const char *kw, 
void *private)
 {
        smp->data.u.sint = date.tv_sec;
 
+       /* report in milliseconds */
+       if (args && args[1].type == ARGT_SINT && args[1].data.sint == 
TIME_UNIT_MS) {
+               smp->data.u.sint *= 1000;
+               smp->data.u.sint += date.tv_usec / 1000;
+       }
+       /* report in microseconds */
+       else if (args && args[1].type == ARGT_SINT && args[1].data.sint == 
TIME_UNIT_US) {
+               smp->data.u.sint *= 1000000;
+               smp->data.u.sint += date.tv_usec;
+       }
+
        /* add offset */
        if (args && args[0].type == ARGT_SINT)
                smp->data.u.sint += args[0].data.sint;
@@ -3259,7 +3305,7 @@ static struct sample_fetch_kw_list smp_kws = {ILH, {
        { "always_false", smp_fetch_false, 0,            NULL, SMP_T_BOOL, 
SMP_USE_INTRN },
        { "always_true",  smp_fetch_true,  0,            NULL, SMP_T_BOOL, 
SMP_USE_INTRN },
        { "env",          smp_fetch_env,   ARG1(1,STR),  NULL, SMP_T_STR,  
SMP_USE_INTRN },
-       { "date",         smp_fetch_date,  ARG1(0,SINT), NULL, SMP_T_SINT, 
SMP_USE_INTRN },
+       { "date",         smp_fetch_date,  ARG2(0,SINT,STR), 
smp_check_date_unit, SMP_T_SINT, SMP_USE_INTRN },
        { "date_us",      smp_fetch_date_us,  0,         NULL, SMP_T_SINT, 
SMP_USE_INTRN },
        { "hostname",     smp_fetch_hostname, 0,         NULL, SMP_T_STR,  
SMP_USE_INTRN },
        { "nbproc",       smp_fetch_nbproc,0,            NULL, SMP_T_SINT, 
SMP_USE_INTRN },
-- 
2.20.1


Reply via email to