packages feed

hs-opentelemetry-sdk-1.0.0.0: src/OpenTelemetry/Resource/Kubernetes/Detector.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      :  OpenTelemetry.Resource.Kubernetes.Detector
Copyright   :  (c) Ian Duncan, 2024
License     :  BSD-3
Description :  Auto-detect Kubernetes resource attributes
Maintainer  :  Ian Duncan
Stability   :  experimental
Portability :  non-portable (GHC extensions)

Detects Kubernetes resource attributes from the downward API, environment
variables, and mounted service account tokens. Returns empty resources
when not running in a Kubernetes cluster.

Detection sources:

* @KUBERNETES_SERVICE_HOST@: presence indicates k8s
* @HOSTNAME@: typically the pod name
* @\/var\/run\/secrets\/kubernetes.io\/serviceaccount\/namespace@: pod namespace
* @OTEL_RESOURCE_ATTRIBUTES@: may contain @k8s.cluster.name@, @k8s.node.name@, etc.

@since 0.1.0.2
-}
module OpenTelemetry.Resource.Kubernetes.Detector (
  detectKubernetes,
  isRunningInKubernetes,
  KubernetesResources (..),
) where

import qualified Data.Text as T
import qualified Data.Text.IO as T
import OpenTelemetry.Resource.Detector.Internal (lookupEnvText)
import OpenTelemetry.Resource.Kubernetes (Cluster (..), Namespace (..), Node (..), Pod)
import qualified OpenTelemetry.Resource.Kubernetes as K8s
import System.Environment (lookupEnv)
import System.IO.Error (tryIOError)


-- | @since 0.0.1.0
isRunningInKubernetes :: IO Bool
isRunningInKubernetes = do
  mHost <- lookupEnv "KUBERNETES_SERVICE_HOST"
  pure $ case mHost of
    Just _ -> True
    Nothing -> False


-- | @since 0.0.1.0
data KubernetesResources = KubernetesResources
  { k8sCluster :: Cluster
  , k8sNamespace :: Namespace
  , k8sNode :: Node
  , k8sPod :: Pod
  }
  deriving (Show)


-- | @since 0.0.1.0
detectKubernetes :: IO (Maybe KubernetesResources)
detectKubernetes = do
  inK8s <- isRunningInKubernetes
  if not inK8s
    then pure Nothing
    else do
      cluster <- detectCluster
      ns <- detectNamespace
      node <- detectNode
      pod <- detectPod
      pure $
        Just
          KubernetesResources
            { k8sCluster = cluster
            , k8sNamespace = ns
            , k8sNode = node
            , k8sPod = pod
            }


detectCluster :: IO Cluster
detectCluster = do
  mName <- lookupEnvText "K8S_CLUSTER_NAME"
  pure Cluster {clusterName = mName, clusterUid = Nothing}


detectNamespace :: IO Namespace
detectNamespace = do
  mNs <- readNamespaceFile
  ns <- case mNs of
    Just n -> pure (Just n)
    Nothing -> lookupEnvText "K8S_NAMESPACE"
  pure Namespace {namespaceName = ns}
  where
    readNamespaceFile = do
      result <- tryIOError (T.readFile saNamespacePath)
      pure $ case result of
        Right content
          | not (T.null (T.strip content)) -> Just (T.strip content)
        _ -> Nothing
    saNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"


detectNode :: IO Node
detectNode = do
  mName <- lookupEnvText "K8S_NODE_NAME"
  pure Node {nodeName = mName, nodeUid = Nothing}


detectPod :: IO Pod
detectPod = do
  mName <- lookupEnvText "K8S_POD_NAME"
  pName <- case mName of
    Just n -> pure (Just n)
    Nothing -> lookupEnvText "HOSTNAME"
  mUid <- lookupEnvText "K8S_POD_UID"
  pure K8s.Pod {K8s.podName = pName, K8s.podUid = mUid}