{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-| This module provides a GHC plugin that will export open telemetry metrics
for your build. Specifically, this plugin will create one span per module
(recording how long that module took to build) and one sub-span per phase
of that module's build (recording how long that phase took).
-}
module OpenTelemetry.Plugin
( -- * Plugin
plugin
) where
import Control.Monad.IO.Class (MonadIO(..))
import GHC.Types.Target (Target(..), TargetId(..))
import OpenTelemetry.Context (Context)
import GHC.Plugins
( CoreToDo(..)
, GenModule(..)
, HscEnv(..)
, Plugin(..)
)
import qualified Control.Monad as Monad
import qualified Data.Text as Text
import qualified GHC.Plugins as Plugins
import qualified GHC.Utils.Outputable as Outputable
import qualified OpenTelemetry.Plugin.Shared as Shared
wrapTodo :: MonadIO io => IO Context -> CoreToDo -> io CoreToDo
wrapTodo getParentContext todo =
case todo of
CoreDoPasses passes ->
fmap CoreDoPasses (traverse (wrapTodo getParentContext) passes)
_ -> liftIO do
let sdoc = Outputable.ppr todo
let label =
Outputable.showSDocOneLine Outputable.defaultSDocContext sdoc
(_, beginPass, endPass) <- do
Shared.makeWrapperPluginPasses False getParentContext (Text.pack label)
let beginPluginPass =
CoreDoPluginPass ("begin " <> label) \modGuts -> liftIO do
beginPass
pure modGuts
let endPluginPass =
CoreDoPluginPass ("end " <> label) \modGuts -> liftIO do
endPass
pure modGuts
pure (CoreDoPasses [ beginPluginPass, todo, endPluginPass ])
-- | GHC plugin that exports open telemetry metrics about the build
plugin :: Plugin
plugin =
Plugins.defaultPlugin
{ driverPlugin
, pluginRecompile
, installCoreToDos
}
where
driverPlugin _ hscEnv@HscEnv{ hsc_targets } = do
let rootModuleNames = do
Target{ targetId = TargetModule rootModuleName } <- hsc_targets
pure (Plugins.moduleNameString rootModuleName)
Shared.setRootModuleNames rootModuleNames
Shared.initializeTopLevelContext
pure hscEnv
installCoreToDos _ todos = do
module_ <- Plugins.getModule
let moduleName_ = moduleName module_
let moduleText = Text.pack (Plugins.moduleNameString moduleName_)
(getCurrentContext, firstPluginPass, lastPluginPass) <- do
liftIO (Shared.makeWrapperPluginPasses True Shared.getTopLevelContext moduleText)
let firstPass =
CoreDoPluginPass "begin module" \modGuts -> liftIO do
firstPluginPass
pure modGuts
let lastPass =
CoreDoPluginPass "end module" \modGuts -> liftIO do
lastPluginPass
isRoot <- Shared.isRootModule (Plugins.moduleNameString moduleName_)
-- Flush metrics if we're compiling one of the root
-- modules. This is to work around the fact that we don't
-- have a proper way to finalize the `TracerProvider`
-- (since the finalizer would normally be responsible for
-- flushing any last metrics).
--
-- You might wonder: why don't we end the top-level span
-- here? Well, we don't know which one of the root modules
-- will be the last one to be compiled. However, flushing
-- once per root module is still fine because flushing is
-- safe to run at any time and in practice there will only
-- be a few root modules.
Monad.when isRoot Shared.flush
pure modGuts
newTodos <- traverse (wrapTodo getCurrentContext) todos
pure ([ firstPass ] <> newTodos <> [ lastPass ])
pluginRecompile = Plugins.purePlugin