packages feed

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

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}

{- |
Module      : OpenTelemetry.Resource.Host.Detector
Description : Resource detector for host attributes. Auto-detects hostname and architecture from the runtime environment.
Stability   : experimental
-}
module OpenTelemetry.Resource.Host.Detector (
  detectHost,
  builtInHostDetectors,
  HostDetector,
) where

import Control.Exception (SomeException, try)
import Control.Monad
import qualified Data.Text as T
import Network.BSD
import OpenTelemetry.Resource.Host
import System.Info (arch)


#if defined(linux_HOST_OS)
import qualified Data.Text.IO as TIO
#elif defined(darwin_HOST_OS)
import System.Process (readProcess)
#endif


adaptedArch :: T.Text
adaptedArch = case arch of
  "aarch64" -> "arm64"
  "arm" -> "arm32"
  "x86_64" -> "amd64"
  "i386" -> "x86"
  "ia64" -> "ia64"
  "powerpc" -> "ppc32"
  "powerpc64" -> "ppc64"
  "powerpc64le" -> "ppc64"
  other -> T.pack other

#if defined(darwin_HOST_OS)
-- | Parse @\"IOPlatformUUID\" = \"…\"@ from @ioreg@ output (see OpenTelemetry host.id for macOS).
parseIOPlatformUUID :: T.Text -> Maybe T.Text
parseIOPlatformUUID txt = do
  line <- findIOPlatformUUIDLine txt
  parseUUIDFromLine line

findIOPlatformUUIDLine :: T.Text -> Maybe T.Text
findIOPlatformUUIDLine t = go (T.lines t)
  where
    go [] = Nothing
    go (l : ls) =
      if "IOPlatformUUID" `T.isInfixOf` l
        then Just l
        else go ls

parseUUIDFromLine :: T.Text -> Maybe T.Text
parseUUIDFromLine line = do
  let (_, rest) = T.breakOn "\" = \"" line
  guard $ not (T.null rest)
  let afterEq = T.drop (T.length "\" = \"") rest
  let (uuid, _) = T.breakOn "\"" afterEq
  guard $ not (T.null uuid)
  Just uuid
#endif


-- | Best-effort @host.id@: @\/etc\/machine-id@ on Linux, @ioreg@ @IOPlatformUUID@ on Darwin; @Nothing@ elsewhere or on failure.
detectHostId :: IO (Maybe T.Text)
#if defined(linux_HOST_OS)
detectHostId = do
  er <-
    try @SomeException $
      do
        content <- TIO.readFile "/etc/machine-id"
        let tid = T.strip content
        pure $ if T.null tid then Nothing else Just tid
  pure $ either (const Nothing) id er
#elif defined(darwin_HOST_OS)
detectHostId = do
  er <-
    try @SomeException $
      do
        out <- readProcess "ioreg" ["-rd1", "-c", "IOPlatformExpertDevice"] ""
        pure $ parseIOPlatformUUID (T.pack out)
  pure $ either (const Nothing) id er
#else
detectHostId = pure Nothing
#endif


-- | Detect as much host information as possible
detectHost :: IO Host
detectHost = do
  mhost <- foldM go Nothing builtInHostDetectors
  pure $ case mhost of
    Nothing -> Host Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
    Just host -> host
  where
    go Nothing hostDetector = hostDetector
    go mhost@(Just _host) _ = pure mhost


{- | Built-in host detectors.

Cloud-specific host attributes (@host.type@, @host.image.id@, etc.) may be
refined by IMDS resource detectors (AWS EC2, GCP Compute, Azure VM). This
detector supplies @host.name@, @host.arch@, and on Linux\/macOS a non-cloud
@host.id@ when available.
-}
builtInHostDetectors :: [HostDetector]
builtInHostDetectors =
  [fallbackHostDetector]


type HostDetector = IO (Maybe Host)


fallbackHostDetector :: HostDetector
fallbackHostDetector =
  Just <$> do
    hostId <- detectHostId
    let hostType = Nothing
        hostArch = Just adaptedArch
        hostImageName = Nothing
        hostImageId = Nothing
        hostImageVersion = Nothing
        hostIp = Nothing
        hostMac = Nothing
    hostName <- Just . T.pack <$> getHostName
    pure Host {..}