Skip to content

Commit 151242b

Browse files
authored
Merge pull request #721 from iRevive/sdk-common/resource-detector
sdk-common: add `TelemetryResourceDetector`
2 parents 2b85b09 + 08bfc85 commit 151242b

File tree

19 files changed

+708
-37
lines changed

19 files changed

+708
-37
lines changed

build.sbt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ThisBuild / tlBaseVersion := "0.8"
1+
ThisBuild / tlBaseVersion := "0.9"
22

33
ThisBuild / organization := "org.typelevel"
44
ThisBuild / organizationName := "Typelevel"
@@ -221,6 +221,7 @@ lazy val `sdk-common` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
221221
)
222222
.settings(munitDependencies)
223223
.settings(scalafixSettings)
224+
.jsSettings(scalaJSLinkerSettings)
224225

225226
lazy val `sdk-metrics` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
226227
.crossType(CrossType.Pure)
@@ -243,6 +244,7 @@ lazy val `sdk-metrics` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
243244
)
244245
.settings(munitDependencies)
245246
.settings(scalafixSettings)
247+
.jsSettings(scalaJSLinkerSettings)
246248

247249
lazy val `sdk-metrics-testkit` =
248250
crossProject(JVMPlatform, JSPlatform, NativePlatform)
@@ -275,6 +277,7 @@ lazy val `sdk-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
275277
)
276278
.settings(munitDependencies)
277279
.settings(scalafixSettings)
280+
.jsSettings(scalaJSLinkerSettings)
278281

279282
lazy val `sdk-trace-testkit` =
280283
crossProject(JVMPlatform, JSPlatform, NativePlatform)
@@ -313,6 +316,7 @@ lazy val sdk = crossProject(JVMPlatform, JSPlatform, NativePlatform)
313316
)
314317
.settings(munitDependencies)
315318
.settings(scalafixSettings)
319+
.jsSettings(scalaJSLinkerSettings)
316320

317321
//
318322
// SDK exporter

docs/sdk/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ If not specified, SDK defaults the service name to `unknown_service:scala`.
5050
| otel.resource.attributes | OTEL\\_RESOURCE\\_ATTRIBUTES | Specify resource attributes in the following format: `key1=val1,key2=val2,key3=val3`. |
5151
| otel.service.name | OTEL\\_SERVICE\\_NAME | Specify logical service name. Takes precedence over `service.name` defined with `otel.resource.attributes`. |
5252
| otel.experimental.resource.disabled-keys | OTEL\\_EXPERIMENTAL\\_RESOURCE\\_DISABLED\\_KEYS | Specify resource attribute keys that are filtered. |
53+
| otel.otel4s.resource.detectors | OTEL\\_OTEL4S\\_RESOURCE\\_DETECTORS | Specify resource detectors to use. Defaults to `host`. |
5354

5455
## Metrics
5556

sdk/all/src/main/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdk.scala

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import org.typelevel.otel4s.sdk.metrics.autoconfigure.MeterProviderAutoConfigure
4343
import org.typelevel.otel4s.sdk.metrics.data.ExemplarData
4444
import org.typelevel.otel4s.sdk.metrics.exemplar.TraceContextLookup
4545
import org.typelevel.otel4s.sdk.metrics.exporter.MetricExporter
46+
import org.typelevel.otel4s.sdk.resource.TelemetryResourceDetector
4647
import org.typelevel.otel4s.sdk.trace.SdkContextKeys
4748
import org.typelevel.otel4s.sdk.trace.SdkTracerProvider
4849
import org.typelevel.otel4s.sdk.trace.autoconfigure.ContextPropagatorsAutoConfigure
@@ -191,6 +192,19 @@ object OpenTelemetrySdk {
191192
customizer: Customizer[TelemetryResource]
192193
): Builder[F]
193194

195+
/** Adds the telemetry resource detector. Multiple detectors can be added,
196+
* and the detected telemetry resources will be merged.
197+
*
198+
* By default, the following detectors are enabled:
199+
* - host: `host.arch`, `host.name`
200+
*
201+
* @param detector
202+
* the detector to add
203+
*/
204+
def addResourceDetector(
205+
detector: TelemetryResourceDetector[F]
206+
): Builder[F]
207+
194208
/** Adds both metric and span exporter configurers. Can be used to
195209
* register exporters that aren't included in the SDK.
196210
*
@@ -297,6 +311,7 @@ object OpenTelemetrySdk {
297311
resourceCustomizer = (a, _) => a,
298312
meterProviderCustomizer = (a: SdkMeterProvider.Builder[F], _) => a,
299313
tracerProviderCustomizer = (a: SdkTracerProvider.Builder[F], _) => a,
314+
resourceDetectors = Set.empty,
300315
metricExporterConfigurers = Set.empty,
301316
spanExporterConfigurers = Set.empty,
302317
samplerConfigurers = Set.empty,
@@ -312,6 +327,7 @@ object OpenTelemetrySdk {
312327
resourceCustomizer: Customizer[TelemetryResource],
313328
meterProviderCustomizer: Customizer[SdkMeterProvider.Builder[F]],
314329
tracerProviderCustomizer: Customizer[SdkTracerProvider.Builder[F]],
330+
resourceDetectors: Set[TelemetryResourceDetector[F]],
315331
metricExporterConfigurers: Set[
316332
AutoConfigure.Named[F, MetricExporter[F]]
317333
],
@@ -354,6 +370,11 @@ object OpenTelemetrySdk {
354370
merge(this.tracerProviderCustomizer, customizer)
355371
)
356372

373+
def addResourceDetector(
374+
detector: TelemetryResourceDetector[F]
375+
): Builder[F] =
376+
copy(resourceDetectors = this.resourceDetectors + detector)
377+
357378
def addExportersConfigurer(
358379
configurer: ExportersAutoConfigure[F]
359380
): Builder[F] =
@@ -466,7 +487,7 @@ object OpenTelemetrySdk {
466487
for {
467488
config <- Resource.eval(customConfig.fold(loadConfig)(Async[F].pure))
468489

469-
resource <- TelemetryResourceAutoConfigure[F]
490+
resource <- TelemetryResourceAutoConfigure[F](resourceDetectors)
470491
.configure(config)
471492
.map(resourceCustomizer(_, config))
472493

sdk/all/src/test/scala/org/typelevel/otel4s/sdk/OpenTelemetrySdkSuite.scala

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
6767

6868
test("withConfig - use the given config") {
6969
val config = Config.ofProps(
70-
Map("otel.traces.exporter" -> "none", "otel.metrics.exporter" -> "none")
70+
Map(
71+
"otel.otel4s.resource.detectors" -> "none",
72+
"otel.traces.exporter" -> "none",
73+
"otel.metrics.exporter" -> "none"
74+
)
7175
)
7276

7377
OpenTelemetrySdk
@@ -119,7 +123,11 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
119123

120124
test("addTracerProviderCustomizer - customize tracer provider") {
121125
val config = Config.ofProps(
122-
Map("otel.traces.exporter" -> "none", "otel.metrics.exporter" -> "none")
126+
Map(
127+
"otel.otel4s.resource.detectors" -> "none",
128+
"otel.traces.exporter" -> "none",
129+
"otel.metrics.exporter" -> "none"
130+
)
123131
)
124132

125133
val sampler = Sampler.AlwaysOff
@@ -137,7 +145,11 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
137145

138146
test("addResourceCustomizer - customize a resource") {
139147
val config = Config.ofProps(
140-
Map("otel.traces.exporter" -> "none", "otel.metrics.exporter" -> "none")
148+
Map(
149+
"otel.otel4s.resource.detectors" -> "none",
150+
"otel.traces.exporter" -> "none",
151+
"otel.metrics.exporter" -> "none"
152+
)
141153
)
142154

143155
val default = TelemetryResource.default
@@ -163,6 +175,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
163175
test("addSpanExporterConfigurer - support external configurers") {
164176
val config = Config.ofProps(
165177
Map(
178+
"otel.otel4s.resource.detectors" -> "none",
166179
"otel.traces.exporter" -> "custom-1,custom-2",
167180
"otel.metrics.exporter" -> "none"
168181
)
@@ -203,6 +216,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
203216
test("addSamplerConfigurer - support external configurers") {
204217
val config = Config.ofProps(
205218
Map(
219+
"otel.otel4s.resource.detectors" -> "none",
206220
"otel.traces.exporter" -> "none",
207221
"otel.metrics.exporter" -> "none",
208222
"otel.traces.sampler" -> "custom-sampler",
@@ -237,6 +251,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
237251
test("addTextMapPropagatorConfigurer - support external configurers") {
238252
val config = Config.ofProps(
239253
Map(
254+
"otel.otel4s.resource.detectors" -> "none",
240255
"otel.traces.exporter" -> "none",
241256
"otel.metrics.exporter" -> "none",
242257
"otel.propagators" -> "tracecontext,custom-1,custom-2,baggage",
@@ -281,6 +296,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
281296
test("addMeterProviderCustomizer - customize meter provider") {
282297
val config = Config.ofProps(
283298
Map(
299+
"otel.otel4s.resource.detectors" -> "none",
284300
"otel.traces.exporter" -> "none",
285301
"otel.metrics.exporter" -> "console"
286302
)
@@ -314,6 +330,7 @@ class OpenTelemetrySdkSuite extends CatsEffectSuite {
314330
test("addMeterExporterConfigurer - support external configurers") {
315331
val config = Config.ofProps(
316332
Map(
333+
"otel.otel4s.resource.detectors" -> "none",
317334
"otel.traces.exporter" -> "none",
318335
"otel.metrics.exporter" -> "custom-1,custom-2"
319336
)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2023 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.otel4s.sdk.resource
18+
19+
import cats.effect.Sync
20+
import org.typelevel.otel4s.Attributes
21+
import org.typelevel.otel4s.sdk.TelemetryResource
22+
import org.typelevel.otel4s.semconv.SchemaUrls
23+
24+
private[resource] trait HostDetectorPlatform { self: HostDetector.type =>
25+
26+
def apply[F[_]: Sync]: TelemetryResourceDetector[F] =
27+
new Detector[F]
28+
29+
private class Detector[F[_]: Sync] extends TelemetryResourceDetector[F] {
30+
def name: String = Const.Name
31+
32+
def detect: F[Option[TelemetryResource]] = Sync[F].delay {
33+
val host = Keys.Host(OS.hostname())
34+
val arch = Keys.Arch(normalizeArch(OS.arch()))
35+
36+
Some(TelemetryResource(Attributes(host, arch), Some(SchemaUrls.Current)))
37+
}
38+
}
39+
40+
// transforms https://nodejs.org/api/os.html#osarch values to match the spec:
41+
// https://opentelemetry.io/docs/specs/semconv/resource/host/
42+
private def normalizeArch(arch: String): String =
43+
arch match {
44+
case "arm" => "arm32"
45+
case "ppc" => "ppc32"
46+
case "x64" => "amd64"
47+
case other => other
48+
}
49+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2023 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.otel4s.sdk.resource
18+
19+
import scala.scalajs.js
20+
import scala.scalajs.js.annotation.JSImport
21+
22+
private object OS {
23+
24+
@js.native
25+
@JSImport("os", "arch")
26+
def arch(): String = js.native
27+
28+
@js.native
29+
@JSImport("os", "hostname")
30+
def hostname(): String = js.native
31+
32+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2023 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.otel4s.sdk.resource
18+
19+
import cats.effect.Sync
20+
import cats.syntax.applicativeError._
21+
import cats.syntax.flatMap._
22+
import cats.syntax.functor._
23+
import org.typelevel.otel4s.Attributes
24+
import org.typelevel.otel4s.sdk.TelemetryResource
25+
import org.typelevel.otel4s.semconv.SchemaUrls
26+
27+
import java.net.InetAddress
28+
29+
private[resource] trait HostDetectorPlatform { self: HostDetector.type =>
30+
31+
def apply[F[_]: Sync]: TelemetryResourceDetector[F] =
32+
new Detector[F]
33+
34+
private class Detector[F[_]: Sync] extends TelemetryResourceDetector[F] {
35+
def name: String = Const.Name
36+
37+
def detect: F[Option[TelemetryResource]] =
38+
for {
39+
hostOpt <- Sync[F]
40+
.blocking(InetAddress.getLocalHost.getHostName)
41+
.redeem(_ => None, Some(_))
42+
43+
archOpt <- Sync[F].delay(sys.props.get("os.arch"))
44+
} yield {
45+
val host = hostOpt.map(Keys.Host(_))
46+
val arch = archOpt.map(Keys.Arch(_))
47+
val attributes = host.to(Attributes) ++ arch.to(Attributes)
48+
Option.when(attributes.nonEmpty)(
49+
TelemetryResource(attributes, Some(SchemaUrls.Current))
50+
)
51+
}
52+
}
53+
54+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2023 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.otel4s.sdk.resource
18+
19+
import cats.effect.Sync
20+
import cats.syntax.applicativeError._
21+
import cats.syntax.flatMap._
22+
import cats.syntax.functor._
23+
import org.typelevel.otel4s.Attributes
24+
import org.typelevel.otel4s.sdk.TelemetryResource
25+
import org.typelevel.otel4s.semconv.SchemaUrls
26+
27+
import scala.scalanative.posix.unistd._
28+
import scala.scalanative.unsafe._
29+
import scala.scalanative.unsigned._
30+
31+
private[resource] trait HostDetectorPlatform { self: HostDetector.type =>
32+
33+
def apply[F[_]: Sync]: TelemetryResourceDetector[F] =
34+
new Detector[F]
35+
36+
private class Detector[F[_]: Sync] extends TelemetryResourceDetector[F] {
37+
def name: String = Const.Name
38+
39+
def detect: F[Option[TelemetryResource]] =
40+
for {
41+
hostOpt <- Sync[F].delay(detectHost).handleError(_ => None)
42+
archOpt <- Sync[F].delay(sys.props.get("os.arch"))
43+
} yield {
44+
val host = hostOpt.map(Keys.Host(_))
45+
val arch = archOpt.map(Keys.Arch(_))
46+
val attributes = host.to(Attributes) ++ arch.to(Attributes)
47+
Option.when(attributes.nonEmpty)(
48+
TelemetryResource(attributes, Some(SchemaUrls.Current))
49+
)
50+
}
51+
52+
private def detectHost: Option[String] =
53+
Zone { implicit z =>
54+
val size = 256.toUInt
55+
val hostnameBuffer = alloc[CChar](size)
56+
if (gethostname(hostnameBuffer, size) == 0) {
57+
val hostname = fromCString(hostnameBuffer)
58+
Some(hostname)
59+
} else {
60+
None
61+
}
62+
}
63+
}
64+
65+
}

0 commit comments

Comments
 (0)