Loading the parity record
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Parity / morgan
Every number on this page was produced by running both implementations over the same cases: the real morgan package pinned at 1.10.0 answers first, and its answer is the expectation the Go port is held to. Nothing is a hand-written expectation, so a new upstream release re-scores the port on its own. See morgan for the port's own documentation. Source: github.com/malcolmston/morgan.
Not a generic diagram: every node below names an artefact of this harness — the pinned package it installed, the runner files it started, the case files it streamed, and the counts it wrote out.
| Case group | Cases | Match | Mismatch | Group parity |
|---|---|---|---|---|
| tokens | 48 | 39 | 9 | 81.3% |
| behaviour | 21 | 16 | 5 | 76.2% |
| formats | 18 | 11 | 7 | 61.1% |
| bugs | 11 | 0 | 11 | 0.0% |
Every exported symbol of the upstream package, and what the port offers for it. The upstream list is derived mechanically, never from a README: cd parity/morgan/node && node -e "
const morgan=require('morgan');
const keys=Object.keys(morgan).sort();
console.log('tokens :', keys.filter(k=>typeof morgan[k]==='function' && !['format','token','compile'].includes(k)));
console.log('formats:', keys.filter(k=>typeof morgan[k]==='string'));". A symbol with no case is untested, never a match.
| Upstream symbol | Go symbol | Status | Cases | Note |
|---|---|---|---|---|
| :method | :method | match | tok-method-get, tok-method-delete | |
| :url | :url | differs | tok-url-with-query, tok-url-root, bug-url-percent-decoded, bug-url-percent-encoded-space, urlencoded-path-combined | port builds the value from r.URL.Path, which net/http has already percent-decoded; upstream logs req.originalUrl verbatim. Plain ASCII paths match. |
| :status | :status | match | tok-status-200, tok-status-404, tok-status-503, immediate-status-unavailable | including - when headers have not been sent |
| :http-version | :http-version | match | tok-http-version | |
| :referrer | :referrer | match | tok-referrer-present, tok-referrer-misspelled-header, tok-referrer-missing | both accept Referer and Referrer |
| :user-agent | :user-agent | match | tok-user-agent-present, tok-user-agent-missing | |
| :remote-addr | :remote-addr | differs | tok-remote-addr, xff-single-entry-remote-addr, xff-multi-entry-remote-addr, xff-combined, bug-xff-trusted-unconditionally, bug-xff-list-renders-nil, bug-xff-non-ip-value | matches on a direct connection; the port trusts X-Forwarded-For unconditionally (upstream needs express trust proxy), and a header that is not a bare IP literal renders as <nil> |
| :remote-user | :remote-user | match | tok-remote-user-basic-auth, tok-remote-user-missing, tok-remote-user-no-password, basic-auth-combined | |
| :date, :date[clf|iso|web] | :date, :date[…] | differs | tok-date-default-arg, tok-date-clf, tok-date-iso, tok-date-web, tok-date-unknown-arg | all three documented arguments and the no-argument default match; an *unknown* argument yields - upstream (the switch has no default) but falls back to the web format in the port |
| :response-time, :response-time[digits] | same | differs | tok-response-time, tok-response-time-digits-5, tok-response-time-digits-0, tok-response-time-digits-invalid, immediate-response-time-unavailable | digit counts match for numeric arguments; a non-numeric argument means 0 decimals upstream (toFixed) but 3 in the port, and in immediate mode upstream renders - while the port renders 0.000 |
| :total-time, :total-time[digits] | same | match | tok-total-time, tok-total-time-digits-1, fmt-raw-rich | |
| :req[header] | :req[header] | match | tok-req-header, tok-req-header-missing, tok-req-header-repeated, tok-req-header-content-length | repeated headers join with ", " on both sides |
| :res[header] | :res[header] | differs | tok-res-header, tok-res-header-missing, tok-res-content-length-explicit, tok-res-content-length-chunked, bug-content-length-*, immediate-content-length-unavailable | arbitrary headers match. :res[content-length] matches only when the handler sets Content-Length itself; when the framework computes it (express res.send) the port has nothing to read and prints -, so **combined/common/short/tiny/dev all lose the response size |
| — | :pid | extra | tok-pid-port-only | port-only; upstream throws TypeError on the unknown token |
| — | :incoming | extra | tok-incoming-port-only | port-only (request Content-Length) |
| — | :protocol | extra | tok-protocol-port-only | port-only |
| — | :host | extra | tok-host-port-only | port-only |
| — | :path | extra | tok-path-port-only | port-only (path without query) |
| — | :query | extra | tok-query-port-only | port-only (raw query string) |
| unknown token → TypeError at render time | unknown token → - | differs | tok-unknown-name | upstream fails the request's log line loudly; the port silently substitutes - |
| morgan('combined') | morgan.Combined | differs | fmt-combined, bug-content-length-combined, status-404-combined, missing-referrer-and-user-agent-combined, basic-auth-combined, immediate-combined, urlencoded-path-combined, xff-combined | identical format string; diverges only through :res[content-length], :url and :remote-addr |
| morgan('common') | morgan.Common | differs | fmt-common, post-with-body-common, bug-content-length-common | same, via :res[content-length] |
| morgan('short') | morgan.Short | differs | fmt-short, status-500-short, bug-content-length-short | same, via :res[content-length] |
| morgan('tiny') | morgan.Tiny | differs | fmt-tiny, head-request-tiny, bug-content-length-tiny | same, via :res[content-length] |
| morgan('dev') | morgan.Dev | differs | fmt-dev-2xx, fmt-dev-3xx, fmt-dev-4xx, fmt-dev-5xx, bug-content-length-dev | the port drops **all ANSI colouring** whenever the stream is not a terminal; upstream always emits ESC[0m … ESC[<colour>m<status>ESC[0m …. Layout, status classes and colour selection logic otherwise agree |
| morgan('default') (deprecated) | — | missing | fmt-default-deprecated | the port has no default name, so "default" is compiled as a literal format string |
| — | morgan.JSON | extra | fmt-json-port-only, bug-json-format-invalid-json | port-only format, and its output is **not valid JSON**: contentLength is interpolated as a bare - when unknown ("contentLength":-) |
| empty format name | — | differs | fmt-raw-empty-string | upstream falls back to morgan.default; the port compiles the empty string and therefore writes no line at all |
| morgan.compile(fmt) (raw :token / :token[arg] strings) | morgan.Compile | match | fmt-raw-minimal, fmt-raw-rich, fmt-raw-literal-only, fmt-raw-adjacent-tokens, fmt-raw-short-name-not-a-token | same token grammar, including the >= 2 name characters rule and - for empty values |
| morgan.format(name, string) | morgan.RegisterFormat | match | fmt-registered-name | |
| morgan.format(name, fn) | morgan.RegisterFormatFunc | match | fmt-registered-func | |
| morgan.token(name, fn) | morgan.Token | match | ctok-const, ctok-from-request, ctok-with-arg, ctok-with-arg-missing, ctok-empty-string | including the :name[arg] form and - for an empty return |
| morgan(format, options) | morgan.New(next, format, cfg) | match | all 98 | the middleware itself: one line per request, written after the response finishes |
| options.stream | morgan.Config.Stream | match | all 98 | every case captures the line through this option |
| options.immediate | morgan.Config.Immediate | differs | immediate-request-fields, immediate-status-unavailable, immediate-content-length-unavailable, immediate-combined, immediate-response-time-unavailable | request-only tokens, :status and :res[…] agree (all -); :response-time does not (- upstream vs 0.000) |
| options.skip | morgan.Config.Skip | match | skip-none, skip-status-below-400-suppresses-200, skip-status-below-400-logs-404, skip-status-below-400-logs-500, skip-path-healthz-suppresses, skip-path-healthz-logs-other | exercised through morgan.SkipStatusBelow and morgan.SkipPaths |
| options.buffer (deprecated) | morgan.Config.Buffer | untested | — | upstream deprecates it and it is interval-based, so it cannot be compared deterministically per request |
| morgan(options) object form (deprecated) | — | missing | — | the port has no options-only constructor |
| — | morgan.Config.TrustProxy | missing | — | Config in v1.2.0 has no such field; X-Forwarded-For is always trusted. Upstream's equivalent is express trust proxy, which is off by default |
| — | morgan.Format, morgan.Config, morgan.Log, morgan.FormatFunc, morgan.TokenFunc, morgan.SkipFunc, morgan.IP | extra | — (types used by every case) | the port is typed; upstream passes loose req/res objects |
| — | morgan.FromRequest | extra | — | used internally by New; no upstream analogue |
| — | morgan.SkipStatusBelow | extra | skip-status-below-400-* | Go form of morgan's documented "only log errors" example |
| — | morgan.SkipPaths | extra | skip-path-healthz-* | |
| — | morgan.SkipStatusBetween | extra | — | |
| — | morgan.SkipUserAgents | extra | — | |
| — | morgan.CombineSkips | extra | — | |
| — | morgan.Clfdate | extra | — (covered indirectly by tok-date-clf) | exported helper behind :date[clf] |
| — | morgan.FormatDuration | extra | — (covered indirectly by tok-response-time*) | exported helper behind :response-time |
| — | morgan.ClientIP | extra | — (covered indirectly by :remote-addr cases) | |
| — | morgan.RequestURL | extra | — (covered indirectly by :url cases) | |
| — | morgan.RequestProtocol | extra | — (covered indirectly by tok-protocol-port-only) | |
| — | morgan.StatusCategory | extra | — | no upstream analogue |
| — | morgan.StatusColorCode | extra | — | the colour selection it encodes is compared through fmt-dev-* |
| — | morgan.IP.String, morgan.Log.String | extra | — | Log.String() renders combined format outside the middleware |
Every case the harness streamed to both runners, with the exact upstream symbol and Go symbol it exercised. A deliberate, documented difference is a deviation and is counted apart from a mismatch.
| Case | Group | Upstream symbol | Go symbol | Status | Note |
|---|---|---|---|---|---|
| skip-none | behaviour | morgan(fmt, {skip}) | morgan.Config.Skip | match | |
| skip-status-below-400-suppresses-200 | behaviour | morgan(fmt, {skip}) | morgan.SkipStatusBelow(400) | match | no line at all |
| skip-status-below-400-logs-500 | behaviour | morgan(fmt, {skip}) | morgan.SkipStatusBelow(400) | match | |
| skip-status-below-400-logs-404 | behaviour | morgan(fmt, {skip}) | morgan.SkipStatusBelow(400) | match | |
| skip-path-healthz-suppresses | behaviour | morgan(fmt, {skip}) | morgan.SkipPaths("/healthz") | match | |
| skip-path-healthz-logs-other | behaviour | morgan(fmt, {skip}) | morgan.SkipPaths("/healthz") | match | |
| immediate-request-fields | behaviour | morgan(fmt, {immediate: true}) | morgan.Config.Immediate | match | request-only tokens must be identical in immediate mode |
| immediate-status-unavailable | behaviour | morgan(fmt, {immediate: true}) | morgan.Config.Immediate | match | headers not sent yet, so :status must be '-' |
| immediate-response-time-unavailable | behaviour | morgan(fmt, {immediate: true}) | morgan.Config.Immediate | mismatch | no response has started, so upstream renders '-' |
| immediate-content-length-unavailable | behaviour | morgan(fmt, {immediate: true}) | morgan.Config.Immediate | match | |
| immediate-combined | behaviour | morgan('combined', {immediate: true}) | morgan.Combined + Config.Immediate | match | |
| urlencoded-path-combined | behaviour | morgan('combined') | morgan.Combined | mismatch | upstream logs req.originalUrl, which is never percent-decoded |
| basic-auth-combined | behaviour | morgan('combined') | morgan.Combined | match | |
| missing-referrer-and-user-agent-combined | behaviour | morgan('combined') | morgan.Combined | match | |
| status-404-combined | behaviour | morgan('combined') | morgan.Combined | match | |
| status-500-short | behaviour | morgan('short') | morgan.Short | match | |
| post-with-body-common | behaviour | morgan('common') | morgan.Common | match | |
| head-request-tiny | behaviour | morgan('tiny') | morgan.Tiny | match | |
| xff-single-entry-remote-addr | behaviour | morgan.token('remote-addr') | :remote-addr | mismatch | express 'trust proxy' is off by default, so upstream ignores X-Forwarded-For |
| xff-multi-entry-remote-addr | behaviour | morgan.token('remote-addr') | :remote-addr | mismatch | |
| xff-combined | behaviour | morgan('combined') | morgan.Combined | mismatch | |
| bug-content-length-tiny | bugs | morgan('tiny') | morgan.Tiny | mismatch | handler writes a buffered body without setting Content-Length itself (express res.send does it; Go's net/http computes it inside the server). The port reads only the response header map, so :res[content-length] is '-'. |
| bug-content-length-combined | bugs | morgan('combined') | morgan.Combined | mismatch | |
| bug-content-length-common | bugs | morgan('common') | morgan.Common | mismatch | |
| bug-content-length-short | bugs | morgan('short') | morgan.Short | mismatch | |
| bug-content-length-dev | bugs | morgan('dev') | morgan.Dev | mismatch | |
| bug-json-format-invalid-json | bugs | — | morgan.JSON | mismatch | port-only format; it interpolates a bare '-' for contentLength, so the line is not valid JSON |
| bug-url-percent-decoded | bugs | morgan.token('url') | :url | mismatch | the port builds :url from r.URL.Path, which net/http has already percent-decoded; upstream logs the raw target |
| bug-url-percent-encoded-space | bugs | morgan.token('url') | :url | mismatch | |
| bug-xff-trusted-unconditionally | bugs | morgan.token('remote-addr') | :remote-addr | mismatch | the port always prefers X-Forwarded-For, so any client can choose the logged address; upstream needs 'trust proxy' to be enabled |
| bug-xff-list-renders-nil | bugs | morgan.token('remote-addr') | :remote-addr | mismatch | the port stores the whole header in a net.IP, so a proxy chain parses as nil and renders the literal '<nil>' |
| bug-xff-non-ip-value | bugs | morgan.token('remote-addr') | :remote-addr | mismatch | |
| fmt-combined | formats | morgan('combined') | morgan.Combined | match | |
| fmt-common | formats | morgan('common') | morgan.Common | match | |
| fmt-short | formats | morgan('short') | morgan.Short | match | |
| fmt-tiny | formats | morgan('tiny') | morgan.Tiny | match | |
| fmt-dev-2xx | formats | morgan('dev') | morgan.Dev | mismatch | |
| fmt-dev-3xx | formats | morgan('dev') | morgan.Dev | mismatch | |
| fmt-dev-4xx | formats | morgan('dev') | morgan.Dev | mismatch | |
| fmt-dev-5xx | formats | morgan('dev') | morgan.Dev | mismatch | |
| fmt-default-deprecated | formats | morgan('default') | — | mismatch | upstream ships a deprecated 'default' format; the port has no such name and compiles the literal string |
| fmt-json-port-only | formats | — | morgan.JSON | mismatch | port-only format; upstream has no 'json' name so it compiles the literal string |
| fmt-raw-minimal | formats | morgan.compile | morgan.Compile | match | |
| fmt-raw-rich | formats | morgan.compile | morgan.Compile | match | |
| fmt-raw-literal-only | formats | morgan.compile | morgan.Compile | match | |
| fmt-raw-adjacent-tokens | formats | morgan.compile | morgan.Compile | match | |
| fmt-raw-short-name-not-a-token | formats | morgan.compile | morgan.Compile | match | morgan's token regex requires >=2 name chars, so :a and :b stay literal |
| fmt-registered-name | formats | morgan.format(name, string) | morgan.RegisterFormat | match | both runners register the same format string under the name 'parity-named' at startup |
| fmt-registered-func | formats | morgan.format(name, fn) | morgan.RegisterFormatFunc | match | both runners register an equivalent format *function* under the name 'parity-fn' at startup |
| fmt-raw-empty-string | formats | morgan.compile | morgan.Compile | mismatch | upstream getFormatFunction falls back to morgan.default for a falsy name |
| tok-method-get | tokens | morgan.token('method') | :method | match | |
| tok-method-delete | tokens | morgan.token('method') | :method | match | |
| tok-url-with-query | tokens | morgan.token('url') | :url | match | |
| tok-url-root | tokens | morgan.token('url') | :url | match | |
| tok-status-200 | tokens | morgan.token('status') | :status | match | |
| tok-status-404 | tokens | morgan.token('status') | :status | match | |
| tok-status-503 | tokens | morgan.token('status') | :status | match | |
| tok-http-version | tokens | morgan.token('http-version') | :http-version | match | |
| tok-referrer-present | tokens | morgan.token('referrer') | :referrer | match | |
| tok-referrer-misspelled-header | tokens | morgan.token('referrer') | :referrer | match | upstream reads req.headers.referer || req.headers.referrer |
| tok-referrer-missing | tokens | morgan.token('referrer') | :referrer | match | |
| tok-user-agent-present | tokens | morgan.token('user-agent') | :user-agent | match | |
| tok-user-agent-missing | tokens | morgan.token('user-agent') | :user-agent | match | |
| tok-remote-addr | tokens | morgan.token('remote-addr') | :remote-addr | match | |
| tok-remote-user-basic-auth | tokens | morgan.token('remote-user') | :remote-user | match | Authorization is Basic base64(alice:s3cret) |
| tok-remote-user-missing | tokens | morgan.token('remote-user') | :remote-user | match | |
| tok-remote-user-no-password | tokens | morgan.token('remote-user') | :remote-user | match | Authorization is Basic base64(bob) — no colon, no password |
| tok-date-default-arg | tokens | morgan.token('date') | :date | match | |
| tok-date-clf | tokens | morgan.token('date') | :date[clf] | match | |
| tok-date-iso | tokens | morgan.token('date') | :date[iso] | match | |
| tok-date-web | tokens | morgan.token('date') | :date[web] | match | |
| tok-date-unknown-arg | tokens | morgan.token('date') | :date[nope] | mismatch | upstream's switch has no default, so an unknown argument yields undefined -> '-' |
| tok-response-time | tokens | morgan.token('response-time') | :response-time | match | value normalised to <T:n> where n is the decimal-digit count |
| tok-response-time-digits-5 | tokens | morgan.token('response-time') | :response-time[5] | match | |
| tok-response-time-digits-0 | tokens | morgan.token('response-time') | :response-time[0] | match | |
| tok-response-time-digits-invalid | tokens | morgan.token('response-time') | :response-time[abc] | mismatch | upstream hands the argument to toFixed, so a non-numeric argument means 0 decimals |
| tok-total-time | tokens | morgan.token('total-time') | :total-time | match | |
| tok-total-time-digits-1 | tokens | morgan.token('total-time') | :total-time[1] | match | |
| tok-req-header | tokens | morgan.token('req') | :req[X-Trace] | match | |
| tok-req-header-missing | tokens | morgan.token('req') | :req[X-Absent] | match | |
| tok-req-header-repeated | tokens | morgan.token('req') | :req[X-Multi] | match | a repeated header is an array in node; morgan joins with ', ' |
| tok-req-header-content-length | tokens | morgan.token('req') | :req[Content-Length] | match | |
| tok-res-header | tokens | morgan.token('res') | :res[X-Custom] | match | |
| tok-res-header-missing | tokens | morgan.token('res') | :res[X-Absent] | match | |
| tok-res-content-length-explicit | tokens | morgan.token('res') | :res[content-length] | match | handler sets Content-Length itself on both sides |
| tok-res-content-length-chunked | tokens | morgan.token('res') | :res[content-length] | match | no Content-Length anywhere: both sides must report '-' |
| ctok-const | tokens | morgan.token(name, fn) | morgan.Token(name, fn) | match | |
| ctok-from-request | tokens | morgan.token(name, fn) | morgan.Token(name, fn) | match | custom token derived from the request |
| ctok-with-arg | tokens | morgan.token(name, fn) with :name[arg] | morgan.Token(name, fn) with :name[arg] | match | |
| ctok-with-arg-missing | tokens | morgan.token(name, fn) with :name[arg] | morgan.Token(name, fn) with :name[arg] | match | |
| ctok-empty-string | tokens | morgan.token(name, fn) | morgan.Token(name, fn) | match | an empty token value must render as '-' on both sides |
| tok-unknown-name | tokens | morgan.compile (unknown token) | morgan.Compile (unknown token) | mismatch | upstream throws a TypeError at render time; the port renders '-' |
| tok-pid-port-only | tokens | — | :pid | mismatch | |
| tok-incoming-port-only | tokens | — | :incoming | mismatch | |
| tok-protocol-port-only | tokens | — | :protocol | mismatch | |
| tok-host-port-only | tokens | — | :host | mismatch | |
| tok-path-port-only | tokens | — | :path | mismatch | |
| tok-query-port-only | tokens | — | :query | mismatch |