{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DefaultSignatures #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  OpenTelemetry.Resource
-- Copyright   :  (c) Ian Duncan, 2021
-- License     :  BSD-3
-- Description :  Facilities for attaching metadata attributes to all spans in a trace
-- Maintainer  :  Ian Duncan
-- Stability   :  experimental
-- Portability :  non-portable (GHC extensions)
--
-- A Resource is an immutable representation of the entity producing
-- telemetry. For example, a process producing telemetry that is running in
-- a container on Kubernetes has a Pod name, it is in a namespace and
-- possibly is part of a Deployment which also has a name. All three of
-- these attributes can be included in the Resource.
--
-----------------------------------------------------------------------------
module OpenTelemetry.Resource 
  ( 
  -- * Creating resources directly
    mkResource
  , Resource
  , (.=)
  , (.=?)
  , ResourceMerge
  , mergeResources
  -- * Creating resources from data structures
  , ToResource(..)
  , materializeResources
  -- * Using resources with a 'OpenTelemetry.Trace.TracerProvider'
  , MaterializedResources
  , emptyMaterializedResources
  , getMaterializedResourcesSchema
  , getMaterializedResourcesAttributes
  ) where

import Data.Proxy (Proxy(..))
import Data.Text (Text)
import GHC.TypeLits
import Data.Maybe (catMaybes)
import OpenTelemetry.Attributes

-- | A set of attributes created from one or more resources.
--
-- A Resource is an immutable representation of the entity producing telemetry as Attributes. 
-- For example, a process producing telemetry that is running in a container on Kubernetes has a Pod name, 
-- it is in a namespace and possibly is part of a Deployment which also has a name. 
-- 
-- All three of these attributes can be included in the Resource. 
--
-- Note that there are certain <https://github.com/open-telemetry/opentelemetry-specification/blob/34144d02baaa39f7aa97ee914539089e1481166c/specification/resource/semantic_conventions/README.md "standard attributes"> that have prescribed meanings.
--
-- A number of these standard resources may be found in the @OpenTelemetry.Resource.*@ modules.
--
-- The primary purpose of resources as a first-class concept in the SDK is decoupling of discovery of resource information from exporters. 
-- This allows for independent development and easy customization for users that need to integrate with closed source environments. 
newtype Resource (schema :: Maybe Symbol) = Resource Attributes

-- | Utility function to create a resource from a list
-- of fields and attributes. See the '.=' and '.=?' functions.
--
-- @since 0.0.1.0
mkResource :: [Maybe (Text, Attribute)] -> Resource r
mkResource :: [Maybe (Text, Attribute)] -> Resource r
mkResource = Attributes -> Resource r
forall (schema :: Maybe Symbol). Attributes -> Resource schema
Resource (Attributes -> Resource r)
-> ([Maybe (Text, Attribute)] -> Attributes)
-> [Maybe (Text, Attribute)]
-> Resource r
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [(Text, Attribute)] -> Attributes
unsafeAttributesFromListIgnoringLimits ([(Text, Attribute)] -> Attributes)
-> ([Maybe (Text, Attribute)] -> [(Text, Attribute)])
-> [Maybe (Text, Attribute)]
-> Attributes
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Maybe (Text, Attribute)] -> [(Text, Attribute)]
forall a. [Maybe a] -> [a]
catMaybes

-- | Utility function to convert a required resource attribute
-- into the format needed for 'mkResource'.
(.=) :: ToAttribute a => Text -> a -> Maybe (Text, Attribute)
Text
k .= :: Text -> a -> Maybe (Text, Attribute)
.= a
v = (Text, Attribute) -> Maybe (Text, Attribute)
forall a. a -> Maybe a
Just (Text
k, a -> Attribute
forall a. ToAttribute a => a -> Attribute
toAttribute a
v)

-- | Utility function to convert an optional resource attribute
-- into the format needed for 'mkResource'.
(.=?) :: ToAttribute a => Text -> Maybe a -> Maybe (Text, Attribute)
Text
k .=? :: Text -> Maybe a -> Maybe (Text, Attribute)
.=? Maybe a
mv = (\Text
k' a
v -> (Text
k', a -> Attribute
forall a. ToAttribute a => a -> Attribute
toAttribute a
v)) Text
k (a -> (Text, Attribute)) -> Maybe a -> Maybe (Text, Attribute)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe a
mv
instance Semigroup (Resource s) where
  <> :: Resource s -> Resource s -> Resource s
(<>) (Resource Attributes
l) (Resource Attributes
r) = Attributes -> Resource s
forall (schema :: Maybe Symbol). Attributes -> Resource schema
Resource (Attributes -> Attributes -> Attributes
unsafeMergeAttributesIgnoringLimits Attributes
l Attributes
r)

instance Monoid (Resource s) where
  mempty :: Resource s
mempty = Attributes -> Resource s
forall (schema :: Maybe Symbol). Attributes -> Resource schema
Resource Attributes
emptyAttributes

-- | Static checks to prevent invalid resources from being merged.
--
-- Note: This is intended to be utilized for merging of resources whose attributes
-- come from different sources,
-- such as environment variables, or metadata extracted from the host or container.
--
-- The resulting resource will have all attributes that are on any of the two input resources.
-- If a key exists on both the old and updating resource, the value of the updating
-- resource will be picked (even if the updated value is "empty").
--
-- The resulting resource will have the Schema URL calculated as follows:
--
-- - If the old resource's Schema URL is empty then the resulting resource's Schema
--   URL will be set to the Schema URL of the updating resource,
-- - Else if the updating resource's Schema URL is empty then the resulting
--   resource's Schema URL will be set to the Schema URL of the old resource,
-- - Else if the Schema URLs of the old and updating resources are the same then
--   that will be the Schema URL of the resulting resource,
-- - Else this is a merging error (this is the case when the Schema URL of the old
--   and updating resources are not empty and are different). The resulting resource is
--   therefore statically prohibited by this type-level function.
--
type family ResourceMerge schemaLeft schemaRight :: Maybe Symbol where
  ResourceMerge 'Nothing 'Nothing = 'Nothing
  ResourceMerge 'Nothing ('Just s) = 'Just s
  ResourceMerge ('Just s) 'Nothing = 'Just s
  ResourceMerge ('Just s) ('Just s) = 'Just s

-- | Combine two 'Resource' values into a new 'Resource' that contains the
-- attributes of the two inputs.
--
-- See the 'ResourceMerge' documentation about the additional semantics of merging two resources.
--
-- @since 0.0.1.0
mergeResources :: 
     Resource old 
  -- ^ the old resource
  -> Resource new
  -- ^ the updating resource whose attributes take precedence
  -> Resource (ResourceMerge old new)
mergeResources :: Resource old -> Resource new -> Resource (ResourceMerge old new)
mergeResources (Resource Attributes
l) (Resource Attributes
r) = Attributes -> Resource (ResourceMerge old new)
forall (schema :: Maybe Symbol). Attributes -> Resource schema
Resource (Attributes -> Attributes -> Attributes
unsafeMergeAttributesIgnoringLimits Attributes
l Attributes
r)

-- | A convenience class for converting arbitrary data into resources.
class ToResource a where
  -- | Resource schema (if any) associated with the defined resource
  type ResourceSchema a :: Maybe Symbol
  type ResourceSchema a = 'Nothing
  -- | Convert the input value to a 'Resource'
  toResource :: a -> Resource (ResourceSchema a)

class MaterializeResource schema where
  -- | Convert resource fields into a version that discharges the schema from the
  -- type level to the runtime level.
  materializeResources :: Resource schema -> MaterializedResources

instance MaterializeResource 'Nothing where
  materializeResources :: Resource 'Nothing -> MaterializedResources
materializeResources (Resource Attributes
attrs) = Maybe String -> Attributes -> MaterializedResources
MaterializedResources Maybe String
forall a. Maybe a
Nothing Attributes
attrs

instance KnownSymbol s => MaterializeResource ('Just s) where
  materializeResources :: Resource ('Just s) -> MaterializedResources
materializeResources (Resource Attributes
attrs) = Maybe String -> Attributes -> MaterializedResources
MaterializedResources (String -> Maybe String
forall a. a -> Maybe a
Just (String -> Maybe String) -> String -> Maybe String
forall a b. (a -> b) -> a -> b
$ Proxy s -> String
forall (n :: Symbol) (proxy :: Symbol -> *).
KnownSymbol n =>
proxy n -> String
symbolVal (Proxy s
forall k (t :: k). Proxy t
Proxy @s)) Attributes
attrs

-- | A read-only resource attribute collection with an associated schema.
data MaterializedResources = MaterializedResources
  { MaterializedResources -> Maybe String
materializedResourcesSchema :: Maybe String
  , MaterializedResources -> Attributes
materializedResourcesAttributes :: Attributes
  }

-- | A placeholder for 'MaterializedResources' when no resource information is
-- available, needed, or required.
--
-- @since 0.0.1.0
emptyMaterializedResources :: MaterializedResources
emptyMaterializedResources :: MaterializedResources
emptyMaterializedResources = Maybe String -> Attributes -> MaterializedResources
MaterializedResources Maybe String
forall a. Maybe a
Nothing Attributes
emptyAttributes

-- | Access the schema for a 'MaterializedResources' value.
--
-- @since 0.0.1.0
getMaterializedResourcesSchema :: MaterializedResources -> Maybe String
getMaterializedResourcesSchema :: MaterializedResources -> Maybe String
getMaterializedResourcesSchema = MaterializedResources -> Maybe String
materializedResourcesSchema

-- | Access the attributes for a 'MaterializedResources' value.
--
-- @since 0.0.1.0
getMaterializedResourcesAttributes :: MaterializedResources -> Attributes
getMaterializedResourcesAttributes :: MaterializedResources -> Attributes
getMaterializedResourcesAttributes = MaterializedResources -> Attributes
materializedResourcesAttributes