Skip to content

Commit

Permalink
Merge pull request #16 from NomadBlacky/#9
Browse files Browse the repository at this point in the history
Support part of Metrics API (#9)
  • Loading branch information
Takumi Kadowaki authored Aug 2, 2019
2 parents 8eab2ca + 10058f7 commit 0529282
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 10 deletions.
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ assert(client.validate())

## Examples

### Service Checks
### [Service Checks](https://docs.datadoghq.com/api/?lang=bash#service-checks)

#### [Post a check run](https://docs.datadoghq.com/api/?lang=bash#post-a-check-run)

Expand All @@ -90,5 +90,43 @@ val response = client.serviceCheck(
tags = List("env" -> "prod")
)

assert(response.status == "ok")
assert(response.isOk)
```

### [Metrics](https://docs.datadoghq.com/api/?lang=bash#metrics)

#### [Get list of active metrics](https://docs.datadoghq.com/api/?lang=bash#get-list-of-active-metrics)

```scala
import java.time._, temporal._

val client = scaladog.Client()

val from = Instant.now().minus(1, ChronoUnit.DAYS)
val host = "myhost"
val response = client.getMetrics(from, host)

println(response) // GetMetricsResponse(List(test.metric),2019-07-30T15:22:39Z,Some(myhost))
```

#### [Post timeseries points](https://docs.datadoghq.com/api/?lang=bash#post-timeseries-points)

```scala
import scaladog._
import java.time.Instant
import scala.util.Random

val response = scaladog.Client().postMetrics(
Seq(
Series(
metric = "test.metric",
points = Seq(Point(Instant.now(), Random.nextInt(1000))),
host = "myhost",
tags = Seq(Tag("project:scaladog")),
MetricType.Gauge
)
)
)

assert(response.isOk)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package scaladog

import java.time.Instant
import java.time.temporal.ChronoUnit

import org.scalatest.Inside

import scala.util.Random

class MetricsAPIIntegrationTest extends ClientITSpec with Inside {

test("GET /metrics") {
val from = Instant.now().minus(1, ChronoUnit.DAYS).truncatedTo(ChronoUnit.SECONDS)
val host = "myhost"
val response = client.getMetrics(from, host)
inside(response) { case GetMetricsResponse(_, actualFrom, actualHost) =>
assert(actualFrom == from)
assert(Some(host) == actualHost)
}
}

test("POST /series") {
val response = client.postMetrics(
Seq(
Series(
metric = "test.metric",
points = Seq(Point(Instant.now(), Random.nextInt(1000))),
host = "myhost",
tags = Seq(Tag("project:scaladog")),
MetricType.Gauge
)
)
)
assert(response.isOk)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ class ServiceChecksAPIIntegrationTest extends ClientITSpec {
tags = List("env" -> "integration_test")
)

assert(response.status == "ok")
assert(response.isOk)
}
}
42 changes: 39 additions & 3 deletions src/main/scala/scaladog/Client.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ trait Client {
timestamp: Instant = Instant.now(),
message: String = "",
tags: Iterable[(String, String)] = Iterable.empty
): PostServiceCheckResponse
): StatusResponse
def getMetrics(from: Instant, host: String = ""): GetMetricsResponse
def postMetrics(series: Seq[Series]): StatusResponse
}

private[scaladog] class ClientImpl(
Expand Down Expand Up @@ -45,7 +47,7 @@ private[scaladog] class ClientImpl(
timestamp: Instant = Instant.now(),
message: String = "",
tags: Iterable[(String, String)] = Iterable.empty
): PostServiceCheckResponse = {
): StatusResponse = {
val bodyJson = ujson.Obj(
"check" -> check,
"host_name" -> hostName,
Expand All @@ -63,7 +65,41 @@ private[scaladog] class ClientImpl(
)

throwErrorOr(response) { res =>
PostServiceCheckResponse(ujson.read(res.text).obj("status").str)
StatusResponse(ujson.read(res.text).obj("status").str)
}
}

def getMetrics(from: Instant, host: String = ""): GetMetricsResponse = {
val response = requester(requests.get)
.apply(
url = s"$baseUrl/metrics",
params = Iterable(
"api_key" -> apiKey,
"application_key" -> appKey,
"from" -> from.getEpochSecond.toString,
"host" -> host
)
)

throwErrorOr(response) { res =>
DDPickle.read[GetMetricsResponse](res.text())
}
}

def postMetrics(series: Seq[Series]): StatusResponse = {
val response = requester(requests.post)
.apply(
url = s"$baseUrl/series",
params = Iterable(
"api_key" -> apiKey,
"application_key" -> appKey
),
headers = Iterable(Client.jsonHeader),
data = DDPickle.write(PostMetricsRequest(series))
)

throwErrorOr(response) { res =>
DDPickle.read[StatusResponse](res.text())
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/main/scala/scaladog/GetMetricsResponse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package scaladog

import java.time.Instant

import DDPickle._

case class GetMetricsResponse(
metrics: Seq[String],
from: Instant,
private val host: Option[String] = None
)

object GetMetricsResponse {
implicit val reader: Reader[GetMetricsResponse] = macroR
}
23 changes: 23 additions & 0 deletions src/main/scala/scaladog/MetricType.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package scaladog

sealed abstract class MetricType(val name: String) {
def asMetricType: MetricType = this
}

object MetricType {
case object Gauge extends MetricType("gauge")
case object Rate extends MetricType("rate")
case object Count extends MetricType("count")

val all: Set[MetricType] = Set(Gauge, Rate, Count)

def fromString(str: String): Option[MetricType] = all.find(_.name == str)

implicit def readwriter: DDPickle.ReadWriter[MetricType] =
DDPickle
.readwriter[String]
.bimap(
_.name,
s => fromString(s).getOrElse(throw new IllegalArgumentException(s"Invalid MetricType: $s"))
)
}
7 changes: 7 additions & 0 deletions src/main/scala/scaladog/PostMetricsRequest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package scaladog

private[scaladog] case class PostMetricsRequest(series: Seq[Series])

private[scaladog] object PostMetricsRequest {
implicit val writer: DDPickle.Writer[PostMetricsRequest] = DDPickle.macroW
}
3 changes: 0 additions & 3 deletions src/main/scala/scaladog/PostServiceCheckResponse.scala

This file was deleted.

22 changes: 22 additions & 0 deletions src/main/scala/scaladog/Series.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package scaladog

import java.time.Instant

case class Series(
metric: String,
points: Seq[Point],
host: String = "",
tags: Seq[Tag] = Seq.empty,
@upickle.implicits.key("type") metricType: MetricType = MetricType.Gauge
)

object Series {
implicit val writer: DDPickle.Writer[Series] = DDPickle.macroW
}

case class Point(timestamp: Instant, value: BigDecimal)

object Point {
implicit val writer: DDPickle.Writer[Point] =
DDPickle.writer[ujson.Arr].comap(p => ujson.Arr(p.timestamp.getEpochSecond, p.value.toString()))
}
9 changes: 9 additions & 0 deletions src/main/scala/scaladog/StatusResponse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package scaladog

case class StatusResponse(status: String) {
val isOk: Boolean = status.toLowerCase == "ok"
}

object StatusResponse {
implicit val reader: DDPickle.Reader[StatusResponse] = DDPickle.macroR
}
26 changes: 26 additions & 0 deletions src/main/scala/scaladog/Tag.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package scaladog

sealed trait Tag {
def asString: String
def asTag: Tag = this
}

object Tag {

case class Value(value: String) extends Tag {
val asString: String = value
}

case class KeyValue(key: String, value: String) extends Tag {
val asString: String = s"$key:$value"
}

def apply(str: String): Tag = {
val (left, right) = str.span(_ != ':')
if (right.isEmpty) Value(left)
else KeyValue(left, right.drop(1))
}

implicit val readwriter: DDPickle.ReadWriter[Tag] =
DDPickle.readwriter[String].bimap(_.asString, apply)
}
44 changes: 44 additions & 0 deletions src/main/scala/scaladog/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import java.time.Instant

package object scaladog {

/**
* http://www.lihaoyi.com/upickle/#CustomConfiguration
*/
private[scaladog] object DDPickle extends upickle.AttributeTagged {
private def camelToSnake(s: String) = {
s.split("(?=[A-Z])", -1).map(_.toLowerCase).mkString("_")
}
private def snakeToCamel(s: String) = {
val res = s.split("_", -1).map(x => x(0).toUpper + x.drop(1)).mkString
s(0).toLower + res.drop(1)
}

override def objectAttributeKeyReadMap(s: CharSequence): String =
snakeToCamel(s.toString)
override def objectAttributeKeyWriteMap(s: CharSequence): String =
camelToSnake(s.toString)

override def objectTypeKeyReadMap(s: CharSequence): String =
snakeToCamel(s.toString)
override def objectTypeKeyWriteMap(s: CharSequence): String =
camelToSnake(s.toString)

override implicit def OptionWriter[T: Writer]: Writer[Option[T]] =
implicitly[Writer[T]].comap[Option[T]] {
case None => null.asInstanceOf[T]
case Some(x) => x
}

override implicit def OptionReader[T: Reader]: Reader[Option[T]] =
implicitly[Reader[T]].mapNulls {
case null => None
case x => Some(x)
}
}

import DDPickle._

private[scaladog] implicit val instantR: Reader[Instant] =
reader[String].map(s => Instant.ofEpochSecond(s.toInt))
}
44 changes: 43 additions & 1 deletion src/test/scala/scaladog/ClientImplTest.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package scaladog
import java.net.HttpCookie
import java.time.Instant
import java.time.temporal.ChronoUnit

import org.scalatest.FunSpec
import requests._
Expand Down Expand Up @@ -75,7 +77,47 @@ class ClientImplTest extends FunSpec {
)

val actual = client.serviceCheck("app.is_ok", "myhost", ServiceCheckStatus.OK)
val expect = PostServiceCheckResponse("ok")
assert(actual.isOk)
}

it("getMetrics") {
val client = genTestClient(
url = "https://api.datadoghq.com/api/v1/check_run",
statusCode = 200,
"""{
| "metrics": [
| "system.cpu.guest",
| "system.cpu.idle",
| "system.cpu.iowait"
| ],
| "from": "1559347200",
| "host": "myhost"
|}
""".stripMargin.trim
)

val actual = client.getMetrics(Instant.now().minus(1, ChronoUnit.DAYS), "myhost")
val expect = GetMetricsResponse(
Seq("system.cpu.guest", "system.cpu.idle", "system.cpu.iowait"),
Instant.ofEpochSecond(1559347200L),
Some("myhost")
)
assert(actual == expect)
}

it("postMetrics") {
val client = genTestClient(
url = "https://api.datadoghq.com/api/v1/series",
statusCode = 200,
"""{
| "status":"ok"
|}
""".stripMargin.trim
)

val actual = client.postMetrics(Seq.empty)
val expect = StatusResponse("ok")

assert(actual == expect)
}
}
Expand Down
21 changes: 21 additions & 0 deletions src/test/scala/scaladog/MetricTypeTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package scaladog

import org.scalatest.FunSpec

class MetricTypeTest extends FunSpec {

describe("readwriter") {

it("should read a string") {
assert(DDPickle.read[MetricType]("\"rate\"") == MetricType.Rate)
}

it("should throw an IllegalArgumentException when read an invalid string") {
assertThrows[IllegalArgumentException](DDPickle.read[MetricType]("\"invalid\"") == null)
}

it("should write JSON from a string") {
assert(DDPickle.write(MetricType.Gauge.asMetricType) == "\"gauge\"")
}
}
}
Loading

0 comments on commit 0529282

Please sign in to comment.