Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rendering the Diff

Text and JSON are right for tools; people reviewing a change want a page they can read and a picture. In this section the diff becomes an HTML report, written by an XSLT stylesheet, and a diagram of the participants of each route, drawn by the drawing module of Project: Drawing Diagrams with SVG.

The diff as XML

XSLT works on trees, so the first step is the diff as XML. The records of trace:diff and trace:versions become elements with one attribute per field; a field that is empty becomes no attribute:

diff-xml.xq:

(: What changed between two trace files, as XML for the report. :)
import module namespace trace = "urn:example:trace" at "trace.xqm";
declare option output:indent "yes";

declare variable $before external := "before.json";
declare variable $after external := "after.json";

let $old := trace:load($before)
let $new := trace:load($after)
return <diff before="{ $before }" after="{ $after }">{
  for $version in trace:versions($old, $new)
  return <version service="{ $version?service }">{
    $version?before ! attribute before { . },
    $version?after ! attribute after { . }
  }</version>,
  for $change in trace:diff($old, $new)
  return <change route="{ $change?route }" caller="{ $change?caller }"
                 callee="{ $change?callee }" before="{ $change?before }"
                 after="{ $change?after }"/>
}</diff>
$ sxml xquery --output=diff.xml diff-xml.xq
$ cat diff.xml
<diff before="before.json" after="after.json">
  <version service="vane" before="2.3.0" after="2.4.0"/>
  <change route="GET /api/getFlags" caller="vane" callee="postgresql" before="1" after="5"/>
  <change route="POST /api/killFlag" caller="vane" callee="hooks.example.com" before="0" after="1"/>
  <change route="POST /api/killFlag" caller="vane" callee="postgresql" before="2" after="3"/>
  <change route="POST /api/killFlag" caller="vane" callee="redis" before="1" after="0"/>
</diff>

An HTML report

The report has a table of versions and a table of changes for each route. xsl:for-each-group makes the routes; a template rule for each kind of change sets the row's class, which the page's style colours:

report.xsl:

<!-- The trace diff (from diff-xml.xq) as an HTML report. With the
     parameter drawings, a list of SVG files from drawing.xq, each route
     shows the drawing whose title names it. -->
<xsl:stylesheet version="4.0" expand-text="yes"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:svg="http://www.w3.org/2000/svg"
    exclude-result-prefixes="svg">
  <xsl:output method="html" html-version="5" indent="yes"/>
  <xsl:param name="drawings" select="''"/>

  <xsl:template match="/diff">
    <html>
      <head>
        <title>Trace diff: {@before} and {@after}</title>
        <style>
          td, th {{ padding: 2px 12px; text-align: left }}
          .added {{ color: #2e7d32 }}
          .removed {{ color: #c62828; text-decoration: line-through }}
          .changed {{ color: #b26a00 }}
        </style>
      </head>
      <body>
        <h1>Trace diff</h1>
        <p>From <code>{@before}</code> to <code>{@after}</code>.</p>
        <h2>Versions</h2>
        <table>
          <tr><th>Service</th><th>Before</th><th>After</th></tr>
          <xsl:apply-templates select="version"/>
        </table>
        <xsl:for-each-group select="change" group-by="@route">
          <h2>{current-grouping-key()}</h2>
          <table>
            <tr><th>Caller</th><th>Callee</th><th>Before</th><th>After</th></tr>
            <xsl:apply-templates select="current-group()"/>
          </table>
          <xsl:copy-of select="tokenize($drawings) ! doc(.)
              /svg:svg[starts-with(svg:title, current-grouping-key() || ':')]"/>
        </xsl:for-each-group>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="version">
    <tr><td>{@service}</td><td>{(@before, 'none')[1]}</td><td>{(@after, 'none')[1]}</td></tr>
  </xsl:template>

  <!-- A call that is new, gone, or made a different number of times. -->
  <xsl:template match="change">
    <xsl:param name="kind" select="'changed'"/>
    <tr class="{$kind}">
      <td>{@caller}</td><td>{@callee}</td><td>{@before}</td><td>{@after}</td>
    </tr>
  </xsl:template>

  <xsl:template match="change[@before = 0]" priority="1">
    <xsl:next-match><xsl:with-param name="kind" select="'added'"/></xsl:next-match>
  </xsl:template>

  <xsl:template match="change[@after = 0]" priority="1">
    <xsl:next-match><xsl:with-param name="kind" select="'removed'"/></xsl:next-match>
  </xsl:template>
</xsl:stylesheet>

A row for an added call matches both change and change[@before = 0]. The second rule has the higher priority and wins; it passes the kind on with xsl:next-match, which runs the next best rule, the general one, with the parameter. The braces in the style are doubled because expand-text="yes" makes single braces text value templates.

$ sxml transform report.xsl diff.xml
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Trace diff: before.json and after.json</title>
    <style>
          td, th { padding: 2px 12px; text-align: left }
          .added { color: #2e7d32 }
          .removed { color: #c62828; text-decoration: line-through }
          .changed { color: #b26a00 }
        </style>
  </head>
  <body>
    <h1>Trace diff</h1>
    <p>From <code>before.json</code> to <code>after.json</code>.</p>
    <h2>Versions</h2>
    <table>
      <tr>
        <th>Service</th>
        <th>Before</th>
        <th>After</th>
      </tr>
      <tr>
        <td>vane</td>
        <td>2.3.0</td>
        <td>2.4.0</td>
      </tr>
    </table>
    <h2>GET /api/getFlags</h2>
    <table>
      <tr>
        <th>Caller</th>
        <th>Callee</th>
        <th>Before</th>
        <th>After</th>
      </tr>
      <tr class="changed">
        <td>vane</td>
        <td>postgresql</td>
        <td>1</td>
        <td>5</td>
      </tr>
    </table>
    <h2>POST /api/killFlag</h2>
    <table>
      <tr>
        <th>Caller</th>
        <th>Callee</th>
        <th>Before</th>
        <th>After</th>
      </tr>
      <tr class="added">
        <td>vane</td>
        <td>hooks.example.com</td>
        <td>0</td>
        <td>1</td>
      </tr>
      <tr class="changed">
        <td>vane</td>
        <td>postgresql</td>
        <td>2</td>
        <td>3</td>
      </tr>
      <tr class="removed">
        <td>vane</td>
        <td>redis</td>
        <td>1</td>
        <td>0</td>
      </tr>
    </table>
  </body>
</html>

The participants as a diagram

The drawing module of the SVG project draws any diagram of nodes and edges, and a class on a node or an edge marks it as added, removed or changed. It lives in the SVG project's directory, and draw.xqm here is a symbolic link to ../svg/draw.xqm, so both projects use the same file.

participants.xq makes the diagram of one route: a node for each participant, before or after, and an edge for each call between two of them, labelled with its count. What only the new file has is added, what only the old one has is removed, and a call whose count changed is changed. A database is drawn as a store: a callee that some client span marks with db. The owners' webhook records no spans of its own either, but it is a web server, not a database, so it is drawn as a box like the programs:

participants.xq:

(: The participants of one route, before and after, as a diagram for
   draw.xqm: what was added, removed or changed has a class, and a
   database is drawn as a store. :)
import module namespace trace = "urn:example:trace" at "trace.xqm";
declare option output:indent "yes";

declare variable $before external := "before.json";
declare variable $after external := "after.json";
declare variable $route external := "GET /api/getFlags";

(: The calls of the route, from a map of cardinalities. :)
declare function local:calls($cardinality as map(*)) as map(*) {
  map:filter($cardinality, fn($key, $edge) { $edge?route = $route })
};

(: Whether a service takes part in one of the calls. :)
declare function local:takes-part($name as xs:string, $calls as map(*)) as xs:boolean {
  some $edge in map:items($calls) satisfies $name = ($edge?caller, $edge?callee)
};

let $old := trace:load($before)
let $new := trace:load($after)
let $old-calls := local:calls(trace:cardinality($old))
let $new-calls := local:calls(trace:cardinality($new))
let $calls := map:merge(($old-calls, $new-calls))
let $databases := ($old, $new)//span[@db]/@peer
return <diagram title="{ $route }: { $before } and { $after }">{
  for $name in distinct-values(map:items($calls) ! (?caller, ?callee))
  return <node id="{ $name }" label="{ $name }">{
    if ($name = $databases) then attribute kind { "store" } else (),
    if (not(local:takes-part($name, $old-calls))) then attribute class { "added" }
    else if (not(local:takes-part($name, $new-calls))) then attribute class { "removed" }
    else ()
  }</node>,
  for $key in map:keys($calls)
  let $count-before := $old-calls($key)?count otherwise 0
  let $count-after := $new-calls($key)?count otherwise 0
  return <edge from="{ $calls($key)?caller }" to="{ $calls($key)?callee }">{
    if ($count-before = $count-after) then attribute label { $count-after }
    else (
      attribute label { `{ $count-before } → { $count-after }` },
      attribute class {
        if ($count-before = 0) then "added"
        else if ($count-after = 0) then "removed"
        else "changed"
      }
    )
  }</edge>
}</diagram>
$ sxml xquery '--param=route=POST /api/killFlag' --output=kill.xml participants.xq
$ cat kill.xml
<diagram title="POST /api/killFlag: before.json and after.json">
  <node id="vane-console" label="vane-console"/>
  <node id="vane" label="vane"/>
  <node id="redis" label="redis" kind="store" class="removed"/>
  <node id="postgresql" label="postgresql" kind="store"/>
  <node id="hooks.example.com" label="hooks.example.com" class="added"/>
  <edge from="vane-console" to="vane" label="1"/>
  <edge from="vane" to="redis" label="1 → 0" class="removed"/>
  <edge from="vane" to="postgresql" label="2 → 3" class="changed"/>
  <edge from="vane" to="hooks.example.com" label="0 → 1" class="added"/>
</diagram>

drawing.xq draws a diagram with the module:

drawing.xq:

(: Draws a diagram (the context item) with the drawing module. :)
import module namespace draw = "urn:example:draw" at "draw.xqm";
declare option output:indent "yes";

draw:svg(/diagram)

The drawings of both routes are in the project directory, and drawing them again gives the same files:

$ sxml xquery --context-item=kill.xml --output=kill.svg drawing.xq
$ cmp kill.svg diff-kill.svg && echo same
same
$ sxml xquery --output=flags.xml participants.xq
$ sxml xquery --context-item=flags.xml --output=flags.svg drawing.xq
$ cmp flags.svg diff-flags.svg && echo same
same

Listing the flags:

The participants of GET /api/getFlags, before and after

Killing a flag:

The participants of POST /api/killFlag, before and after

The modules of the service are not in the drawings: a call from one module to another is a function call inside the one program, and its internal span is not a call. The drawings show what crosses the network, and that is where this change shows: more queries, a cache that is no longer read, and a new webhook.

The drawings in the report

An HTML page can hold SVG directly. With the parameter drawings, a list of SVG files, report.xsl copies into each route's section the drawing whose title begins with the route, so the report becomes one file with everything in it:

$ sxml transform --param='drawings=diff-flags.svg diff-kill.svg' --output=report.html report.xsl diff.xml
$ sxml xpath 'count(html-doc("report.html")//*:svg)'
2

The stylesheet reads the drawings with doc(), as XML files; html-doc reads the HTML page back, with the SVG elements in their own namespace, as a browser does. Open report.html in a browser to see the tables and the two drawings.