Open Telemetry Auto Instrumentation
This is a GHC plugin for automatically instrumenting a Haskell application with
open telemetry spans based on user configuration. The instrumentation
functionality is provided by
hs-opentelemetry
.
Quick start
Configuration
Configuration is supplied by a user defined TOML file that declares a set of
rules used to determine which functions should be instrumented. The plugin will
only consider top level functions that have type signatures when matching
against these rules. By default the plugin looks for a config file called
auto-instrument-config.toml
in the project root. You can change this by
passing a config file path as a plugin option, for example: -fplugin AutoInstrument -fplugin-opt AutoInstrument:my-config.toml
.
Config structure
- The
targets
key is an array of tables that specify how to identify a function
to instrument based on its type signature.
- These tables have a
type
field that can either "constructor"
or "constraints"
and a value
key with the value corresponding to the chosen type
.
"constructor"
is used to target the return type of the function. This will
typically be your application's monad. It is not necessary to provide all
arguments to this type and arguments that should be ignored can replaced with
an underscore.
"constraints"
allows for a set of constraints to be specified which must
all be present in the constraint context of a function in order for it to
be instrumented. The value
field should be an array of constraint types
which do not need to be fully applied and can have underscore wildcards.
- The
exclusions
key is an array with the same structure as targets
. If any
of these rules match a type signature, the corresponding declaration(s) will
not be instrumented.
Example config
# Targets are things that should be auto instrumented for tracing.
# "constructor" means that it should match the return type of the function
# while "constraints" means that all the constraints in the "value" array must
# be present in the constraint context of the function.
[[targets]]
type = "constructor"
value = "AppMonad"
[[targets]]
type = "constraints"
value = ["MonadUnliftIO"]
# Exclusions denote types that should not be instrumented. This is primarily
# needed for when a target constraint appears in a definition's context but
# doesn't apply directly to the return type, for example:
# server :: MonadUnliftIO m => ServerT Api m
[[exclusions]]
type = "constructor"
value = "ServerT"
[[exclusions]]
type = "constructor"
value = "ConduitT"
Known pitfalls
Functions that loop can be problematic when instrumented if a new span is
entered for each iteration. For example, if an application has a process that
continually performs some polling action in a loop, then instrumenting that
process would result in a space leak due to the mass of nested spans being
allocated and retained on the heap. One way for dealing with this is to define
a type synonym type NotInstrumented a = a
, add an exclusion rule for it to
the config, and apply it to the result type of any such looping functions:
type NotInstrumented a = a
loop :: NotInstrumented (MyApp ())
loop = do
...
loop
[[exclusions]]
type = "constructor"
value = "NotInstrumented"