Skip to content

Commit

Permalink
sdk-contrib: add AWSBeanstalkDetector
Browse files Browse the repository at this point in the history
  • Loading branch information
iRevive committed Sep 14, 2024
1 parent 5d6fd41 commit 5def4cc
Show file tree
Hide file tree
Showing 4 changed files with 311 additions and 5 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ lazy val `sdk-contrib-aws-resource` =
libraryDependencies ++= Seq(
"org.http4s" %%% "http4s-ember-client" % Http4sVersion,
"org.http4s" %%% "http4s-circe" % Http4sVersion,
"io.circe" %%% "circe-parser" % CirceVersion,
"org.http4s" %%% "http4s-dsl" % Http4sVersion % Test
)
)
Expand Down
59 changes: 54 additions & 5 deletions docs/sdk/aws-resource-detectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ AWSLambdaDetector[IO].detect.unsafeRunSync().foreach { resource =>
println("```")
```

### 3. aws-ec2
### 2. aws-ec2

The detector fetches instance metadata from the `http://169.254.169.254` endpoint.
See [AWS documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html) for more
Expand Down Expand Up @@ -106,6 +106,51 @@ AWSEC2Detector[IO](uri"", client).detect.unsafeRunSync().foreach { resource =>
println("```")
```

### 4. aws-beanstalk

The detector parses environment details from the `/var/elasticbeanstalk/xray/environment.conf` file to configure the telemetry resource.

Expected configuration attributes:
- `deployment_id`
- `version_label`
- `environment_name`

```scala mdoc:reset:passthrough
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import fs2.io.file.Files
import io.circe.Json
import io.circe.syntax._
import org.typelevel.otel4s.sdk.contrib.aws.resource._

val content = Json.obj(
"deployment_id" := 2,
"version_label" := "1.1",
"environment_name" := "production-eu-west"
).noSpaces

println("The content of the `/var/elasticbeanstalk/xray/environment.conf` file: ")
println("```json")
println(content)
println("```")

println("Detected resource: ")
println("```yaml")
val detected = Files[IO].tempFile.use { path =>
for {
_ <- fs2.Stream(content).through(fs2.text.utf8.encode).through(Files[IO].writeAll(path)).compile.drain
r <- AWSBeanstalkDetector[IO](path).detect
} yield r
}.unsafeRunSync()

detected.foreach { resource =>
resource.attributes.toList.sortBy(_.key.name).foreach { attribute =>
println(attribute.key.name + ": " + attribute.value)
}
}
println("```")
```

## Getting Started

@:select(build-tool)
Expand Down Expand Up @@ -167,6 +212,8 @@ object TelemetryApp extends IOApp.Simple {
.addResourceDetector(AWSLambdaDetector[IO])
// register AWS EC2 detector
.addResourceDetector(AWSEC2Detector[IO])
// register AWS Beanstalk detector
.addResourceDetector(AWSBeanstalkDetector[IO])
)
.use { autoConfigured =>
val sdk = autoConfigured.sdk
Expand Down Expand Up @@ -203,6 +250,8 @@ object TelemetryApp extends IOApp.Simple {
.addResourceDetector(AWSLambdaDetector[IO])
// register AWS EC2 detector
.addResourceDetector(AWSEC2Detector[IO])
// register AWS Beanstalk detector
.addResourceDetector(AWSBeanstalkDetector[IO])
)
.use { autoConfigured =>
program(autoConfigured.tracerProvider)
Expand Down Expand Up @@ -231,21 +280,21 @@ There are several ways to configure the options:
Add settings to the `build.sbt`:

```scala
javaOptions += "-Dotel.otel4s.resource.detectors.enabled=aws-lambda,aws-ec2"
envVars ++= Map("OTEL_OTEL4S_RESOURCE_DETECTORS_ENABLE" -> "aws-lambda,aws-ec2")
javaOptions += "-Dotel.otel4s.resource.detectors.enabled=aws-lambda,aws-ec2,aws-beanstalk"
envVars ++= Map("OTEL_OTEL4S_RESOURCE_DETECTORS_ENABLE" -> "aws-lambda,aws-ec2,aws-beanstalk")
```

@:choice(scala-cli)

Add directives to the `*.scala` file:

```scala
//> using javaOpt -Dotel.otel4s.resource.detectors.enabled=aws-lambda,aws-ec2
//> using javaOpt -Dotel.otel4s.resource.detectors.enabled=aws-lambda,aws-ec2,aws-beanstalk
```

@:choice(shell)

```shell
$ export OTEL_OTEL4S_RESOURCE_DETECTORS_ENABLED=aws-lambda,aws-ec2
$ export OTEL_OTEL4S_RESOURCE_DETECTORS_ENABLED=aws-lambda,aws-ec2,aws-beanstalk
```
@:@
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2024 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.contrib.aws.resource

import cats.effect.Concurrent
import cats.effect.std.Console
import cats.syntax.flatMap._
import cats.syntax.functor._
import cats.syntax.show._
import fs2.io.file.Files
import fs2.io.file.Path
import fs2.text
import io.circe.Decoder
import org.typelevel.otel4s.AttributeKey
import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.sdk.TelemetryResource
import org.typelevel.otel4s.sdk.resource.TelemetryResourceDetector
import org.typelevel.otel4s.semconv.SchemaUrls
import org.typelevel.otel4s.semconv.attributes.ServiceAttributes

private class AWSBeanstalkDetector[F[_]: Concurrent: Files: Console] private (
path: Path
) extends TelemetryResourceDetector[F] {

import AWSBeanstalkDetector.Const
import AWSBeanstalkDetector.Keys
import AWSBeanstalkDetector.Metadata

def name: String = Const.Name

def detect: F[Option[TelemetryResource]] =
Files[F]
.exists(path)
.ifM(
parseFile,
Console[F].errorln(s"AWSBeanstalkDetector: config file doesn't exist at path $path").as(None)
)

private def parseFile: F[Option[TelemetryResource]] =
for {
content <- Files[F].readAll(path).through(text.utf8.decode).compile.foldMonoid
resource <- io.circe.parser
.decode[Metadata](content)
.fold(
e => Console[F].errorln(show"AWSBeanstalkDetector: cannot parse metadata from $path. $e").as(None),
m => Concurrent[F].pure(Some(build(m)))
)
} yield resource

private def build(metadata: Metadata): TelemetryResource = {
val builder = Attributes.newBuilder

builder.addOne(Keys.CloudProvider, Const.CloudProvider)
builder.addOne(Keys.CloudPlatform, Const.CloudPlatform)

metadata.deploymentId.foreach(id => builder.addOne(Keys.ServiceInstanceId, id.toString))
metadata.versionLabel.foreach(v => builder.addOne(Keys.ServiceVersion, v))
metadata.environmentName.foreach(n => builder.addOne(Keys.ServiceNamespace, n))

TelemetryResource(builder.result(), Some(SchemaUrls.Current))
}

}

object AWSBeanstalkDetector {

private object Const {
val Name = "aws-beanstalk"
val CloudProvider = "aws"
val CloudPlatform = "aws_elastic_beanstalk"
val ConfigFilePath = "/var/elasticbeanstalk/xray/environment.conf"
}

private object Keys {
val CloudProvider: AttributeKey[String] = AttributeKey("cloud.provider")
val CloudPlatform: AttributeKey[String] = AttributeKey("cloud.platform")
val ServiceInstanceId: AttributeKey[String] = AttributeKey("service.instance.id")
val ServiceNamespace: AttributeKey[String] = AttributeKey("service.namespace")
val ServiceVersion: AttributeKey[String] = ServiceAttributes.ServiceVersion
}

private final case class Metadata(
deploymentId: Option[Int],
versionLabel: Option[String],
environmentName: Option[String]
)

private object Metadata {
implicit val metadataDecoder: Decoder[Metadata] =
Decoder.forProduct3(
"deployment_id",
"version_label",
"environment_name"
)(Metadata.apply)
}

/** The detector parses environment details from the `/var/elasticbeanstalk/xray/environment.conf` file.
*
* Expected configuration attributes:
* - `deployment_id`
* - `version_label`
* - `environment_name`
*
* @example
* {{{
* OpenTelemetrySdk
* .autoConfigured[IO](
* // register OTLP exporters configurer
* _.addExportersConfigurer(OtlpExportersAutoConfigure[IO])
* // register AWS Beanstalk detector
* .addResourceDetector(AWSBeanstalkDetector[IO])
* )
* .use { autoConfigured =>
* val sdk = autoConfigured.sdk
* ???
* }
* }}}
*/
def apply[F[_]: Concurrent: Files: Console]: TelemetryResourceDetector[F] =
new AWSBeanstalkDetector[F](Path(Const.ConfigFilePath))

/** The detector parses environment details from the file at the given `path`.
*
* Expected configuration attributes:
* - `deployment_id`
* - `version_label`
* - `environment_name`
*
* @example
* {{{
* OpenTelemetrySdk
* .autoConfigured[IO](
* // register OTLP exporters configurer
* _.addExportersConfigurer(OtlpExportersAutoConfigure[IO])
* // register AWS Beanstalk detector
* .addResourceDetector(AWSBeanstalkDetector[IO])
* )
* .use { autoConfigured =>
* val sdk = autoConfigured.sdk
* ???
* }
* }}}
*/
def apply[F[_]: Concurrent: Files: Console](path: Path): TelemetryResourceDetector[F] =
new AWSBeanstalkDetector[F](path)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2024 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.contrib.aws.resource

import cats.effect.IO
import fs2.Stream
import fs2.io.file.Files
import fs2.io.file.Path
import io.circe.Json
import io.circe.syntax._
import munit.CatsEffectSuite
import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.sdk.TelemetryResource
import org.typelevel.otel4s.semconv.SchemaUrls
import org.typelevel.otel4s.semconv.attributes.ServiceAttributes
import org.typelevel.otel4s.semconv.experimental.attributes.CloudExperimentalAttributes._
import org.typelevel.otel4s.semconv.experimental.attributes.ServiceExperimentalAttributes._

class AWSBeanstalkDetectorSuite extends CatsEffectSuite {

test("parse config file and add attributes") {
Files[IO].tempFile.use { path =>
val id = 11
val versionLabel = "1"
val envName = "production-env"

val content = Json.obj(
"deployment_id" := id,
"version_label" := versionLabel,
"environment_name" := envName
)

val expected = TelemetryResource(
Attributes(
CloudProvider(CloudProviderValue.Aws.value),
CloudPlatform(CloudPlatformValue.AwsElasticBeanstalk.value),
ServiceInstanceId(id.toString),
ServiceNamespace(envName),
ServiceAttributes.ServiceVersion(versionLabel)
),
Some(SchemaUrls.Current)
)

for {
_ <- write(content.noSpaces, path)
r <- AWSBeanstalkDetector[IO](path).detect
} yield assertEquals(r, Some(expected))
}
}

test("add only provider and platform when file is empty") {
Files[IO].tempFile.use { path =>
val expected = TelemetryResource(
Attributes(
CloudProvider(CloudProviderValue.Aws.value),
CloudPlatform(CloudPlatformValue.AwsElasticBeanstalk.value)
),
Some(SchemaUrls.Current)
)

for {
_ <- write("{}", path)
r <- AWSBeanstalkDetector[IO](path).detect
} yield assertEquals(r, Some(expected))
}
}

test("return None when config file is unparsable") {
Files[IO].tempFile.use { path =>
AWSBeanstalkDetector[IO](path).detect.assertEquals(None)
}
}

test("return None when the config file doesn't exist") {
AWSBeanstalkDetector[IO].detect.assertEquals(None)
}

private def write(content: String, path: Path): IO[Unit] =
Stream(content).through(fs2.text.utf8.encode).through(Files[IO].writeAll(path)).compile.drain

}

0 comments on commit 5def4cc

Please sign in to comment.