hs-opentelemetry-api-0.2.0.0: src/OpenTelemetry/Internal/Common/Types.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module OpenTelemetry.Internal.Common.Types (
InstrumentationLibrary (..),
AnyValue (..),
ToValue (..),
ShutdownResult (..),
FlushResult (..),
ExportResult (..),
parseInstrumentationLibrary,
detectInstrumentationLibrary,
) where
import Control.Exception (SomeException)
import Data.ByteString (ByteString)
import Data.Data (Data)
import qualified Data.HashMap.Strict as H
import Data.Hashable (Hashable)
import Data.Int (Int64)
import Data.String (IsString (fromString))
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
import qualified Language.Haskell.TH as TH
import qualified Language.Haskell.TH.Syntax as TH
import OpenTelemetry.Attributes (Attributes, emptyAttributes)
import Text.Regex.TDFA ((=~~))
{- | An identifier for the library that provides the instrumentation for a given Instrumented Library.
Instrumented Library and Instrumentation Library may be the same library if it has built-in OpenTelemetry instrumentation.
The inspiration of the OpenTelemetry project is to make every library and application observable out of the box by having them call OpenTelemetry API directly.
However, many libraries will not have such integration, and as such there is a need for a separate library which would inject such calls, using mechanisms such as wrapping interfaces,
subscribing to library-specific callbacks, or translating existing telemetry into the OpenTelemetry model.
A library that enables OpenTelemetry observability for another library is called an Instrumentation Library.
An instrumentation library should be named to follow any naming conventions of the instrumented library (e.g. 'middleware' for a web framework).
If there is no established name, the recommendation is to prefix packages with "hs-opentelemetry-instrumentation", followed by the instrumented library name itself.
In general, the simplest way to get the instrumentation library is to use 'detectInstrumentationLibrary', which uses the Haskell package name and version.
-}
data InstrumentationLibrary = InstrumentationLibrary
{ libraryName :: {-# UNPACK #-} !Text
-- ^ The name of the instrumentation library
, libraryVersion :: {-# UNPACK #-} !Text
-- ^ The version of the instrumented library
, librarySchemaUrl :: {-# UNPACK #-} !Text
, libraryAttributes :: Attributes
}
deriving (Ord, Eq, Generic, Show, TH.Lift)
instance Hashable InstrumentationLibrary
instance IsString InstrumentationLibrary where
fromString :: String -> InstrumentationLibrary
fromString str = InstrumentationLibrary (fromString str) "" "" emptyAttributes
{- | An attribute represents user-provided metadata about a span, link, or event.
'Any' values are used in place of 'Standard Attributes' in logs because third-party
logs may not conform to the 'Standard Attribute' format.
Telemetry tools may use this data to support high-cardinality querying, visualization
in waterfall diagrams, trace sampling decisions, and more.
-}
data AnyValue
= TextValue Text
| BoolValue Bool
| DoubleValue Double
| IntValue Int64
| ByteStringValue ByteString
| ArrayValue [AnyValue]
| HashMapValue (H.HashMap Text AnyValue)
| NullValue
deriving stock (Read, Show, Eq, Ord, Data, Generic)
deriving anyclass (Hashable)
-- | Create a `TextAttribute` from the string value.
instance IsString AnyValue where
fromString :: String -> AnyValue
fromString = TextValue . fromString
{- | Convert a Haskell value to an 'Any' value.
@
data Foo = Foo
instance ToValue Foo where
toValue Foo = TextValue "Foo"
@
-}
class ToValue a where
toValue :: a -> AnyValue
instance ToValue Text where
toValue :: Text -> AnyValue
toValue = TextValue
instance ToValue Bool where
toValue :: Bool -> AnyValue
toValue = BoolValue
instance ToValue Double where
toValue :: Double -> AnyValue
toValue = DoubleValue
instance ToValue Int64 where
toValue :: Int64 -> AnyValue
toValue = IntValue
instance ToValue ByteString where
toValue :: ByteString -> AnyValue
toValue = ByteStringValue
instance (ToValue a) => ToValue [a] where
toValue :: (ToValue a) => [a] -> AnyValue
toValue = ArrayValue . fmap toValue
instance (ToValue a) => ToValue (H.HashMap Text a) where
toValue :: (ToValue a) => H.HashMap Text a -> AnyValue
toValue = HashMapValue . fmap toValue
instance ToValue AnyValue where
toValue :: AnyValue -> AnyValue
toValue = id
data ShutdownResult = ShutdownSuccess | ShutdownFailure | ShutdownTimeout
-- | The outcome of a call to @OpenTelemetry.Trace.forceFlush@ or @OpenTelemetry.Logs.forceFlush@
data FlushResult
= -- | One or more spans or @LogRecord@s did not export from all associated exporters
-- within the alotted timeframe.
FlushTimeout
| -- | Flushing spans or @LogRecord@s to all associated exporters succeeded.
FlushSuccess
| -- | One or more exporters failed to successfully export one or more
-- unexported spans or @LogRecord@s.
FlushError
deriving (Show)
data ExportResult
= Success
| Failure (Maybe SomeException)
-- | Parses a package-version string into an InstrumentationLibrary'.
parseInstrumentationLibrary :: (MonadFail m) => String -> m InstrumentationLibrary
parseInstrumentationLibrary packageString = do
let packageNameRegex :: String = "([a-zA-Z0-9-]+[a-zA-Z0-9]+)"
let versionRegex :: String = "([0-9\\.]+)"
-- First try and parse with a mandatory version string on the end. If that fails, try
-- to parse just a package name
let fullRegex :: String = "(" <> packageNameRegex <> "-" <> versionRegex <> ")|" <> packageNameRegex
(_ :: String, _ :: String, _ :: String, groups :: [String]) <- packageString =~~ fullRegex
-- We end up with 5 groups overall
(name, version) <- case groups of
[_, name, version, ""] -> pure (name, version)
[_, _, _, name] -> pure (name, "")
_ -> fail $ "could not parse package string: " <> packageString
pure $ InstrumentationLibrary {libraryName = T.pack name, libraryVersion = T.pack version, librarySchemaUrl = "", libraryAttributes = emptyAttributes}
-- | Works out the instrumentation library for your package.
detectInstrumentationLibrary :: forall m. (TH.Quasi m, TH.Quote m) => m TH.Exp
detectInstrumentationLibrary = do
TH.Loc {loc_package} <- TH.qLocation
lib <- parseInstrumentationLibrary loc_package
TH.lift lib