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
.
- Add this package as a project dependency
- Create a file called
auto-instrument-config.toml
in the project root directory:Replace[[targets]] type = "constructor" value = "MyAppMonad"
MyAppMonad
with your application's primary monad. This monad needs to have an instance forMonadUnliftIO
, otherwise you'll get a type error. - Initialize the global tracer provider as part of application startup. The
plugin will not insert spans until after the gobal tracer provider has been
initialized. See the
hs-opentelemetry-sdk
documentation for instructions. - Pass the
-fplugin AutoInstrument
argument to GHC when compiling the project. This can be done project-wide in the*.cabal
orpackage.yaml
file usingghc-options: -fplugin AutoInstrument
, or by adding{- OPTIONS_GHC -fplugin AutoInstrument -}
to individual modules. - Only top-level functions that have type signatures with a return type that matches the target monad will be instrumented.
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
.
- 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 avalue
key with the value corresponding to the chosentype
."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. Thevalue
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 astargets
. If any of these rules match a type signature, the corresponding declaration(s) will not be instrumented.
# 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"
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"