Travesty
What is it?
Travesty is a utility library for Akka Streams. It has two uses:
-
generating structural diagrams of your Akka Streams, both as graphics and text (the latter useful for logging). Example:
-
generating Tinkerpop 3 / Gremlin graph data structures - usable for e.g. writing tests that check stage sequencing in dynamically constructed Streams.
What’s new?
0.9.2
-
Switched to Scala 2.13 and updated dependencies (also relased support for 2.6.10 to 2.6.14 for Scala 2.12, as 0.9.1)
0.9.1
-
Fixed issue with graph not generating when some of the stages have no names (thanks to @FXHibon for the fix).
0.9
-
Added support for partial graphs (see below for example),
-
Added support for Akka 2.5.10-11.
0.8
-
Added "manual support" for typed diagrams/graphs (see Typed diagrams),
-
made Graphviz engine selection configurable for better speed/compatibility (#3),
-
fixed #4,
-
extended Akka version support up to 2.5.9 .
How to use it
Include in your project
Add to your build.sbt
//repo of dependency used for text rendering
resolvers += "indvd00m-github-repo" at "https://raw.githubusercontent.com/indvd00m/maven-repo/master/repository"
"net.mikolak" %% "travesty" % "0.9_AKKAVERSION"
//for example "net.mikolak" %% "travesty" % "0.9_2.5.11"
Where AKKAVERSION
is the version of Akka (Streams) you’re using in your project. Currently supported are versions 2.5.[4-11]
, and Scala 2.12
.
You can review all available versions on Maven Central.
Diagram generation
import net.mikolak.travesty
import net.mikolak.travesty.OutputFormat
val graph = ??? //your graph here
//render as image to file, PNG is supported as well
travesty.toFile(graph, OutputFormat.SVG)("/tmp/stream.svg")
//render as text "image"
log.info(travesty.toString(graph)) //take care NOT to wrap the text
Example text output
Here’s how a typical travesty.toString(graph)
may look like:
********* *******
**** **** ** **
* *
*INLET* * seqSink *
* *
**** **** ●●● ** **
********* ●●● ●● ●● *******
●●●●● ●● ●●
●●●●●● ● ●●●●● ●
●●●●● ********* ********* ●●●●●
●●●● **** **** *** *** ●●●
* * ●● * *
ZipWith2 ●●●●●●●●● ●● * broadcast *
* * ●● * *
**** **** ● *** ***
●●●● ********* ********* ●●
●●●●●●●●●● ●●●● ●
●●●● ● ●●●●● ●●
************* ●● ●●●●●● *************
******************* ●● ******* *******
** ** * *
singleSource * *OUTLET* *
** ** * *
******************* ******* *******
************* *************
Flow[String,String,akka.NotUsed]
And here’s the same example in a vertical orientation, i.e. travesty.toString(graph, direction = BottomToTop)
:
************* *********
******************* **** ****
** ** * *
singleSource *INLET*
** ** * *
******************* **** ****
************* *********
● ●
● ●
● ●
● ●
● ●
● ●
● ●
● ●
● ●
● ● ● ●
●● ● ●
● ● ● ●●
●●● ●●●
● ●
*********
**** ****
* *
ZipWith2
* *
**** ****
*********
●
●
●
●
●
●
●
●
●
●● ●
● ●
●●
●
*********
*** ***
* *
* broadcast *
* *
*** ***
*********
● ●
● ●
● ●
● ●
● ●
● ●
● ●
● ●
● ●
●● ●●
● ● ● ●
●●● ●●●
● ●
************* *******
******* ******* ** **
* *OUTLET* * seqSink
* * * *
* *
******* ******* ** **
************* *******
Flow[String,String,akka.NotUsed]
Naming stages
Travesty uses the name
attribute, which all graph stages have, to label the nodes of the graphs. This means you can easily override the naming by invoking .named
on the relevant stage.
This example:
Source.single("t").named("beginning") (1)
.map(_ + "a")
.to(Sink.ignore.named("end")) (2)
-
custom name
-
another custom name
will render as:
Alternatively, if you’re making a one-shot sketch, you can render the image as an SVG, and edit the names as text in any SVG editor such as Inkscape.
Partial graphs
Travesty now supports Akka Stream graphs with any shape.
For example, this:
Flow[String].map(_ + "a").to(Sink.ignore)
will render as:
The labels for open inlets and outlets are configurable via the partial-names
section of the config:
travesty.partial-names {
inlet = "*INLET*"
outlet = "*OUTLET*"
}
Typed diagrams
Currently, it works like this:
import net.mikolak.travesty
import net.mikolak.travesty.OutputFormat
import registry._ //adds special .↓ and .register methods to stages
val graph = Source.single("1").↓.via(Flow[String].map(_.toInt).↓).to(Sink.seq)
//render as image to file, PNG is supported as well
travesty.toFile(graph, OutputFormat.SVG)("/tmp/stream.svg")
register
, aliased to ↓
, is a special pass-through extension method that allows Travesty to recognize the types going through your stream. Append .register
/.↓
to every stage you need type labels for.
Automatic support is coming, but unfortunately is a non-trivial problem to solve. For more details, see issue #1.
Graph testing
import net.mikolak.travesty
import gremlin.scala._ //traversal operations
val graph = ??? //your graph here
val tested = travesty.toAbstractGraph(graph)
//checks whether the only path through the stream has length two
tested.E().simplePath().toList() must have size 2
For more examples, see e.g. TravestyToGraphSpec
.
For general examples of what you can do with Gremlin in Scala, see the appropriately named gremlin-scala project.
Advanced usage
For further tweaking the rendering, you can use LowLevelApi
:
val vizGraph = LowLevelApi.toVizGraph(travesty.toAbstractGraph(graph))
//use the instance to change splines, node shapes, etc. etc.
//and finally, use the Java API to render
vizGraph.render(Format.PNG)
How does it work?
Generally, creating a graph of an Akka Stream is hard. This is because it’s difficult to "get to" the internals of a Stream and infer its structure. There definitely is no easy solution.
Travesty "cheats" by using the internal Traversal
API. The Traversal
is a stack-like structure containing instructions on how to construct a running Stream
.
This stack is parsed and converted into a Gremlin graph, convenient for annotating, pre-processing (e.g. additional decoration of Sources and Sinks), and testing.
The Gremlin graph is converted into a Graphviz graph, using graphviz-java.
Finally, the Graphviz graph is rendered into the required output format.
Caveats
No materialization annotations (for now)
Completely doable, but not present in the current version. Track #2 to be notified when this gets added.
Represents the graph "blueprint", not running stream
The graph/diagram generated from the Traversal
object does not correspond 1:1 to what will be present in the running Stream. There are at least two reasons for this:
-
the default materializer uses fusing to join stages that can be processed synchronously;
-
there can be other optimizations used by the materializer, such as ignoring stages, adding new stages, etc. Currently, the most prominent are the "virtual"
Sink
stages that can appear in some scenarios.
Slooooow
graphviz-java
provides several implementations of Graphviz to use. However, the one selected as default by travesty
, for maximum portability, is also the slowest one. While generating the graph is always fast, rendering the diagram may take up to ~10 seconds.
If you would like to try switching to a faster engine, see reference.conf
for more info.
Tight coupling with Akka Stream’s internals
As mentioned before, travesty
uses the internal API for graph/diagram generation. This is why the version number follows Akka’s versioning scheme.