From 50ada25ea59f90e5666d18c9168f2b5deeaea709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Cichoci=C5=84ski?= Date: Tue, 29 Oct 2019 22:44:38 +0100 Subject: [PATCH 1/2] feat(morph_logger): basic implementation --- dune-project | 11 ++ morph_logger.json | 26 ++++ src/morph/src/Headers.re | 16 +++ src/morph/src/Headers.rei | 9 ++ src/morph/src/Morph.re | 2 + src/morph/src/Morph.rei | 2 + src/morph/src/Request.re | 4 +- src/morph/src/Request.rei | 4 +- src/morph/src/Response.re | 43 ++++--- src/morph/src/Response.rei | 11 +- src/morph/src/Utils.re | 6 + src/morph_logger/.npmignore | 4 + src/morph_logger/src/Morph_logger.re | 170 ++++++++++++++++++++++++++ src/morph_logger/src/Morph_logger.rei | 23 ++++ src/morph_logger/src/dune | 4 + 15 files changed, 303 insertions(+), 32 deletions(-) create mode 100644 morph_logger.json create mode 100644 src/morph/src/Headers.re create mode 100644 src/morph/src/Headers.rei create mode 100644 src/morph_logger/.npmignore create mode 100644 src/morph_logger/src/Morph_logger.re create mode 100644 src/morph_logger/src/Morph_logger.rei create mode 100644 src/morph_logger/src/dune diff --git a/dune-project b/dune-project index de3c287b..f0ef4319 100644 --- a/dune-project +++ b/dune-project @@ -116,6 +116,17 @@ graphql-lwt )) +(package + (name morph_logger) + (depends + (ocaml (>= 4.04.2)) + (dune (>= 1.11)) + reason + lwt + mtime + logs + morph)) + (package (name morph_test) (depends diff --git a/morph_logger.json b/morph_logger.json new file mode 100644 index 00000000..042e0ce8 --- /dev/null +++ b/morph_logger.json @@ -0,0 +1,26 @@ +{ + "name": "@reason-native-web/morph_logger", + "version": "0.1.2", + "esy": { + "build": "dune build -p morph_logger", + "buildEnv": { + "ODOC_SYNTAX": "re" + } + }, + "scripts": { + "format": "dune build @fmt --auto-promote" + }, + "dependencies": { + "@opam/dune": "*", + "@opam/lwt": "*", + "@opam/mtime": "*", + "@opam/logs": "*", + "@reason-native-web/morph": "*", + "ocaml": "< 4.09.0", + "@esy-ocaml/reason": "*" + }, + "resolutions": { + "@reason-native-web/morph": "link:./morph.json" + }, + "devDependencies": {} +} diff --git a/src/morph/src/Headers.re b/src/morph/src/Headers.re new file mode 100644 index 00000000..9b1dfc6e --- /dev/null +++ b/src/morph/src/Headers.re @@ -0,0 +1,16 @@ +type t = list((string, string)); + +let empty = []; + +let get_header = (key, headers) => + headers + |> List.find_opt(((header, _)) => + String.lowercase_ascii(header) == String.lowercase_ascii(key) + ) + |> Utils.map_opt(snd); + +let add_header = (new_header, headers) => + headers @ [new_header]; + +let add_headers = (new_headers, headers: t) => + headers @ new_headers; diff --git a/src/morph/src/Headers.rei b/src/morph/src/Headers.rei new file mode 100644 index 00000000..0f4fbdd7 --- /dev/null +++ b/src/morph/src/Headers.rei @@ -0,0 +1,9 @@ +type t = list((string, string)); + +let empty: t; + +let get_header: (string, t) => option(string); + +let add_header: ((string, string), t) => t; + +let add_headers: (list((string, string)), t) => t; diff --git a/src/morph/src/Morph.re b/src/morph/src/Morph.re index 901c5f96..41faea25 100644 --- a/src/morph/src/Morph.re +++ b/src/morph/src/Morph.re @@ -17,3 +17,5 @@ module Response = Response; module Method = Method; module Status = Status; + +module Headers = Headers; diff --git a/src/morph/src/Morph.rei b/src/morph/src/Morph.rei index da126afa..e85f0142 100644 --- a/src/morph/src/Morph.rei +++ b/src/morph/src/Morph.rei @@ -29,3 +29,5 @@ module Response = Response; module Method = Method; module Status = Status; + +module Headers = Headers; diff --git a/src/morph/src/Request.re b/src/morph/src/Request.re index c5581dd7..693376a4 100644 --- a/src/morph/src/Request.re +++ b/src/morph/src/Request.re @@ -1,7 +1,7 @@ type t = { target: string, meth: Method.t, - headers: list((string, string)), + headers: Headers.t, read_body: unit => Lwt.t(string), context: Hmap.t, }; @@ -9,7 +9,7 @@ type t = { let make = ( ~meth=`GET, - ~headers=[], + ~headers=Headers.empty, ~read_body=() => Lwt.return(""), ~context=Hmap.empty, target, diff --git a/src/morph/src/Request.rei b/src/morph/src/Request.rei index ea17f08d..f27ca3f1 100644 --- a/src/morph/src/Request.rei +++ b/src/morph/src/Request.rei @@ -1,7 +1,7 @@ type t = { target: string, meth: Method.t, - headers: list((string, string)), + headers: Headers.t, read_body: unit => Lwt.t(string), context: Hmap.t, }; @@ -9,7 +9,7 @@ type t = { let make: ( ~meth: Method.t=?, - ~headers: list((string, string))=?, + ~headers: Headers.t=?, ~read_body: unit => Lwt.t(string)=?, ~context: Hmap.t=?, string diff --git a/src/morph/src/Response.re b/src/morph/src/Response.re index 1e080257..16aa3ada 100644 --- a/src/morph/src/Response.re +++ b/src/morph/src/Response.re @@ -1,4 +1,3 @@ -type headers = list((string, string)); type body = [ | `String(string) @@ -8,20 +7,20 @@ type body = [ type t = { status: Status.t, - headers, + headers: Headers.t, body, }; -let empty = {status: `OK, headers: [], body: `String("")}; +let empty = {status: `OK, headers: Headers.empty, body: `String("")}; -let make = (~status=`OK, ~headers=[], body) => {status, headers, body}; +let make = (~status=`OK, ~headers=Headers.empty, body) => {status, headers, body}; let add_header = (new_header: (string, string), res: t) => { - {...res, headers: res.headers @ [new_header]}; + {...res, headers: res.headers |> Headers.add_header(new_header)}; }; -let add_headers = (new_headers: headers, res: t) => { - {...res, headers: res.headers @ new_headers}; +let add_headers = (new_headers: list((string, string)), res: t) => { + {...res, headers: res.headers |> Headers.add_headers(new_headers)}; }; let set_status = (status: Status.t, res: t) => { @@ -33,7 +32,8 @@ let set_body = (body: body, res: t) => { }; let ok = (res: t) => { - add_header(("Content-length", "2"), res) + res + |> add_header(("Content-length", "2")) |> set_status(`OK) |> set_body(`String("ok")) |> Lwt.return; @@ -41,14 +41,18 @@ let ok = (res: t) => { let text = (text, res: t) => { let content_length = text |> String.length |> string_of_int; - add_header(("Content-length", content_length), res) + + res + |> add_header(("Content-length", content_length)) |> set_body(`String(text)) |> Lwt.return; }; let json = (json, res: t) => { let content_length = json |> String.length |> string_of_int; - add_header(("Content-type", "application/json"), res) + + res + |> add_header(("Content-type", "application/json")) |> add_header(("Content-length", content_length)) |> set_body(`String(json)) |> Lwt.return; @@ -56,7 +60,9 @@ let json = (json, res: t) => { let html = (markup, res: t) => { let content_length = markup |> String.length |> string_of_int; - add_header(("Content-type", "text/html"), res) + + res + |> add_header(("Content-type", "text/html")) |> add_header(("Content-length", content_length)) |> set_body(`String(markup)) |> Lwt.return; @@ -65,7 +71,8 @@ let html = (markup, res: t) => { let redirect = (~code=303, targetPath, res: t) => { let content_length = targetPath |> String.length |> string_of_int; - add_header(("Content-length", content_length), res) + res + |> add_header(("Content-length", content_length)) |> add_header(("Location", targetPath)) |> set_status(`Code(code)) |> set_body(`String(targetPath)) @@ -73,20 +80,16 @@ let redirect = (~code=303, targetPath, res: t) => { }; let unauthorized = (message, res: t) => { - add_header( - ("Content-length", String.length(message) |> string_of_int), - res, - ) + res + |> add_header(("Content-length", String.length(message) |> string_of_int)) |> set_status(`Unauthorized) |> set_body(`String(message)) |> Lwt.return; }; let not_found = (~message="Not found", res: t) => { - add_header( - ("content-length", String.length(message) |> string_of_int), - res, - ) + res + |> add_header(("content-length", String.length(message) |> string_of_int)) |> set_status(`Not_found) |> set_body(`String(message)) |> Lwt.return; diff --git a/src/morph/src/Response.rei b/src/morph/src/Response.rei index db025ca5..878ee807 100644 --- a/src/morph/src/Response.rei +++ b/src/morph/src/Response.rei @@ -1,8 +1,3 @@ -/** -[headers] is represented as a list of (string, string) tuples. -*/ -type headers = list((string, string)); - /** [Response.body] variant type structure. There are currently 3 types of bodies. @@ -26,14 +21,14 @@ The core [Response.t] type */ type t = { status: Status.t, - headers, + headers: Headers.t, body, }; /** [make status headers body] creates a response. */ -let make: (~status: Status.t=?, ~headers: headers=?, body) => t; +let make: (~status: Status.t=?, ~headers: Headers.t=?, body) => t; /** [empty t] an empty response, a starting place to compose an http response. @@ -48,7 +43,7 @@ let add_header: ((string, string), t) => t; /** [add_header headers response] returns a copy of t of response with the headers added. */ -let add_headers: (headers, t) => t; +let add_headers: (list((string, string)), t) => t; /** [set_status status response] returns a copy of t with the given status. diff --git a/src/morph/src/Utils.re b/src/morph/src/Utils.re index 1fa261fa..c0a042ab 100644 --- a/src/morph/src/Utils.re +++ b/src/morph/src/Utils.re @@ -3,3 +3,9 @@ let map_or = (~default: 'b, fn: 'a => 'b, opt: option('a)) => | Some(value) => fn(value) | None => default }; + +let map_opt = (fn: 'a => 'b, opt: option('a)) => + switch(opt) { + | None => None + | Some(v) => Some(fn(v)) + }; diff --git a/src/morph_logger/.npmignore b/src/morph_logger/.npmignore new file mode 100644 index 00000000..8323cee3 --- /dev/null +++ b/src/morph_logger/.npmignore @@ -0,0 +1,4 @@ +*.install +*.merlin +*.lock +_esy diff --git a/src/morph_logger/src/Morph_logger.re b/src/morph_logger/src/Morph_logger.re new file mode 100644 index 00000000..c813a6e1 --- /dev/null +++ b/src/morph_logger/src/Morph_logger.re @@ -0,0 +1,170 @@ + +let get_or_else = default => + fun + | None => default + | Some(v) => v; + +module Date = { + type format = + | CLF + | ISO + | WEB; + + let month_to_string = + fun + | 0 => "Jan" + | 1 => "Feb" + | 2 => "Mar" + | 3 => "Apr" + | 4 => "May" + | 5 => "Jun" + | 6 => "Jul" + | 7 => "Aug" + | 8 => "Sep" + | 9 => "Oct" + | 10 => "Nov" + | 11 => "Dec" + | _ => "-"; + + let day_to_string = + fun + | 0 => "Sun" + | 1 => "Mon" + | 2 => "Tue" + | 3 => "Wed" + | 4 => "Thu" + | 5 => "Fri" + | 6 => "Sat" + | _ => "-"; + + let print = (format: format) => { + let {tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, tm_wday, _}: Unix.tm = + Unix.gettimeofday() |> Unix.gmtime; + + let pad_digits = v => + v < 10 ? "0" ++ string_of_int(v) : string_of_int(v); + + switch (format) { + | ISO => + /* common ISO 8601 date time format (2000-10-10T13:55:36.000Z) */ + Format.sprintf( + "%d-%s-%sT%s:%s:%s.000Z", + tm_year + 1900, + pad_digits(tm_mon + 1), + pad_digits(tm_mday), + pad_digits(tm_hour), + pad_digits(tm_min), + pad_digits(tm_sec), + ) + | CLF => + /* common log format ("10/Oct/2000:13:55:36 +0000") */ + Format.sprintf( + "%s/%s/%d:%s:%s:%s +0000", + pad_digits(tm_mday), + month_to_string(tm_mon), + tm_year + 1900, + pad_digits(tm_hour), + pad_digits(tm_min), + pad_digits(tm_sec), + ) + | WEB => + /* common RFC 1123 date time format (Tue, 10 Oct 2000 13:55:36 GMT) */ + Format.sprintf( + "%s, %s %s %d %s:%s:%s GMT", + day_to_string(tm_wday), + pad_digits(tm_mday), + month_to_string(tm_mon), + tm_year + 1900, + pad_digits(tm_hour), + pad_digits(tm_min), + pad_digits(tm_sec), + ) + }; + }; +}; + +type t = + | Dev + | Tiny + | Custom(list(token)) +and token = + | Url + | Status + | Method + | ResHeader(string) + | ReqHeader(string) + | S(string) + | ResponseTime + | UserAgent + | Date(Date.format); + + + + +let rec print = + ( + ~response_time, + ~request: Morph.Request.t, + ~response: Morph.Response.t, + ) => + fun + | Dev => + /* :method :url :status :response-time ms - :res[content-length] */ + Custom([ + Method, + Url, + Status, + ResponseTime, + S("ms"), + S("-"), + ResHeader("content-length"), + ]) + |> print(~response_time, ~request, ~response) + | Tiny => + /* :method :url :status :res[content-length] - :response-time ms */ + Custom([ + Method, + Url, + Status, + ResHeader("content-length"), + S("-"), + ResponseTime, + S("ms"), + ]) + |> print(~response_time, ~request, ~response) + | Custom(tokens) => + tokens + |> List.map( + fun + | Url => request.target + | Status => response.status |> Morph.Status.to_code |> string_of_int + | Method => request.meth |> Morph.Method.to_string + | ResHeader(header) => + response.headers |> Morph.Headers.get_header(header) |> get_or_else("-") + | ReqHeader(header) => + request.headers |> Morph.Headers.get_header(header) |> get_or_else("-") + | S(separator) => separator + | ResponseTime => response_time |> string_of_float + | UserAgent => + request.headers |> Morph.Headers.get_header("user-agent") |> get_or_else("-") + | Date(format) => Date.print(format), + ) + |> List.fold_left((acc, item) => acc ++ " " ++ item, "") + |> String.trim; + +let logger = (~format: t, service, request: Morph.Request.t) => { + let start_request = Mtime_clock.elapsed(); + + service(request) + |> Lwt.map((response: Morph.Response.t) => { + let end_request = Mtime_clock.elapsed(); + let response_time = + Mtime.Span.abs_diff(start_request, end_request) |> Mtime.Span.to_ms; + + Logs.info(m => + m("%s", print(~response_time, ~request, ~response, format)) + ); + + response; + }); +}; diff --git a/src/morph_logger/src/Morph_logger.rei b/src/morph_logger/src/Morph_logger.rei new file mode 100644 index 00000000..61ab0ff3 --- /dev/null +++ b/src/morph_logger/src/Morph_logger.rei @@ -0,0 +1,23 @@ +module Date: { + type format = + | CLF + | ISO + | WEB; +}; + +type t = + | Dev + | Tiny + | Custom(list(token)) +and token = + | Url + | Status + | Method + | ResHeader(string) + | ReqHeader(string) + | S(string) + | ResponseTime + | UserAgent + | Date(Date.format); + +let logger: (~format: t) => Morph.Server.middleware; diff --git a/src/morph_logger/src/dune b/src/morph_logger/src/dune new file mode 100644 index 00000000..a1940bfc --- /dev/null +++ b/src/morph_logger/src/dune @@ -0,0 +1,4 @@ +(library + (name morph_logger) + (public_name morph_logger) + (libraries lwt morph mtime mtime.clock.os)) From 4776dd88d94e9c81d19bcaae452c3996dd9a511e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Cichoci=C5=84ski?= Date: Wed, 30 Oct 2019 10:12:09 +0100 Subject: [PATCH 2/2] feat(morph_logger): generate morph_logger opam and fix formatting --- dune-project | 1 + morph_logger.opam | 30 ++++++++++++++++++++++++++++ src/morph/src/Headers.re | 6 ++---- src/morph/src/Response.re | 15 ++++++++------ src/morph/src/Utils.re | 6 +++--- src/morph_logger/src/Morph_logger.re | 16 ++++++++------- 6 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 morph_logger.opam diff --git a/dune-project b/dune-project index f0ef4319..06da9a5b 100644 --- a/dune-project +++ b/dune-project @@ -118,6 +118,7 @@ (package (name morph_logger) + (synopsis "Logging middleware for morph") (depends (ocaml (>= 4.04.2)) (dune (>= 1.11)) diff --git a/morph_logger.opam b/morph_logger.opam new file mode 100644 index 00000000..c74f54a3 --- /dev/null +++ b/morph_logger.opam @@ -0,0 +1,30 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "Logging middleware for morph" +homepage: "https://reason-native-web.github.io" +doc: "https://reason-native-web.github.io/morph" +bug-reports: "https://github.com/reason-native-web/morph/issues" +depends: [ + "ocaml" {>= "4.04.2"} + "dune" {>= "1.11"} + "reason" + "lwt" + "mtime" + "logs" + "morph" +] +build: [ + ["dune" "subst"] {pinned} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git+https://github.com/reason-native-web/morph.git" diff --git a/src/morph/src/Headers.re b/src/morph/src/Headers.re index 9b1dfc6e..4c9e8d11 100644 --- a/src/morph/src/Headers.re +++ b/src/morph/src/Headers.re @@ -9,8 +9,6 @@ let get_header = (key, headers) => ) |> Utils.map_opt(snd); -let add_header = (new_header, headers) => - headers @ [new_header]; +let add_header = (new_header, headers) => headers @ [new_header]; -let add_headers = (new_headers, headers: t) => - headers @ new_headers; +let add_headers = (new_headers, headers: t) => headers @ new_headers; diff --git a/src/morph/src/Response.re b/src/morph/src/Response.re index 16aa3ada..c6f3c16b 100644 --- a/src/morph/src/Response.re +++ b/src/morph/src/Response.re @@ -1,4 +1,3 @@ - type body = [ | `String(string) | `Stream(Lwt_stream.t(char)) @@ -13,7 +12,11 @@ type t = { let empty = {status: `OK, headers: Headers.empty, body: `String("")}; -let make = (~status=`OK, ~headers=Headers.empty, body) => {status, headers, body}; +let make = (~status=`OK, ~headers=Headers.empty, body) => { + status, + headers, + body, +}; let add_header = (new_header: (string, string), res: t) => { {...res, headers: res.headers |> Headers.add_header(new_header)}; @@ -32,7 +35,7 @@ let set_body = (body: body, res: t) => { }; let ok = (res: t) => { - res + res |> add_header(("Content-length", "2")) |> set_status(`OK) |> set_body(`String("ok")) @@ -61,7 +64,7 @@ let json = (json, res: t) => { let html = (markup, res: t) => { let content_length = markup |> String.length |> string_of_int; - res + res |> add_header(("Content-type", "text/html")) |> add_header(("Content-length", content_length)) |> set_body(`String(markup)) @@ -71,7 +74,7 @@ let html = (markup, res: t) => { let redirect = (~code=303, targetPath, res: t) => { let content_length = targetPath |> String.length |> string_of_int; - res + res |> add_header(("Content-length", content_length)) |> add_header(("Location", targetPath)) |> set_status(`Code(code)) @@ -88,7 +91,7 @@ let unauthorized = (message, res: t) => { }; let not_found = (~message="Not found", res: t) => { - res + res |> add_header(("content-length", String.length(message) |> string_of_int)) |> set_status(`Not_found) |> set_body(`String(message)) diff --git a/src/morph/src/Utils.re b/src/morph/src/Utils.re index c0a042ab..a90763a3 100644 --- a/src/morph/src/Utils.re +++ b/src/morph/src/Utils.re @@ -5,7 +5,7 @@ let map_or = (~default: 'b, fn: 'a => 'b, opt: option('a)) => }; let map_opt = (fn: 'a => 'b, opt: option('a)) => - switch(opt) { - | None => None - | Some(v) => Some(fn(v)) + switch (opt) { + | None => None + | Some(v) => Some(fn(v)) }; diff --git a/src/morph_logger/src/Morph_logger.re b/src/morph_logger/src/Morph_logger.re index c813a6e1..4cdd975e 100644 --- a/src/morph_logger/src/Morph_logger.re +++ b/src/morph_logger/src/Morph_logger.re @@ -1,4 +1,3 @@ - let get_or_else = default => fun | None => default @@ -98,9 +97,6 @@ and token = | UserAgent | Date(Date.format); - - - let rec print = ( ~response_time, @@ -140,13 +136,19 @@ let rec print = | Status => response.status |> Morph.Status.to_code |> string_of_int | Method => request.meth |> Morph.Method.to_string | ResHeader(header) => - response.headers |> Morph.Headers.get_header(header) |> get_or_else("-") + response.headers + |> Morph.Headers.get_header(header) + |> get_or_else("-") | ReqHeader(header) => - request.headers |> Morph.Headers.get_header(header) |> get_or_else("-") + request.headers + |> Morph.Headers.get_header(header) + |> get_or_else("-") | S(separator) => separator | ResponseTime => response_time |> string_of_float | UserAgent => - request.headers |> Morph.Headers.get_header("user-agent") |> get_or_else("-") + request.headers + |> Morph.Headers.get_header("user-agent") + |> get_or_else("-") | Date(format) => Date.print(format), ) |> List.fold_left((acc, item) => acc ++ " " ++ item, "")