iand675 / hs-opentelemetry

OpenTelemetry support for the Haskell programming language
https://iankduncan.com/projects/opentelemetry
BSD 3-Clause "New" or "Revised" License
73 stars 34 forks source link

There is no way to set the `service.name` attribute in code #80

Closed solomon-b closed 1 year ago

solomon-b commented 1 year ago

AFAICT the only supported way to set the service name is via the OTEL_SERVICE_NAME env var. The problem with this is that if multiple services are running in the same environment (such as in CI) then all the services will pick up that env var.

I've seen other (non-haskell) opentelemetry libraries which allow you to set the service.name attribute explicitly in code, but have the env var take precedence. This seems like a reasonable behavior.

Without all the data constructors exported, the closest a library consumer can get to this behavior is to write their own version of initializeTracerProvider:

myInitializeTracerProvider = do
  (processors, opts) <- Otel.getTracerProviderInitializationOptions' (Otel.mkResource ["service.name" Otel..= ("my_cool_service_name" :: Text)] :: Otel.Resource 'Nothing)
  Otel.createTracerProvider processors opts

However, this doesn't work because merging HashMaps sets precedence to the left map. It seems like you have to patch the library to make this work.

I think the simplest solution would be to change detectService to receive an optional custom service name and thread that name down from a custom initializeTracerProvider:

detectService :: Maybe T.Text -> IO Service
detectService customName = do
  mSvcName <- lookupEnv "OTEL_SERVICE_NAME"
  svcName <- case mSvcName of
    Nothing -> maybe (T.pack . ("unknown_service:" <>) <$> getProgName) pure customName
    Just svcName -> pure $ T.pack svcName
  pure $
    Service
      { serviceName = svcName
      , serviceNamespace = Nothing
      , serviceInstanceId = Nothing
      , serviceVersion = Nothing
      }

initializeTracerProviderCustomService :: T.Text -> IO TracerProvider
initializeTracerProviderCustomService = ...

I have a branch with these changes on it if we want this behavior. Or if you prefer another implementation I am happy to write the PR.

parsonsmatt commented 1 year ago

Another way that end users can work-around this is by using setEnv.

withOtelServiceName :: String -> IO a -> IO a
withOtelServiceName newName action =
  -- use bracket to restore the old service name
  bracket (lookupEnv "OTEL_SERVICE_NAME") (setEnv "OTEL_SERVICE_NAME" . fromMaybe "") $ \maybeOldName -> do
    setEnv "OTEL_SERVICE_NAME" newName
    action

withOtelServiceName "my.cool.service" $ do
  -- initialize the service normally
solomon-b commented 1 year ago

Nice workaround @parsonsmatt.

Depending on timing issues across applications it looks like you could accidentally pickup the new service name in another process running on the same machine?

Profpatsch commented 1 year ago

Depending on timing issues across applications it looks like you could accidentally pickup the new service name in another process running on the same machine?

setEnv modifies the process-internal environment mapping. Only the process and any children should see the new variable name.

Profpatsch commented 1 year ago

@parsonsmatt the bracket is a bit misleading here, because the environment gets set process-wide, meaning it’s not lexically scoped. Just setting it should be fine, since it will be dropped automatically when ending the process.

solomon-b commented 1 year ago

Great, I'll make this as closed for now.