{-# LANGUAGE DefaultSignatures #-}

{- |
Module      :  OpenTelemetry.Resource
Copyright   :  (c) Ian Duncan, 2021-2026
License     :  BSD-3
Description :  Metadata describing the entity producing telemetry
Stability   :  experimental

= Overview

A 'Resource' is an immutable set of attributes describing the entity that
produces telemetry: the service name, host, container, cloud environment, etc.
Every span, metric, and log record is associated with a resource.

= Quick example

@
import OpenTelemetry.Resource

-- Build a resource from key-value pairs:
myResource :: Resource
myResource = mkResource
  [ "service.name" .= ("my-service" :: Text)
  , "service.version" .= ("1.2.0" :: Text)
  ]

-- Build from a typed data structure:
import OpenTelemetry.Resource.Service
myService :: Resource
myService = toResource Service
  { serviceName = "my-service"
  , serviceNamespace = Just "production"
  , serviceInstanceId = Nothing
  , serviceVersion = Just "1.2.0"
  , serviceCriticality = Nothing
  }
@

= Automatic detection

The SDK automatically detects resources from the environment (hostname, OS,
process info, container ID, cloud metadata). You can add your own on top:

@
(processors, opts) <- getTracerProviderInitializationOptions' myResource
@

= Merging

Resources can be combined with '<>' or 'mergeResources'. When keys conflict,
the first argument (the /updating/ resource) wins. Schema URLs are merged per the OTel spec.

= Spec reference

<https://opentelemetry.io/docs/specs/otel/resource/sdk/>
-}
module OpenTelemetry.Resource (
  -- * Creating resources directly
  mkResource,
  mkResourceWithSchema,
  semConvSchemaUrl,
  Resource,
  (.=),
  (.=?),
  mergeResources,

  -- * Creating resources from data structures
  ToResource (..),

  -- * Using resources with a 'OpenTelemetry.Trace.TracerProvider'
  MaterializedResources,
  materializeResources,
  emptyMaterializedResources,
  getMaterializedResourcesSchema,
  getMaterializedResourcesAttributes,

  -- * Convenience constructors
  materializeResourcesWithSchema,
  setMaterializedResourcesSchema,

  -- * Accessing resource fields
  getResourceAttributes,
  getResourceSchemaUrl,
) where

import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Text as T
import OpenTelemetry.Attributes
import OpenTelemetry.Internal.Logging (otelLogWarning)
import System.IO.Unsafe (unsafePerformIO)


{- | The OpenTelemetry semantic conventions schema URL for version 1.40.0.
Resources that use semantic convention attributes SHOULD carry this URL
so backends can perform automatic attribute migration across versions.

@since 0.4.0.0
-}
semConvSchemaUrl :: Text
semConvSchemaUrl :: Text
semConvSchemaUrl = Text
"https://opentelemetry.io/schemas/1.40.0"


{- | A set of attributes with an optional schema URL.

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.

@since 0.0.1.0
-}
data Resource = Resource
  { Resource -> Maybe Text
resourceSchemaUrl :: !(Maybe Text)
  , Resource -> Attributes
resourceAttributes :: !Attributes
  }


instance Show Resource where
  showsPrec :: Int -> Resource -> ShowS
showsPrec Int
d (Resource Maybe Text
s Attributes
a) =
    Bool -> ShowS -> ShowS
showParen (Int
d Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
10) (ShowS -> ShowS) -> ShowS -> ShowS
forall a b. (a -> b) -> a -> b
$
      String -> ShowS
showString String
"Resource "
        ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Maybe Text -> ShowS
forall a. Show a => Int -> a -> ShowS
showsPrec Int
11 Maybe Text
s
        ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Char -> ShowS
showChar Char
' '
        ShowS -> ShowS -> ShowS
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Attributes -> ShowS
forall a. Show a => Int -> a -> ShowS
showsPrec Int
11 Attributes
a


instance Eq Resource where
  Resource Maybe Text
s1 Attributes
a1 == :: Resource -> Resource -> Bool
== Resource Maybe Text
s2 Attributes
a2 = Maybe Text
s1 Maybe Text -> Maybe Text -> Bool
forall a. Eq a => a -> a -> Bool
== Maybe Text
s2 Bool -> Bool -> Bool
&& Attributes
a1 Attributes -> Attributes -> Bool
forall a. Eq a => a -> a -> Bool
== Attributes
a2


{- | 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
mkResource :: [Maybe (Text, Attribute)] -> Resource
mkResource = Maybe Text -> Attributes -> Resource
Resource Maybe Text
forall a. Maybe a
Nothing (Attributes -> Resource)
-> ([Maybe (Text, Attribute)] -> Attributes)
-> [Maybe (Text, Attribute)]
-> Resource
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


{- | Create a resource with an explicit schema URL.

@since 0.4.0.0
-}
mkResourceWithSchema :: Maybe Text -> [Maybe (Text, Attribute)] -> Resource
mkResourceWithSchema :: Maybe Text -> [Maybe (Text, Attribute)] -> Resource
mkResourceWithSchema Maybe Text
schema = Maybe Text -> Attributes -> Resource
Resource Maybe Text
schema (Attributes -> Resource)
-> ([Maybe (Text, Attribute)] -> Attributes)
-> [Maybe (Text, Attribute)]
-> Resource
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'.

 @since 0.0.1.0
-}
(.=) :: (ToAttribute a) => Text -> a -> Maybe (Text, Attribute)
Text
k .= :: forall a. ToAttribute a => 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'.

 @since 0.0.1.0
-}
(.=?) :: (ToAttribute a) => Text -> Maybe a -> Maybe (Text, Attribute)
Text
k .=? :: forall a.
ToAttribute a =>
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


-- | Merge two resources, taking the left-biased union of attributes.
instance Semigroup Resource where
  <> :: Resource -> Resource -> Resource
(<>) = Resource -> Resource -> Resource
mergeResources


instance Monoid Resource where
  mempty :: Resource
mempty = Maybe Text -> Attributes -> Resource
Resource Maybe Text
forall a. Maybe a
Nothing Attributes
emptyAttributes


{- | Combine two 'Resource' values into a new 'Resource' that contains the
 attributes of the two inputs.

 If a key exists on both resources, the value of the first (updating)
 resource takes precedence.

 Schema URL merge follows the OpenTelemetry specification:

 * If one resource's Schema URL is empty, the other's is used.
 * If both are the same, that URL is used.
 * If both are non-empty and different, the first resource's URL is kept
   (the spec says this case is \"implementation-specific\"); a warning is also
   emitted to the OTel diagnostic logger.

 @since 0.0.1.0
-}
mergeResources
  :: Resource
  -- ^ the updating resource whose attributes take precedence
  -> Resource
  -- ^ the old resource
  -> Resource
mergeResources :: Resource -> Resource -> Resource
mergeResources (Resource Maybe Text
newSchema Attributes
newAttrs) (Resource Maybe Text
oldSchema Attributes
oldAttrs) =
  Maybe Text -> Attributes -> Resource
Resource (Maybe Text -> Maybe Text -> Maybe Text
mergeSchemaUrls Maybe Text
newSchema Maybe Text
oldSchema) (Attributes -> Attributes -> Attributes
unsafeMergeAttributesIgnoringLimits Attributes
newAttrs Attributes
oldAttrs)


mergeSchemaUrls :: Maybe Text -> Maybe Text -> Maybe Text
mergeSchemaUrls :: Maybe Text -> Maybe Text -> Maybe Text
mergeSchemaUrls Maybe Text
Nothing Maybe Text
b = Maybe Text
b
mergeSchemaUrls Maybe Text
a Maybe Text
Nothing = Maybe Text
a
mergeSchemaUrls a :: Maybe Text
a@(Just Text
s1) (Just Text
s2)
  | Text
s1 Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
s2 = Maybe Text
a
  | Bool
otherwise =
      IO (Maybe Text) -> Maybe Text
forall a. IO a -> a
unsafePerformIO (IO (Maybe Text) -> Maybe Text) -> IO (Maybe Text) -> Maybe Text
forall a b. (a -> b) -> a -> b
$ do
        String -> IO ()
otelLogWarning (String
"Resource schema URL conflict: '" String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
s1 String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
"' vs '" String -> ShowS
forall a. Semigroup a => a -> a -> a
<> Text -> String
T.unpack Text
s2 String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
"'")
        Maybe Text -> IO (Maybe Text)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe Text
a


{- | A convenience class for converting arbitrary data into resources.

@since 0.0.1.0
-}
class ToResource a where
  -- | Convert the input value to a 'Resource'
  toResource :: a -> Resource


{- | Access the attributes of a resource.

@since 0.0.1.0
-}
getResourceAttributes :: Resource -> Attributes
getResourceAttributes :: Resource -> Attributes
getResourceAttributes = Resource -> Attributes
resourceAttributes


{- | Access the schema URL of a resource.

@since 0.0.1.0
-}
getResourceSchemaUrl :: Resource -> Maybe Text
getResourceSchemaUrl :: Resource -> Maybe Text
getResourceSchemaUrl = Resource -> Maybe Text
resourceSchemaUrl


{- | A read-only resource attribute collection with an associated schema.

@since 0.0.1.0
-}

-- \| Convert resource fields into a version that discharges the schema from the
--  type level to the runtime level.
--
data MaterializedResources = MaterializedResources
  { MaterializedResources -> Maybe String
materializedResourcesSchema :: Maybe String
  , MaterializedResources -> Attributes
materializedResourcesAttributes :: Attributes
  }
  deriving (Int -> MaterializedResources -> ShowS
[MaterializedResources] -> ShowS
MaterializedResources -> String
(Int -> MaterializedResources -> ShowS)
-> (MaterializedResources -> String)
-> ([MaterializedResources] -> ShowS)
-> Show MaterializedResources
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> MaterializedResources -> ShowS
showsPrec :: Int -> MaterializedResources -> ShowS
$cshow :: MaterializedResources -> String
show :: MaterializedResources -> String
$cshowList :: [MaterializedResources] -> ShowS
showList :: [MaterializedResources] -> ShowS
Show, MaterializedResources -> MaterializedResources -> Bool
(MaterializedResources -> MaterializedResources -> Bool)
-> (MaterializedResources -> MaterializedResources -> Bool)
-> Eq MaterializedResources
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: MaterializedResources -> MaterializedResources -> Bool
== :: MaterializedResources -> MaterializedResources -> Bool
$c/= :: MaterializedResources -> MaterializedResources -> Bool
/= :: MaterializedResources -> MaterializedResources -> Bool
Eq)


{- | 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


{- | Convert a 'Resource' to 'MaterializedResources'.

@since 0.0.1.0
-}
materializeResources :: Resource -> MaterializedResources
materializeResources :: Resource -> MaterializedResources
materializeResources (Resource Maybe Text
mSchema Attributes
attrs) =
  Maybe String -> Attributes -> MaterializedResources
MaterializedResources (Text -> String
T.unpack (Text -> String) -> Maybe Text -> Maybe String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Text
mSchema) Attributes
attrs


{- | Materialize a resource with an explicit runtime schema URL,
overriding any schema URL on the resource itself.

@
let res = materializeResourcesWithSchema
            (Just "https:\/\/opentelemetry.io\/schemas\/1.25.0")
            (mkResource ["service.name" .= ("my-app" :: Text)])
@

@since 0.4.0.0
-}
materializeResourcesWithSchema :: Maybe String -> Resource -> MaterializedResources
materializeResourcesWithSchema :: Maybe String -> Resource -> MaterializedResources
materializeResourcesWithSchema Maybe String
schema (Resource Maybe Text
_ Attributes
attrs) = Maybe String -> Attributes -> MaterializedResources
MaterializedResources Maybe String
schema Attributes
attrs


{- | Override the schema URL on an already-materialized resource.
Replaces any previously set schema URL.

@since 0.4.0.0
-}
setMaterializedResourcesSchema :: Maybe String -> MaterializedResources -> MaterializedResources
setMaterializedResourcesSchema :: Maybe String -> MaterializedResources -> MaterializedResources
setMaterializedResourcesSchema Maybe String
schema MaterializedResources
mr = MaterializedResources
mr {materializedResourcesSchema = schema}