Modality Concepts
# Events and Timelines
Modality's data model is based on events and timelines. An event simply records that something happened. A timeline is a sequence of events which happened in order. Timelines can represent different things in your system, depending on the implementation. In a desktop application, each thread could be a timeline, whereas in an embedded system you might have a timeline per microcontroller.
You can think of an event as something happening in your system, and the timeline as being the 'place' that it happened.
imu o-------o--------o----------o--------o------->
start init measure measure report
# Event and Timeline names
Events and timelines are identified in two ways. The first is by name: events have names, and timelines have names. They aren't necessarily unique, so this isn't a great way to identify a specific event occurrence at a specific point in time. But they are very useful for understanding the collected trace, and for categorization.
Some example event names might be:
startup
/shutdown
send_motor_control_command
/receive_motor_control_command
read_imu
over_voltage_detected
Some example timeline names might be:
ui_thread
imu
gateway_connection
# event@timeline
notation
It is very common in Modality to deal with events occurring on
timelines, by name. We do this when printing recorded trace
information using modality log
, when composing SpeQTr queries for
use in modality query
, and in many other places. For this purpose,
we use @
in a fashion that is similar to an email address.
For example, events named measure
on timelines named imu
could be
referred to as measure@imu
.
TIP
When used in a query context, event@timeline
is automatically
expanded to the query _.name = 'event' AND _.timeline.name = 'timeline'
.
# Event Coordinates
Names are useful for referring to some kind of thing happening on some kind of timeline, but we often need to be more specific. For this, we use event coordinates. Event coordinates are composed of two parts: a timeline id, and an event id. They look like this:
60a20a3f243a439baa6211d4969a065e:03a6e0
------------------------------- ------
\ \
-- Timeline Id -- Event Id
The first part of this is the Timeline ID. This is used to identify a specific timeline instance stored in Modality. Each timeline has a name, but many timelines may share the same name (for example, if you record multiple traces from the same system). The Timeline ID is used to identify a specific event trace from the part of the system you've identified with that name.
TIP
Be careful not to confuse timeline names with timeline IDs! Many timeline instances can share the same name, but timeline IDs are used only once.
The second part of the event coordinate (after the :
) identifies the
specific event on the timeline.
# Event and Timeline Attributes
In addition to their names, Events and Timelines can have arbitrary
key/value metadata attached to them called attributes. Attribute
keys are always strings; they use .
characters as a namespacing
mechanism. Attribute values can be a scalar value of a number of
types: strings, numbers, timestamps, etc.
Some example attribute keys and values might be:
- event.name = "startup"
- event.timestamp = 1798789234ns
- timeline.name = "imu"
- timeline.mycorp.build_number = 157
All attributes attached to events start with event.
, and all
attributes attached to timelines start with timeline.
. But, this can
be skipped in places where the context is clear. For example, if
you're writing an event pattern, instead of event.name
you can just
write name
.
You can attach any attribute keys to events and timelines, but some attribute keys are special. Modality looks for these special keys to understand various things about the trace. This includes event timestamps, and information for causality tracking (see below).
For specific attribute semantics, see Special Attributes.
# Connections Between Timelines (Causality)
Timelines can represent a linear sequence of events, from a single 'place' in your system. But actual systems are made of many interacting components; this is what makes them powerful, but also makes them difficult to analyze and test.
This is where Modality is different from other event databases. Modality analyzes the stored events and timelines to derive connections between the timelines. For example, if one timeline has a 'sent message' event, and another has a 'received message' event, and those share the same id (written as event attributes), Modality can create an inter-timeline connection between those two timelines.
start config_imu recv_imu_report
control o-------o----------------------------------o---->
\ /
\ /
imu o----------o--------o----------o--------o------->
start init measure measure report
This is useful in a lot of different ways. The most important, and the reason this is built into Modality, is to have a basis of 'happens-before' and 'happens-after' across different components of your system. This idea is called causality. Since a system's current state and inputs dictate what it will do next, events that come before could have contributed to causing events that come later. By recording system execution and runtime parameters with events and their attributes, and keeping those events in order, you can see which conditions ultimately contribute to causing good or bad behavior.
This example from the modality log
command shows recorded causality
information. The white arrows between the vertical colored timelines
denote interactions. We see a complete interaction from Producer sending measurement message
on the producer
timeline to Received measurement message
on the consumer
timeline. This establishes a
causal link, meaning that events on the consumer
timeline after it
received the measurement message may have been caused by events on the
producer
timeline before it sent the message.
○───╮ ║ ║ ║ [Interaction i0001] "Producer sending measurement message" @ producer [df2571efa4ee4186b4926ed0b31b0ec7:160c0b]
║ │ ║ ║ ║ destination = consumer
║ │ ║ ║ ║ name = Producer sending measurement message
║ │ ║ ║ ║ sample = 1
║ │ ║ ║ ║ severity = info
║ │ ║ ║ ║ source.file = tracing-modality/examples/monitored_pipeline.rs
║ │ ║ ║ ║ source.line = 251
╟─────╮ ║ ║ ║ [Interaction i0002]
║ │ │ ║ ║ ║
■ │ │ ║ ║ ║ "Sending heartbeat message" @ producer [df2571efa4ee4186b4926ed0b31b0ec7:18bd47]
║ │ │ ║ ║ ║ destination = monitor
║ │ │ ║ ║ ║ name = Sending heartbeat message
║ │ │ ║ ║ ║ severity = info
║ │ │ ║ ║ ║ source.file = tracing-modality/examples/monitored_pipeline.rs
║ │ │ ║ ║ ║ source.line = 436
║ │ │ ║ ║ ║
║ ╰──────▶○ ║ [Interaction i0001] "Received measurement message" @ consumer [8ab93cb714214fe38d4c7656dc14dc47:19510f]
║ │ ║ ║ ║ name = Received measurement message
║ │ ║ ║ ║ sample = 1
║ │ ║ ║ ║ severity = info
║ │ ║ ║ ║ source.file = tracing-modality/examples/monitored_pipeline.rs
║ │ ║ ║ ║ source.line = 309
# System Level Reasoning
# An Abstract View
Modality thinks about causality by building on two basic axioms about the happened-after relation between events:
- Timeline-local ordering: If an event a and b occur on the same timeline, and a occurs earlier on the timeline than b according the the explicit ordering given on each timeline, then b happened after a.
T -----------o-------------------o---------------->
a b
- Cross-timeline ordering: If an event a on timeline T1 represents a communication of some piece of information, and if an event b on timeline T2 represents an observation of that same information, then b happened after a.
Here we suppose that the value of x (42) is communicated from one timeline to the other:
a(x=42)
T1 --------o--------------------------------------->
\
\
T2 -----------o------------------------------------>
b(x=42)
These are the building blocks. But to them, we add a fundamental law:
- Transitive Law of Causality: If an event b happened after event a, and if an event c happened after b, then c happened after a.
a(x=42)
T1 --------o--------------------------------------->
\
\
T2 -----------o-------------o---------------------->
b(x=42) c
And because we have transitivity, we can then work with transitive closure of the happens-after relation, to consider all events that happened after some point, whether directly or indirectly. That is, you can glue these things together.
For example, here we add a second kind of interaction, governed by communicating the value y
.
a(x=42)
T1 --------o--------------------------------------->
\
\ c(y=12)
T2 -----------o-------o------o------o-------------->
b(x=42) \
\
T3 ------------------------------------o----------->
d(y=12)
But because happens-after is transitive, you could also choose to elide everything on T2 (in case you don't care about it, for some piece of analysis):
a(x=42)
T1 --------o--------------------------------------->
\
- - - - - - - - - - - - -
\
T3 ------------------------------------o----------->
d(y=12)
While event d is not directly related to a via information observations, the events
on T2
allow us to transitively conclude that d happens after a.
Note that our definition of happens-after does not necessarily define a relationship between every event in the system. Take a 'join' pattern, for example:
a
T1 --------o--------------------------------------->
\
\ d
T2 -----------o-------------o---------------------->
b /
/
T3 ----------------------o-------------------------->
c
In the above scenario, there is no known relationship between events a and c; we have no way of knowing if one of them occurred before the other. happens-after is a partial order, wherein the ordering between some pairs of events may not be defined.
# Practical Application in Modality
Modality takes these simple rules and builds them out into a practical system that is usable for real-world systems engineering applications.
Entire systems can be composed out of timelines on the basis of local interactions. The x and y we used above to represent the abstract communication of some piece of information are realized, in real systems, by the nonces and sequence IDs that are a part of any network protocol or middleware stack.
This information may be provided directly at ingest time, using special interaction attributes. Or, you may configure a Modality Workspace to use existing attributes on your events. We call this style of configuration custom interactions. This makes it easy to use platform-native tracing / observability solutions for each component in a diverse system, importing data from each source into Modality using one of our many integrations, then connecting them together with configuration.
Because causality is transitive, all you need to do is stitch together components at their boundaries. Modality will then turn that into a coherent view over the whole system. We call this assembly a System Trace.
New trace data can be added after the fact, or asynchronously; Modality uses information recorded in the trace itself to determine that the interactions exist. This kind of operational flexibility is key for HiL-test and field testing scenarios, where some of the data may be recorded to local storage before being ingested for later analysis. It also allows for information to arrive at different cadences, for different components in a large system-of-systems.
Analytics built on top of Modality's data model (including Conform specifications) are very robust to system changes. We evaluate queries by computing the transitive closure of the happens-after relationship, sometimes repeatedly, for various events in the causal graph. So, changes to intermediate events or even to the system structure between point A and point B have no impact on the result, unless you are specifically looking for them. (A naive implementation of this is computationally intractable for any real system; Modality uses lazy computation, a variety of indexing strategies, and a custom-built query planner to make it fast).
It's tempting to lean on clock synchronization when thinking about event ordering in complex systems. But this inevitably falls down; the system grows, your clock precision isn't good enough, your clocks aren't actually synced as reliably as you thought they were, the reasons are many. Modality's model of happens-after is completely agnostic to clock synchronization (although we do provide some conveniences to help you understand and specify time-based behaviors), so it's robust to these kinds of failures. It guides you towards analysis and specifications that are robust in this way as well.
# The SpeQTr Language
Once you have collected data about your running system you can use Auxon's SpeQTr (opens new window) as a query language to ask nuanced questions about what happened. You can look for local or system-wide patterns of events, filter on event and timeline attributes, and calculate aggregate statistics. This lets you confirm that your system is doing exactly what it's supposed to or pinpoint the place where things went wrong. In addition, Modality has tools to help you understand the general structure of your system and find areas of risk.
Here you can see an example of a simple query, finding all of the
places in the collected data where the producer
sends a measurement
and then the consumer
receives it.
"Producer sending measurement message"@producer FOLLOWED BY
"Received measurement message"@consumer
Running this query gives the below results:
Result 1:
═════════
■ ║ "Producer sending measurement message" @ producer [bd40e6ad2b4747a58536cb3c850ffb14:096cb1]
║ ║ destination=consumer
║ ║ nonce=-4882720374935248381
║ ║ sample=-1
║ ║ severity=info
║ ║ source.file=tracing-modality/examples/monitored_pipeline.rs
║ ║ source.line=251
║ ║ source.module=monitored_pipeline::producer
║ ║ timestamp=1663319617641549835ns
║ ║ query.label='Producer sending measurement message'@producer
║ ║
╚═»╗ producer interacted with consumer at 5be148ebc1b84ceba6defedf4667a007:0ba86c
║ ║
║ ■ "Received measurement message" @ consumer [5be148ebc1b84ceba6defedf4667a007:0ba86c]
║ ║ interaction.remote_nonce=-4882720374935248381
║ ║ interaction.remote_timeline_id=bd40e6ad-2b47-47a5-8536-cb3c850ffb14
║ ║ interaction.remote_timestamp=1663319617641282787ns
║ ║ sample=-1
║ ║ severity=info
║ ║ source.file=tracing-modality/examples/monitored_pipeline.rs
║ ║ source.line=309
║ ║ source.module=monitored_pipeline::consumer
║ ║ timestamp=1663319617641830705ns
║ ║ query.label='Received measurement message'@consumer
║ ║
Result 2:
═════════
■ ║ "Producer sending measurement message" @ producer [bd40e6ad2b4747a58536cb3c850ffb14:087c563e]
║ ║ destination=consumer
║ ║ nonce=-6868605139615718718
║ ║ sample=-1
║ ║ severity=info
║ ║ source.file=tracing-modality/examples/monitored_pipeline.rs
║ ║ source.line=251
║ ║ source.module=monitored_pipeline::producer
║ ║ timestamp=1663319617783789766ns
║ ║ query.label='Producer sending measurement message'@producer
║ ║
╚═»╗ producer interacted with consumer at 5be148ebc1b84ceba6defedf4667a007:08cb793d
║ ║
║ ■ "Received measurement message" @ consumer [5be148ebc1b84ceba6defedf4667a007:08cb793d]
║ ║ interaction.remote_nonce=-6868605139615718718
║ ║ interaction.remote_timeline_id=bd40e6ad-2b47-47a5-8536-cb3c850ffb14
║ ║ interaction.remote_timestamp=1663319617782985545ns
║ ║ sample=-1
║ ║ severity=info
║ ║ source.file=tracing-modality/examples/monitored_pipeline.rs
║ ║ source.line=309
║ ║ source.module=monitored_pipeline::consumer
║ ║ timestamp=1663319617788634110ns
║ ║ query.label='Received measurement message'@consumer
║ ║
...
These results allow you to quickly find events that match your query,
including attribute values for the matching events. To continue to
build a better understanding of what happened you can use the modality log
command to explore the surrounding trace.
# Workspaces and Segmentation
In Modality, all collected traces are stored together. In order to split the data into useful pieces, for different users and different kinds of analysis, Modality provides the Workspace and Segmentation features.
# Workspaces
Workspaces are like views onto the data lake. They provide a top-level
filtering to decide which timelines should be included. This works
on the basis of timeline attributes; this includes timeline names, but
can also include any custom timeline attributes which you may have
added. For example, you might have a workspace which looks at the
value of the fleet.geo
custom attribute, and selects only those
timelines with the value of Europe
.
Workspaces also contain additional configuration that is used to further split up the data for analysis (see 'Segmentation' below).
Nearly every operation you do in Modality will be in the context of
some workspace. When you install modalityd
, it comes pre-configured
with a workspace called default
that includes all data.
# Segmentation
Inside of a workspace, you can split up the timelines into different
segments. This is also done on the basis of timeline attributes,
similar to workspace filtering. The difference is that for segments,
we use the attribute values to split the data into chunks. For
example, inside a workspace, you might segment the timelines based on the
value of the fleet.model
custom attribute. This would give you a
bunch of segments, named after the model number, each containing the
timelines collected from systems with that model number.
You can have multiple segmentation methods in a single workspace, so you can cut up the data in different ways.
Many operations in Modality (and the applications built on it) work with the data from a single selected segment (the 'active' one). Others can be configured to work with a single segment, or in an aggregation mode across multiple segments.
The default
workspace comes pre-configured with a single
segmentation method based on the run_id
timeline attribute; this
attribute is provided by all of the Modality collectors as a way to
easily split up the data on the basis of infrastructure cycles. The
most recently collected segment will be active by default.
# How workspaces and segments are meant to be used
Workspaces and segments provide a very generic and dynamic way to split up your collected trace data. You can use them in a lot of different ways, but this is how we designed them to be used:
Workspaces should be used as a coarse, top-level filtering mechanism. You'll probably have a small handful of workspaces, each for a different use case. Most users shouldn't have to deal with more than 1 or 2 workspaces on a daily basis.
Segmentation methods should be configured to reflect some thing that exists in your workflow; maybe a test or CI run, or field test. They could align with a physical piece of infrastructure, like a specific drone or a test rig in your lab. You can have multiple segmentation methods in a workspace, so you can do all of these together.
# Operational Architecture
Modality is a client-server application. The server is called
modalityd
; the command-line client is just called modality
. There
is also an event conversion and router application called
modality-reflector
.
# modalityd
modalityd
is the database server component of Modality. It is
deployed as a single process with local storage, which is split into
multiple files based on the storage class. It implements a token-based
authentication and authorization system to manage data ingest and client
connections.
modalityd
is typically deployed on a central server, which is
accessed by all users who want to use it. It is suitable for both
cloud-based or on-premises deployment.
# modality-reflector
The reflector provides a few key functions in a Modality deployment:
- It manages configuration and execution of plugins for collectors, importing, and mutations.
- It connects back to modalityd, or to another reflector.
- It can add additional attributes to the timelines which pass through it.
Some simple Modality deployments may only need a single reflector, or may not need one at all (if you are using a tracing framework which directly supports the Modality event ingest protocol). Other deployments may use multiple reflectors.
Here are some common scenarios where you might want to deploy additional reflectors:
Some embedded collector and mutation plugins require direct access to peripheral hardware, like a JTAG probe. For these scenarios, you can run modality-reflector on the computer with the hardware installed, configured to run those plugins.
For network-based tracing systems, the network topology may not allow incoming connections from collection infrastructure. In this case, modality-reflector can be deployed in the inner network; all Modality network connections are client-initiated, so many different network topologies can be supported.
If you have a system-of-systems, you may want to deploy an intermediate modality-reflector for routing or annotation purposes. Each reflector process can add its own metadata, allowing clean separation between the information known to a system about itself, and information known about its operational context.
# modality
CLI
The Modality command-line client application (executed as just
modality
) is the primary user interface for most modality users. It
can be used to view logs, evaluate queries, set up and select
workspaces and segments, and perform administrative tasks.
# Visual Studio Code Extension
A sophisticated Visual Studio Code extension is available which provides a graphical user interface for browsing and visualizing Modality data. It is available in the VS Code Extension marketplace (opens new window).
You can also install it from inside VS Code's quick open (Ctrl+P
) dialog with:
ext install auxoncorp.speqtr