instana-haskell-trace-sdk-0.1.0.0: src/Instana/SDK/Internal/AgentConnection/ConnectLoop.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-|
Module : Instana.SDK.Internal.AgentConnection.ConnectLoop
Description : Establishes a connection to the agent.
-}
module Instana.SDK.Internal.AgentConnection.ConnectLoop
( initConnectLoop
) where
import qualified Control.Concurrent as Concurrent
import qualified Control.Concurrent.STM as STM
import Control.Exception (SomeException,
catch)
import Control.Monad (forever)
import Data.Maybe (isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TextIO
import qualified System.Environment as Environment
import System.Log.Logger (debugM,
infoM,
warningM)
import qualified System.Posix.Process as PosixProcess
import qualified Instana.SDK.Internal.AgentConnection.AgentHostLookup as AgentHostLookup
import Instana.SDK.Internal.AgentConnection.ProcessInfo (ProcessInfo (ProcessInfo))
import qualified Instana.SDK.Internal.AgentConnection.ProcessInfo as ProcessInfo
import Instana.SDK.Internal.AgentConnection.SchedFile (parsePidFromSchedFile)
import Instana.SDK.Internal.Context (ConnectionState (..),
InternalContext)
import qualified Instana.SDK.Internal.Context as InternalContext
import Instana.SDK.Internal.Logging (instanaLogger)
{-| Kick of a thread that loops endlessly and checks once in a while if the
agent connection is still up. If not, a connection attempt will be initiated.
The first attempt is made immediately when calling this.
-}
initConnectLoop :: InternalContext -> IO ()
initConnectLoop context = do
pid <- PosixProcess.getProcessID
progName <- Environment.getProgName
execPath <- Environment.getExecutablePath
args <- Environment.getArgs
let
pidStr = show pid
cpuSetFileContent <- getCpuSetFileContent pidStr
parentNsPid <- getPidFromParentNamespace pidStr
let
processInfo =
ProcessInfo
{ ProcessInfo.pidString = pidStr
, ProcessInfo.programName = progName
, ProcessInfo.executablePath = execPath
, ProcessInfo.arguments = args
, ProcessInfo.cpuSetFileContent = cpuSetFileContent
, ProcessInfo.parentNsPid = parentNsPid
}
if isNothing parentNsPid then
warningM instanaLogger $ "Could not parse PID from sched file. " ++
"Discovery might not work if this process is running inside a " ++
"container."
else
if (Just pidStr) == parentNsPid then
debugM instanaLogger $
"PID in sched file matches process PID. Probably not running inside " ++
"a PID namespace"
else do
let
Just parentPid = parentNsPid
infoM instanaLogger $ "Changing PID from " ++ pidStr ++ " to " ++
parentPid ++
" due to successful identification of PID in parent namespace."
debugM instanaLogger $ "discovered process info " ++ show processInfo
-- connection loop works as follows:
-- - try to connect to an an agent at either the agent host/port received via
-- configuration, environment variables, default (127.0.0.1:42699) or default
-- gateway
-- - establishAgentConnection tries to connect to the agent by issuing
-- a POST to /com.instana.plugin.haskell.discovery
-- - establishAgentConnection only ever terminates if it has been successful,
-- then we have switched to announced state.
-- - after that, establishAgentConnection is called every 5 seconds,
-- - if the connection is still up, establishAgentConnection does nothing and
-- returns immediately,
-- - should the connection have been lost, the cycle starts again, that is,
-- establishAgentConnection will retry the POST forever and only terminate
-- after success.
forever $ do
establishAgentConnectionSafe context processInfo
Concurrent.threadDelay $ 5 * 1000 * 1000
establishAgentConnectionSafe ::
InternalContext
-> ProcessInfo
-> IO ()
establishAgentConnectionSafe context processInfo =
catch
(establishAgentConnection context processInfo)
-- exceptions in establishAgentConnection must not kill the loop, so we just
-- catch everything
(\e -> warningM instanaLogger $ show (e :: SomeException))
establishAgentConnection ::
InternalContext
-> ProcessInfo
-> IO ()
establishAgentConnection context processInfo = do
currentState <- STM.atomically $
STM.readTVar (InternalContext.connectionState context)
-- Do nothing if a connection attempt is already in progress or connection has
-- already been established.
if currentState /= Unconnected
then
return ()
else do
STM.atomically $ STM.writeTVar
(InternalContext.connectionState context)
AgentHostLookup
debugM instanaLogger $ "agent connection is not up, attempting reconnect"
-- Initial status: Unconnected
-- step 1: do agent host looup (retry forever until an agent has
-- been found)
-- New status: Unannounced
-- step 2: announce request (retry 3 times with 200 ms delay)
-- New status: Announced
-- step 3: check whether agent is ready to accept data (retry 10 times
-- with 10 second delay)
-- New status: Connected
-- If anything fails in between, go back to "Unconnected"
AgentHostLookup.lookupAgentHost context processInfo
getCpuSetFileContent :: String -> IO (Maybe Text)
getCpuSetFileContent pidStr = do
let
cpuSetPath = "/proc/" ++ pidStr ++ "/cpuset"
catch
( do
content <- TextIO.readFile cpuSetPath
-- paranoid check - if the cpusets file for whatever reason is really big,
-- we don't want to send it to the agent at all.
if T.length content >= 2000 then
return Nothing
else
return $ Just content
)
(\(_ :: SomeException) -> do
debugM instanaLogger $ "Can't read " ++ cpuSetPath ++ ", process " ++
"is probably not running in a container."
return Nothing
)
getPidFromParentNamespace :: String -> IO (Maybe String)
getPidFromParentNamespace pidStr = do
let
schedFilePath = "/proc/" ++ pidStr ++ "/sched"
catch
( do
schedFileContent <- readFile schedFilePath
return $ parsePidFromSchedFile schedFileContent
)
(\(_ :: SomeException) -> do
debugM instanaLogger $ "Can't read " ++ schedFilePath ++ "."
return Nothing
)