Skip to content

Commit

Permalink
Merge pull request #162 from christiankjaer/ckjaer/observable
Browse files Browse the repository at this point in the history
Observable instruments
  • Loading branch information
iRevive authored Apr 11, 2023
2 parents fce3d4d + c8c64f6 commit 7a95c3d
Show file tree
Hide file tree
Showing 26 changed files with 650 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ object TraceBenchmark {
.setTracerProvider(tracerProvider)
.build()

OtelJava.forSync(otel).flatMap {
OtelJava.forAsync(otel).flatMap {
_.tracerProvider.tracer("trace-benchmark").get
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.typelevel.otel4s.metrics

import cats.Applicative
import cats.effect.Resource

trait Meter[F[_]] {

Expand Down Expand Up @@ -63,6 +64,16 @@ trait Meter[F[_]] {
name: String
): SyncInstrumentBuilder[F, UpDownCounter[F, Long]]

def observableGauge(
name: String
): ObservableInstrumentBuilder[F, Double, ObservableGauge]
def observableCounter(
name: String
): ObservableInstrumentBuilder[F, Long, ObservableCounter]
def observableUpDownCounter(
name: String
): ObservableInstrumentBuilder[F, Long, ObservableUpDownCounter]

}

object Meter {
Expand Down Expand Up @@ -96,5 +107,46 @@ object Meter {
def withDescription(description: String): Self = this
def create: F[UpDownCounter[F, Long]] = F.pure(UpDownCounter.noop)
}
def observableGauge(
name: String
): ObservableInstrumentBuilder[F, Double, ObservableGauge] =
new ObservableInstrumentBuilder[F, Double, ObservableGauge] {
type Self = this.type

def withUnit(unit: String): Self = this
def withDescription(description: String): Self = this
def createWithCallback(
cb: ObservableMeasurement[F, Double] => F[Unit]
): Resource[F, ObservableGauge] =
Resource.pure(new ObservableGauge {})
}

def observableCounter(
name: String
): ObservableInstrumentBuilder[F, Long, ObservableCounter] =
new ObservableInstrumentBuilder[F, Long, ObservableCounter] {
type Self = this.type

def withUnit(unit: String): Self = this
def withDescription(description: String): Self = this
def createWithCallback(
cb: ObservableMeasurement[F, Long] => F[Unit]
): Resource[F, ObservableCounter] =
Resource.pure(new ObservableCounter {})
}

def observableUpDownCounter(
name: String
): ObservableInstrumentBuilder[F, Long, ObservableUpDownCounter] =
new ObservableInstrumentBuilder[F, Long, ObservableUpDownCounter] {
type Self = this.type

def withUnit(unit: String): Self = this
def withDescription(description: String): Self = this
def createWithCallback(
cb: ObservableMeasurement[F, Long] => F[Unit]
): Resource[F, ObservableUpDownCounter] =
Resource.pure(new ObservableUpDownCounter {})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

package org.typelevel.otel4s.metrics

trait ObservableCounter[F[_], A]
trait ObservableCounter
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

package org.typelevel.otel4s.metrics

trait ObservableGauge[F[_], A]
trait ObservableGauge
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

package org.typelevel.otel4s.metrics

trait ObservableInstrumentBuilder[F[_], A] {
type Self <: ObservableInstrumentBuilder[F, A]
import cats.effect.Resource

trait ObservableInstrumentBuilder[F[_], A, Instrument] {
type Self <: ObservableInstrumentBuilder[F, A, Instrument]

/** Sets the unit of measure for this instrument.
*
Expand All @@ -43,7 +45,20 @@ trait ObservableInstrumentBuilder[F[_], A] {
*/
def withDescription(description: String): Self

/** Creates an instrument with the given `unit` and `description` (if any).
/** Creates an instrument with the given callback, using `unit` and
* `description` (if any).
*
* The callback will be called when the instrument is being observed.
*
* The callback is expected to abide by the following restrictions:
* - Short-living and (ideally) non-blocking
* - Run in a finite amount of time
* - Safe to call repeatedly, across multiple threads
*
* @param cb
* The callback which observes measurements when invoked
*/
def create: F[A]
def createWithCallback(
cb: ObservableMeasurement[F, A] => F[Unit]
): Resource[F, Instrument]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2022 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.metrics

import org.typelevel.otel4s.Attribute

trait ObservableMeasurement[F[_], A] {

/** Records a value with a set of attributes.
*
* @param value
* the value to record
*
* @param attributes
* the set of attributes to associate with the value
*/
def record(value: A, attributes: Attribute[_]*): F[Unit]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

package org.typelevel.otel4s.metrics

trait ObservableUpDownCounter[F, A]
trait ObservableUpDownCounter
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2022 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
package metrics

import cats.effect.IO
import cats.effect.Ref
import cats.effect.Resource
import munit.CatsEffectSuite

class ObservableSuite extends CatsEffectSuite {

import ObservableSuite._

test("observable test") {

for {
_ <- new InMemoryObservableInstrumentBuilder[Double]
.createWithCallback(instrument =>
instrument.record(2.0) *> instrument.record(3.0)
)
.use { r =>
for {
_ <- r.observations.get.assertEquals(List.empty)
_ <- r.run
_ <- r.observations.get.assertEquals(
List(Record(3.0, Seq.empty), Record(2.0, Seq.empty))
)
} yield ()
}
} yield ()

}

}

object ObservableSuite {

final case class Record[A](value: A, attributes: Seq[Attribute[_]])

final case class InMemoryObservable[A](
callback: ObservableMeasurement[IO, A] => IO[Unit],
observations: Ref[IO, List[Record[A]]]
) {
def run: IO[Unit] =
callback(new ObservableMeasurement[IO, A] {
def record(value: A, attributes: Attribute[_]*): IO[Unit] =
observations.update(Record(value, attributes) :: _)
})
}

class InMemoryObservableInstrumentBuilder[A]
extends ObservableInstrumentBuilder[IO, A, InMemoryObservable[A]] {

type Self =
ObservableInstrumentBuilder[IO, A, InMemoryObservable[A]]

def withUnit(unit: String): Self = this

def withDescription(description: String): Self = this

def createWithCallback(
cb: ObservableMeasurement[IO, A] => IO[Unit]
): Resource[IO, InMemoryObservable[A]] =
Resource
.eval(Ref.of[IO, List[Record[A]]](List.empty))
.map(obs => InMemoryObservable[A](cb, obs))

}

}
22 changes: 11 additions & 11 deletions docs/examples/honeycomb.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ We will cover the configuration of OpenTelemetry exporter, as well as the instru
otel4s library.

Unlike [Jaeger example](jaeger-docker.md), you do not need to set up a collector service locally. The metrics and traces
will be sent to a remote Honeycomb API.
will be sent to a remote Honeycomb API.

At the time of writing, Honeycomb allows having up to 20 million spans per month for a free account.
It offers robust analysis and visualization tools that are handy for exploring the world of telemetry.
It offers robust analysis and visualization tools that are handy for exploring the world of telemetry.

### Project setup

Expand Down Expand Up @@ -46,12 +46,12 @@ Add directives to the `tracing.scala`:

@:@

1) Add the `otel4s` library
2) Add an OpenTelemetry exporter. Without the exporter, the application will crash
3) Add an OpenTelemetry autoconfigure extension
4) Enable OpenTelemetry SDK autoconfigure mode
5) Add the name of the application to use in the traces
6) Add the Honeycomb API endpoint
1) Add the `otel4s` library
2) Add an OpenTelemetry exporter. Without the exporter, the application will crash
3) Add an OpenTelemetry autoconfigure extension
4) Enable OpenTelemetry SDK autoconfigure mode
5) Add the name of the application to use in the traces
6) Add the Honeycomb API endpoint

### OpenTelemetry SDK configuration

Expand All @@ -72,14 +72,14 @@ Once you have done this, log into your account and navigate to the environment s

The Honeycomb [official configuration guide](https://docs.honeycomb.io/getting-data-in/opentelemetry-overview/).

In order to send metrics and traces to Honeycomb, the API key and dataset name need to be configured.
In order to send metrics and traces to Honeycomb, the API key and dataset name need to be configured.
Since the API key is sensitive data, we advise providing them via environment variables:

```shell
$ export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=your-api-key,x-honeycomb-dataset=honeycomb-example"
```

1) `x-honeycomb-team` - the API key
1) `x-honeycomb-team` - the API key
2) `x-honeycomb-dataset` - the name of the dataset to send metrics to. We use `honeycomb-example` so both metrics and traces appear in the same dataset.

**Note:** if the `x-honeycomb-dataset` header is not configured, the **metrics** will be sent to a dataset called `unknown_metrics`.
Expand Down Expand Up @@ -138,7 +138,7 @@ object TracingExample extends IOApp.Simple {
def otelResource: Resource[IO, Otel4s[IO]] =
Resource
.eval(IO(GlobalOpenTelemetry.get))
.evalMap(OtelJava.forSync[IO])
.evalMap(OtelJava.forAsync[IO])

def run: IO[Unit] = {
otelResource.use { otel4s =>
Expand Down
24 changes: 12 additions & 12 deletions docs/examples/jaeger-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ Add directives to the `tracing.scala`:
//> using lib "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:@OPEN_TELEMETRY_VERSION@-alpha" // <3>
//> using `java-opt` "-Dotel.java.global-autoconfigure.enabled=true" // <4>
//> using `java-opt` "-Dotel.service.name=jaeger-example" // <5>
//> using `java-opt` "-Dotel.metrics.exporter=none" // <6>
//> using `java-opt` "-Dotel.metrics.exporter=none" // <6>
```

@:@

1) Add the `otel4s` library
2) Add an OpenTelemetry exporter. Without the exporter, the application will crash
3) Add an OpenTelemetry autoconfigure extension
4) Enable OpenTelemetry SDK autoconfigure mode
5) Add the name of the application to use in the traces
6) Disable metrics exporter since Jaeger is compatible only with traces
1) Add the `otel4s` library
2) Add an OpenTelemetry exporter. Without the exporter, the application will crash
3) Add an OpenTelemetry autoconfigure extension
4) Enable OpenTelemetry SDK autoconfigure mode
5) Add the name of the application to use in the traces
6) Disable metrics exporter since Jaeger is compatible only with traces

### OpenTelemetry SDK configuration

Expand All @@ -69,9 +69,9 @@ $ docker run --name jaeger \
jaegertracing/all-in-one:1.35
```

1) `-e COLLECTOR_OTLP_ENABLED=true` - enable OpenTelemetry receiver
2) `-p 16686:16686` - forward Jaeger UI
3) `-p 4317:4317` and `-p 4318:4318` - the OpenTelemetry receiver ports for HTTP and gRPC protocols
1) `-e COLLECTOR_OTLP_ENABLED=true` - enable OpenTelemetry receiver
2) `-p 16686:16686` - forward Jaeger UI
3) `-p 4317:4317` and `-p 4318:4318` - the OpenTelemetry receiver ports for HTTP and gRPC protocols

### Application example

Expand Down Expand Up @@ -122,7 +122,7 @@ object TracingExample extends IOApp.Simple {
def tracerResource: Resource[IO, Tracer[IO]] =
Resource
.eval(IO(GlobalOpenTelemetry.get))
.evalMap(OtelJava.forSync[IO])
.evalMap(OtelJava.forAsync[IO])
.evalMap(_.tracerProvider.get("Example"))

def run: IO[Unit] = {
Expand Down Expand Up @@ -157,4 +157,4 @@ Jaeger UI is available at http://localhost:16686. You can find the collected tra

@:image(jaeger_traces_example.png) {
alt = Jaeger Traces Example
}
}
10 changes: 5 additions & 5 deletions examples/src/main/scala/KleisliExample.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,26 @@
*/

import cats.data.Kleisli
import cats.effect.Async
import cats.effect.IO
import cats.effect.IOApp
import cats.effect.Resource
import cats.effect.Sync
import cats.mtl.Local
import io.opentelemetry.api.GlobalOpenTelemetry
import org.typelevel.otel4s.java.OtelJava
import org.typelevel.otel4s.trace.Tracer
import org.typelevel.vault.Vault

object KleisliExample extends IOApp.Simple {
def work[F[_]: Sync: Tracer] =
Tracer[F].span("work").surround(Sync[F].delay(println("I'm working")))
def work[F[_]: Async: Tracer] =
Tracer[F].span("work").surround(Async[F].delay(println("I'm working")))

def tracerResource[F[_]](implicit
F: Sync[F],
F: Async[F],
L: Local[F, Vault]
): Resource[F, Tracer[F]] =
Resource
.eval(Sync[F].delay(GlobalOpenTelemetry.get))
.eval(Async[F].delay(GlobalOpenTelemetry.get))
.map(OtelJava.local[F])
.evalMap(_.tracerProvider.get("kleisli-example"))

Expand Down
Loading

0 comments on commit 7a95c3d

Please sign in to comment.