diff --git a/sdk/all/src/main/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdk.scala b/sdk/all/src/main/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdk.scala index 4b3334d73..4c3e26e5d 100644 --- a/sdk/all/src/main/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdk.scala +++ b/sdk/all/src/main/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdk.scala @@ -198,6 +198,9 @@ object OpenTelemetrySdk { * By default, the following detectors are enabled: * - host: `host.arch`, `host.name` * - os: `os.type`, `os.description` + * - process: `process.command`, `process.command_args`, + * `process.command_line`, `process.executable.name`, + * `process.executable.path`, `process.pid`, `process.owner` * - process_runtime: `process.runtime.name`, * `process.runtime.version`, `process.runtime.description` * diff --git a/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/OS.scala b/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/OS.scala index 746c326a0..cae56c251 100644 --- a/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/OS.scala +++ b/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/OS.scala @@ -42,4 +42,12 @@ private object OS { @JSImport("os", "release") def release(): String = js.native + @js.native + @JSImport("os", "userInfo") + def userInfo(): UserInfo = js.native + + @js.native + trait UserInfo extends js.Object { + def username: String = js.native + } } diff --git a/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/Process.scala b/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/Process.scala index c958e602a..cdb8406a4 100644 --- a/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/Process.scala +++ b/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/Process.scala @@ -30,6 +30,26 @@ private object Process { @JSImport("process", "versions") def versions: Versions = js.native + @js.native + @JSImport("process", "pid") + def pid: Int = js.native + + @js.native + @JSImport("process", "title") + def title: String = js.native + + @js.native + @JSImport("process", "execPath") + def execPath: String = js.native + + @js.native + @JSImport("process", "argv") + def argv: js.Array[String] = js.native + + @js.native + @JSImport("process", "execArgv") + def execArgv: js.Array[String] = js.native + @js.native trait Versions extends js.Object { def node: String = js.native diff --git a/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala b/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala new file mode 100644 index 000000000..906bd4542 --- /dev/null +++ b/sdk/common/js/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala @@ -0,0 +1,60 @@ +/* + * Copyright 2023 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.sdk.resource + +import cats.effect.Sync +import org.typelevel.otel4s.Attributes +import org.typelevel.otel4s.sdk.TelemetryResource +import org.typelevel.otel4s.semconv.SchemaUrls + +import scala.util.Try + +private[resource] trait ProcessDetectorPlatform { self: ProcessDetector.type => + + def apply[F[_]: Sync]: TelemetryResourceDetector[F] = + new Detector[F] + + private class Detector[F[_]: Sync] extends TelemetryResourceDetector[F] { + def name: String = Const.Name + + def detect: F[Option[TelemetryResource]] = Sync[F].delay { + val argv = Process.argv.toList + + val command = + if (argv.length > 1) Attributes(Keys.Command(Process.argv(1))) + else Attributes.empty + + val owner = + Try( + Attributes(Keys.Owner(OS.userInfo().username)) + ).getOrElse(Attributes.empty) + + val args = + argv.headOption.toSeq ++ Process.execArgv ++ argv.drop(1) + + val attributes = Attributes( + Keys.Pid(Process.pid.toLong), + Keys.ExecutableName(Process.title), + Keys.ExecutablePath(Process.execPath), + Keys.CommandArgs(args) + ) ++ command ++ owner + + Some(TelemetryResource(attributes, Some(SchemaUrls.Current))) + } + } + +} diff --git a/sdk/common/jvm/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala b/sdk/common/jvm/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala new file mode 100644 index 000000000..d8fdba78e --- /dev/null +++ b/sdk/common/jvm/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala @@ -0,0 +1,111 @@ +/* + * Copyright 2023 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.sdk.resource + +import cats.effect.Sync +import cats.syntax.flatMap._ +import cats.syntax.functor._ +import org.typelevel.otel4s.Attributes +import org.typelevel.otel4s.sdk.TelemetryResource +import org.typelevel.otel4s.semconv.SchemaUrls + +import java.io.File +import java.lang.management.ManagementFactory +import java.lang.management.RuntimeMXBean +import java.util.Locale +import java.util.regex.Pattern + +private[resource] trait ProcessDetectorPlatform { self: ProcessDetector.type => + + private val JarFilePattern = + Pattern.compile("^\\S+\\.(jar|war)", Pattern.CASE_INSENSITIVE) + + def apply[F[_]: Sync]: TelemetryResourceDetector[F] = + new Detector[F] + + private class Detector[F[_]: Sync] extends TelemetryResourceDetector[F] { + def name: String = Const.Name + + def detect: F[Option[TelemetryResource]] = + for { + runtime <- Sync[F].delay(ManagementFactory.getRuntimeMXBean) + javaHomeOpt <- Sync[F].delay(sys.props.get("java.home")) + osNameOpt <- Sync[F].delay(sys.props.get("os.name")) + javaCommandOpt <- Sync[F].delay(sys.props.get("sun.java.command")) + } yield { + val builder = Attributes.newBuilder + + getPid(runtime).foreach(pid => builder.addOne(Keys.Pid(pid))) + + javaHomeOpt.foreach { javaHome => + val exePath = executablePath(javaHome, osNameOpt) + val cmdLine = exePath + commandLineArgs(runtime, javaCommandOpt) + + builder.addOne(Keys.ExecutablePath(exePath)) + builder.addOne(Keys.CommandLine(cmdLine)) + } + + Some(TelemetryResource(builder.result(), Some(SchemaUrls.Current))) + } + + private def getPid(runtime: RuntimeMXBean): Option[Long] = + runtime.getName.split("@").headOption.flatMap(_.toLongOption) + + private def executablePath( + javaHome: String, + osName: Option[String] + ): String = { + val executablePath = new StringBuilder(javaHome) + executablePath + .append(File.separatorChar) + .append("bin") + .append(File.separatorChar) + .append("java") + + if (osName.exists(_.toLowerCase(Locale.ROOT).startsWith("windows"))) + executablePath.append(".exe") + + executablePath.result() + } + + private def commandLineArgs( + runtime: RuntimeMXBean, + javaCommandOpt: Option[String] + ): String = { + val commandLine = new StringBuilder() + + // VM args: -Dfile.encoding=UTF-8, -Xms2000m, -Xmx2000m, etc + runtime.getInputArguments.forEach { arg => + commandLine.append(' ').append(arg) + () + } + + // general args, e.g.: org.ClassName param1 param2 + javaCommandOpt.foreach { javaCommand => + // '-jar' is missing when launching a jar directly, add it if needed + if (JarFilePattern.matcher(javaCommand).matches()) + commandLine.append(" -jar") + + commandLine.append(' ').append(javaCommand) + () + } + + commandLine.result() + } + } + +} diff --git a/sdk/common/native/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala b/sdk/common/native/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala new file mode 100644 index 000000000..d2fad0d98 --- /dev/null +++ b/sdk/common/native/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetectorPlatform.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.sdk.resource + +import cats.effect.Sync +import org.typelevel.otel4s.Attributes +import org.typelevel.otel4s.sdk.TelemetryResource +import org.typelevel.otel4s.semconv.SchemaUrls + +import scala.scalanative.posix.unistd._ + +private[resource] trait ProcessDetectorPlatform { self: ProcessDetector.type => + + def apply[F[_]: Sync]: TelemetryResourceDetector[F] = + new Detector[F] + + private class Detector[F[_]: Sync] extends TelemetryResourceDetector[F] { + def name: String = Const.Name + + def detect: F[Option[TelemetryResource]] = Sync[F].delay { + val attributes = Attributes( + Keys.Pid(getpid().longValue) + ) + + Some(TelemetryResource(attributes, Some(SchemaUrls.Current))) + } + } + +} diff --git a/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala b/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala index c8a65a8ea..9b0b3c803 100644 --- a/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala +++ b/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigure.scala @@ -40,7 +40,7 @@ import java.nio.charset.StandardCharsets * | otel.resource.attributes | OTEL_RESOURCE_ATTRIBUTES | Specify resource attributes in the following format: key1=val1,key2=val2,key3=val3 | * | otel.service.name | OTEL_SERVICE_NAME | Specify logical service name. Takes precedence over `service.name` defined with `otel.resource.attributes` | * | otel.experimental.resource.disabled-keys | OTEL_EXPERIMENTAL_RESOURCE_DISABLED_KEYS | Specify resource attribute keys that are filtered. | - * | otel.otel4s.resource.detectors | OTEL_OTEL4S_RESOURCE_DETECTORS | Specify resource detectors to use. Defaults to `host,os,process_runtime`. | + * | otel.otel4s.resource.detectors | OTEL_OTEL4S_RESOURCE_DETECTORS | Specify resource detectors to use. Defaults to `host,os,process,process_runtime`. | * }}} * * @see @@ -206,6 +206,7 @@ private[sdk] object TelemetryResourceAutoConfigure { val Detectors: Set[String] = Set( HostDetector.Const.Name, OSDetector.Const.Name, + ProcessDetector.Const.Name, ProcessRuntimeDetector.Const.Name ) } @@ -219,7 +220,7 @@ private[sdk] object TelemetryResourceAutoConfigure { * | otel.resource.attributes | OTEL_RESOURCE_ATTRIBUTES | Specify resource attributes in the following format: key1=val1,key2=val2,key3=val3 | * | otel.service.name | OTEL_SERVICE_NAME | Specify logical service name. Takes precedence over `service.name` defined with `otel.resource.attributes` | * | otel.experimental.resource.disabled-keys | OTEL_EXPERIMENTAL_RESOURCE_DISABLED_KEYS | Specify resource attribute keys that are filtered. | - * | otel.otel4s.resource.detectors | OTEL_OTEL4S_RESOURCE_DETECTORS | Specify resource detectors to use. Defaults to `host,os,process_runtime`. | + * | otel.otel4s.resource.detectors | OTEL_OTEL4S_RESOURCE_DETECTORS | Specify resource detectors to use. Defaults to `host,os,process,process_runtime`. | * }}} * * @see diff --git a/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetector.scala b/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetector.scala new file mode 100644 index 000000000..6e150b110 --- /dev/null +++ b/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/resource/ProcessDetector.scala @@ -0,0 +1,56 @@ +/* + * Copyright 2023 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.typelevel.otel4s.sdk.resource + +import org.typelevel.otel4s.AttributeKey + +/** Detects process-specific parameters such as executable name, executable + * path, PID, and so on. + * + * @see + * [[https://opentelemetry.io/docs/specs/semconv/resource/process/]] + */ +object ProcessDetector extends ProcessDetectorPlatform { + + private[sdk] object Const { + val Name = "process" + } + + private[resource] object Keys { + val Command: AttributeKey[String] = + AttributeKey("process.command") + + val CommandArgs: AttributeKey[Seq[String]] = + AttributeKey("process.command_args") + + val CommandLine: AttributeKey[String] = + AttributeKey("process.command_line") + + val ExecutableName: AttributeKey[String] = + AttributeKey("process.executable.name") + + val ExecutablePath: AttributeKey[String] = + AttributeKey("process.executable.path") + + val Pid: AttributeKey[Long] = + AttributeKey("process.pid") + + val Owner: AttributeKey[String] = + AttributeKey("process.owner") + } + +} diff --git a/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetector.scala b/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetector.scala index 15f97555a..880e307bd 100644 --- a/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetector.scala +++ b/sdk/common/shared/src/main/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetector.scala @@ -54,12 +54,18 @@ object TelemetryResourceDetector { * Includes: * - host detector * - os detector + * - process detector * - process runtime detector * * @tparam F * the higher-kinded type of a polymorphic effect */ def default[F[_]: Sync]: Set[TelemetryResourceDetector[F]] = - Set(HostDetector[F], OSDetector[F], ProcessRuntimeDetector[F]) + Set( + HostDetector[F], + OSDetector[F], + ProcessDetector[F], + ProcessRuntimeDetector[F] + ) } diff --git a/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigureSuite.scala b/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigureSuite.scala index 3c5fffe54..27229b88e 100644 --- a/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigureSuite.scala +++ b/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/autoconfigure/TelemetryResourceAutoConfigureSuite.scala @@ -71,6 +71,24 @@ class TelemetryResourceAutoConfigureSuite extends CatsEffectSuite { val host = Set("host.arch", "host.name") val os = Set("os.type", "os.description") + val process = + if (PlatformCompat.isJS) + Set( + "process.executable.name", + "process.owner", + "process.command_args", + "process.executable.path", + "process.pid" + ) + else if (PlatformCompat.isNative) + Set("process.pid") + else + Set( + "process.command_line", + "process.executable.path", + "process.pid" + ) + val runtime = { val name = "process.runtime.name" val version = "process.runtime.version" @@ -87,7 +105,7 @@ class TelemetryResourceAutoConfigureSuite extends CatsEffectSuite { ) val all = - host ++ os ++ runtime ++ service ++ telemetry + host ++ os ++ process ++ runtime ++ service ++ telemetry IO(assertEquals(resource.attributes.map(_.key.name).toSet, all)) } diff --git a/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetectorSuite.scala b/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetectorSuite.scala index a8b252b69..2e5de5ace 100644 --- a/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetectorSuite.scala +++ b/sdk/common/shared/src/test/scala/org/typelevel/otel4s/sdk/resource/TelemetryResourceDetectorSuite.scala @@ -45,6 +45,33 @@ class TelemetryResourceDetectorSuite extends CatsEffectSuite { } } + test("ProcessDetector - detect pid, exe path, exe name") { + val keys = + if (PlatformCompat.isJS) + Set( + "process.executable.path", + "process.pid", + "process.executable.name", + "process.owner", + "process.command_args" + ) + else if (PlatformCompat.isNative) + Set("process.pid") + else + Set( + "process.executable.path", + "process.pid", + "process.command_line" + ) + + for { + resource <- ProcessDetector[IO].detect + } yield { + assertEquals(resource.map(_.attributes.map(_.key.name).toSet), Some(keys)) + assertEquals(resource.flatMap(_.schemaUrl), Some(SchemaUrls.Current)) + } + } + test("ProcessRuntimeDetector - detect name, version, and description") { val keys = { val name = "process.runtime.name" @@ -63,9 +90,9 @@ class TelemetryResourceDetectorSuite extends CatsEffectSuite { } } - test("default - contain host, os, process_runtime detectors") { + test("default - contain host, os, process, process_runtime detectors") { val detectors = TelemetryResourceDetector.default[IO].map(_.name) - val expected = Set("host", "os", "process_runtime") + val expected = Set("host", "os", "process", "process_runtime") assertEquals(detectors.map(_.name), expected) } diff --git a/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/SdkMetrics.scala b/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/SdkMetrics.scala index 26678c788..273801b32 100644 --- a/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/SdkMetrics.scala +++ b/sdk/metrics/src/main/scala/org/typelevel/otel4s/sdk/metrics/SdkMetrics.scala @@ -143,6 +143,9 @@ object SdkMetrics { * By default, the following detectors are enabled: * - host: `host.arch`, `host.name` * - os: `os.type`, `os.description` + * - process: `process.command`, `process.command_args`, + * `process.command_line`, `process.executable.name`, + * `process.executable.path`, `process.pid`, `process.owner` * - process_runtime: `process.runtime.name`, * `process.runtime.version`, `process.runtime.description` * diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTraces.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTraces.scala index 7d26117da..0e61bc7e6 100644 --- a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTraces.scala +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/SdkTraces.scala @@ -164,6 +164,9 @@ object SdkTraces { * By default, the following detectors are enabled: * - host: `host.arch`, `host.name` * - os: `os.type`, `os.description` + * - process: `process.command`, `process.command_args`, + * `process.command_line`, `process.executable.name`, + * `process.executable.path`, `process.pid`, `process.owner` * - process_runtime: `process.runtime.name`, * `process.runtime.version`, `process.runtime.description` *