diff --git a/Dockerfile b/Dockerfile index 38fe231..964627f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,10 +12,10 @@ LABEL org.opencontainers.image.vendor="neuland – Büro für Informatik GmbH" LABEL org.opencontainers.image.licenses="Apache-2.0" LABEL org.opencontainers.image.title="bandwhichd-server" LABEL org.opencontainers.image.description="bandwhichd server collecting measurements and calculating statistics" -LABEL org.opencontainers.image.version="0.6.0-rc8" +LABEL org.opencontainers.image.version="0.6.0-rc9" USER guest ENTRYPOINT ["/opt/java/openjdk/bin/java"] CMD ["-jar", "/opt/bandwhichd-server.jar"] EXPOSE 8080 STOPSIGNAL SIGTERM -COPY --from=build --chown=root:root /tmp/bandwhichd-server/target/scala-3.1.3/bandwhichd-server-assembly-0.6.0-rc8.jar /opt/bandwhichd-server.jar \ No newline at end of file +COPY --from=build --chown=root:root /tmp/bandwhichd-server/target/scala-3.1.3/bandwhichd-server-assembly-0.6.0-rc9.jar /opt/bandwhichd-server.jar \ No newline at end of file diff --git a/build.sbt b/build.sbt index 7952421..0eefec6 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ lazy val root = (project in file(".")) .settings( organization := "de.neuland-bfi", name := "bandwhichd-server", - version := "0.6.0-rc8", + version := "0.6.0-rc9", scalaVersion := "3.1.3", Compile / scalaSource := baseDirectory.value / "src" / "main" / "scala", Test / scalaSource := baseDirectory.value / "src" / "test" / "scala", diff --git a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala index bd14b66..9fbceb5 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsCodecs.scala @@ -1,13 +1,10 @@ package de.neuland.bandwhichd.server.adapter.in.v1.stats import de.neuland.bandwhichd.server.domain.stats.* -import de.neuland.bandwhichd.server.lib.dot.* -import io.circe.Json - -import java.util.concurrent.atomic.AtomicReference +import io.circe.{Encoder, Json} object StatsCodecs { - val circeEncoder: io.circe.Encoder[MonitoredStats] = + val encoder: Encoder[MonitoredStats] = (stats: MonitoredStats) => Json.obj( "hosts" -> Json.fromFields( @@ -40,32 +37,4 @@ object StatsCodecs { }) ) ) - - val dotEncoder - : de.neuland.bandwhichd.server.lib.dot.codec.Encoder[MonitoredStats] = - (stats: MonitoredStats) => { - - val nodeStatements: Seq[Node] = - stats.allHosts.toSeq.map(host => - Node( - id = NodeId(host.hostId.uuid.toString), - attributes = Seq( - Attribute.Label(host.host.toString) - ) - ) - ) - val edgeStatements: Seq[Edge] = - stats.connections.toSeq - .map(connection => - Edge( - idA = NodeId(connection._1.uuid.toString), - idB = NodeId(connection._2.uuid.toString) - ) - ) - - Graph( - `type` = Directed, - statements = nodeStatements ++ edgeStatements - ) - } } diff --git a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsController.scala b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsController.scala index b7869eb..1caa254 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsController.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/adapter/in/v1/stats/StatsController.scala @@ -6,9 +6,7 @@ import cats.implicits.* import de.neuland.bandwhichd.server.application.StatsApplicationService import de.neuland.bandwhichd.server.domain.measurement.Timing import de.neuland.bandwhichd.server.domain.stats.* -import de.neuland.bandwhichd.server.lib.dot.Dot import de.neuland.bandwhichd.server.lib.http4s.Helpers -import de.neuland.bandwhichd.server.lib.http4s.dot.DotHttp4s import de.neuland.bandwhichd.server.lib.time.cats.TimeContext import io.circe.Json import io.circe.syntax.* @@ -40,53 +38,10 @@ class StatsController[F[_]: Async]( response <- { val statsWithinMonitoredNetworks: MonitoredStats = monitoredStats.withoutHostsOutsideOfMonitoredNetworks - if (useDotInsteadOfJson(request.headers)) { - import de.neuland.bandwhichd.server.lib.http4s.dot.DotHttp4s.dotEntityEncoder - val dot: Dot = StatsCodecs.dotEncoder(statsWithinMonitoredNetworks) - Ok(dot) - } else { - import org.http4s.circe.CirceEntityEncoder.circeEntityEncoder - val json: Json = - statsWithinMonitoredNetworks.asJson(StatsCodecs.circeEncoder) - Ok(json) - } + import org.http4s.circe.CirceEntityEncoder.circeEntityEncoder + val json: Json = + statsWithinMonitoredNetworks.asJson(StatsCodecs.encoder) + Ok(json) } } yield response - - private def useDotInsteadOfJson(headers: Headers) = { - val maybeAcceptHeader: Option[Accept] = - headers.get( - Header.Select.recurringHeadersWithMerge( - org.http4s.headers.Accept.headerSemigroupInstance, - org.http4s.headers.Accept.headerInstance - ) - ) - - maybeAcceptHeader.fold(false) { acceptHeader => - - val mediaRangeAndQValues: NonEmptyList[MediaRangeAndQValue] = - acceptHeader.values - - val maybeDotMediaRangeAndQValue: Option[MediaRangeAndQValue] = - mediaRangeAndQValues.find { mediaRangeAndQValue => - DotHttp4s.mediaType.satisfiedBy(mediaRangeAndQValue.mediaRange) - } - - maybeDotMediaRangeAndQValue.fold(false) { dotMediaRangeAndQValue => - - val maybeJsonMediaRangeAndQValue = - mediaRangeAndQValues.find { mediaRangeAndQValue => - MediaType.application.json.satisfiedBy( - mediaRangeAndQValue.mediaRange - ) - } - - maybeJsonMediaRangeAndQValue.fold(true) { jsonMediaRangeAndQValue => - dotMediaRangeAndQValue.qValue.compare( - jsonMediaRangeAndQValue.qValue - ) >= 0 - } - } - } - } } diff --git a/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala b/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala index 070d06e..84bd2a6 100644 --- a/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala +++ b/src/main/scala/de/neuland/bandwhichd/server/domain/stats/Stats.scala @@ -7,7 +7,6 @@ import de.neuland.bandwhichd.server.domain.measurement.{Measurement, Timing} import de.neuland.bandwhichd.server.domain.stats.Stats.Bundle import de.neuland.bandwhichd.server.lib.time.Interval import de.neuland.bandwhichd.server.lib.time.cats.TimeContext -import fs2.Stream import java.nio.charset.StandardCharsets.UTF_8 import java.time.temporal.ChronoUnit.HOURS diff --git a/src/main/scala/de/neuland/bandwhichd/server/lib/dot/Dot.scala b/src/main/scala/de/neuland/bandwhichd/server/lib/dot/Dot.scala deleted file mode 100644 index 2cecc9d..0000000 --- a/src/main/scala/de/neuland/bandwhichd/server/lib/dot/Dot.scala +++ /dev/null @@ -1,20 +0,0 @@ -package de.neuland.bandwhichd.server.lib.dot - -sealed trait Dot - -case class Graph(`type`: GraphType, statements: Seq[Statement]) extends Dot - -sealed trait GraphType -case object Undirected extends GraphType -case object Directed extends GraphType - -sealed trait Statement -case class Node(id: NodeId, attributes: Seq[Attribute]) extends Statement -case class Edge(idA: NodeId, idB: NodeId) extends Statement - -case class NodeId(value: String) - -sealed trait Attribute -object Attribute { - case class Label(value: String) extends Attribute -} diff --git a/src/main/scala/de/neuland/bandwhichd/server/lib/dot/codec/Encoder.scala b/src/main/scala/de/neuland/bandwhichd/server/lib/dot/codec/Encoder.scala deleted file mode 100644 index 2e0e567..0000000 --- a/src/main/scala/de/neuland/bandwhichd/server/lib/dot/codec/Encoder.scala +++ /dev/null @@ -1,7 +0,0 @@ -package de.neuland.bandwhichd.server.lib.dot.codec - -import de.neuland.bandwhichd.server.lib.dot.Dot - -trait Encoder[A] { - def apply(a: A): Dot -} diff --git a/src/main/scala/de/neuland/bandwhichd/server/lib/dot/fs2/BC.scala b/src/main/scala/de/neuland/bandwhichd/server/lib/dot/fs2/BC.scala deleted file mode 100644 index de1d018..0000000 --- a/src/main/scala/de/neuland/bandwhichd/server/lib/dot/fs2/BC.scala +++ /dev/null @@ -1,90 +0,0 @@ -package de.neuland.bandwhichd.server.lib.dot.fs2 - -import _root_.fs2.Chunk -import cats.implicits.* -import de.neuland.bandwhichd.server.lib.dot.* - -trait BC[A] { - def toByteChunk(a: A): Chunk[Byte] - extension (a: A) { - def bc: Chunk[Byte] = toByteChunk(a) - } -} - -extension [A](seq: Seq[A]) { - def intersperse(element: A): Seq[A] = - if (seq.size < 2) { - seq - } else { - seq.init.flatMap(a => Seq(a, element)).appended(seq.last) - } -} - -given stringBC: BC[String] with { - override def toByteChunk(string: String): Chunk[Byte] = - Chunk.array(string.getBytes(java.nio.charset.StandardCharsets.UTF_8)) -} - -given nodeId(using BC[String]): BC[NodeId] with { - override def toByteChunk(nodeId: NodeId): Chunk[Byte] = - s"\"${nodeId.value.replace("\"", "\\\"")}\"".bc -} - -given attributeBC(using BC[String]): BC[Attribute] with { - override def toByteChunk(attribute: Attribute): Chunk[Byte] = - attribute match - case Attribute.Label(value) => s"label=\"$value\"".bc -} - -given attributeSeqBC(using BC[String], BC[Attribute]): BC[Seq[Attribute]] with { - override def toByteChunk(attributes: Seq[Attribute]): Chunk[Byte] = - attributes.map(_.bc).intersperse(" ".bc).reduce { case (a, b) => a ++ b } -} - -given statementBC(using - BC[String], - BC[Seq[Attribute]] -): BC[(Statement, GraphType)] with { - override def toByteChunk( - statementAndGraphType: (Statement, GraphType) - ): Chunk[Byte] = - statementAndGraphType._1 match - case Node(id, attributes) if attributes.isEmpty => id.bc - case Node(id, attributes) => id.bc ++ " [".bc ++ attributes.bc ++ "]".bc - case Edge(idA, idB) => - idA.bc ++ (statementAndGraphType._2 match - case Undirected => " -- " - case Directed => " -> " - ).bc ++ idB.bc -} - -given statementSeqBC(using - BC[String], - BC[(Statement, GraphType)] -): BC[(Seq[Statement], GraphType)] with { - override def toByteChunk( - statementsAndGraphType: (Seq[Statement], GraphType) - ): Chunk[Byte] = - statementsAndGraphType._1.foldLeft(Chunk.empty[Byte]) { - case (acc, statement) => - acc ++ " ".bc ++ ( - statement, - statementsAndGraphType._2 - ).bc ++ ";\n".bc - } -} - -given graphBC(using BC[String], BC[(Seq[Statement], GraphType)]): BC[Graph] - with { - override def toByteChunk(graph: Graph): Chunk[Byte] = - (graph.`type` match - case Undirected => "graph {\n" - case Directed => "digraph {\n" - ).bc ++ (graph.statements, graph.`type`).bc ++ "}".bc -} - -given dotBC(using graphBC: BC[Graph]): BC[Dot] with { - override def toByteChunk(dot: Dot): Chunk[Byte] = - dot match - case graph @ Graph(_, _) => graphBC.toByteChunk(graph) -} diff --git a/src/main/scala/de/neuland/bandwhichd/server/lib/http4s/dot/DotHttp4s.scala b/src/main/scala/de/neuland/bandwhichd/server/lib/http4s/dot/DotHttp4s.scala deleted file mode 100644 index 61a1609..0000000 --- a/src/main/scala/de/neuland/bandwhichd/server/lib/http4s/dot/DotHttp4s.scala +++ /dev/null @@ -1,26 +0,0 @@ -package de.neuland.bandwhichd.server.lib.http4s.dot - -import de.neuland.bandwhichd.server.lib.dot.Dot -import org.http4s.headers.`Content-Type` -import org.http4s.{Charset, EntityEncoder, MediaType} - -object DotHttp4s { - val mediaType: MediaType = - MediaType( - mainType = "text", - subType = "vnd.graphviz" - ) - val contentType: `Content-Type` = - `Content-Type`( - mediaType = mediaType, - charset = Charset.`UTF-8` - ) - - implicit def dotEntityEncoder[F[_]]: EntityEncoder[F, Dot] = { - import de.neuland.bandwhichd.server.lib.dot.fs2.{BC, given} - EntityEncoder - .Pure[_root_.fs2.Chunk[Byte]] - .contramap[Dot](summon[BC[Dot]].toByteChunk) - .withContentType(contentType) - } -} diff --git a/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala b/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala index eba0ede..6195fcb 100644 --- a/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala +++ b/src/test/scala/de/neuland/bandwhichd/server/BandwhichdServerApiV1Spec.scala @@ -258,7 +258,7 @@ class BandwhichdServerApiV1Spec } } - "have JSON stats" in { + "have stats" in { // given val request = Request[IO]( method = Method.GET, @@ -315,59 +315,7 @@ class BandwhichdServerApiV1Spec } } - "have DOT stats" in { - // given - val request = Request[IO]( - method = Method.GET, - uri = uri"/v1/stats", - headers = Headers( - Header.Raw( - ci"accept", - "application/json; q=0.8, text/vnd.graphviz; q=0.9" - ), - Header.Raw( - ci"origin", - "http://localhost:3000" - ) - ) - ) - - val app = inMemoryApp[IO]( - fixedTimeContext(MeasurementFixtures.fullTimeframe.end.instant), - configuration - ) - val httpApp = app.httpApp - - for { - _ <- app.measurementApplicationService.record( - MeasurementFixtures.exampleNetworkConfigurationMeasurement - ) - _ <- app.measurementApplicationService.record( - MeasurementFixtures.exampleNetworkUtilizationMeasurement - ) - - // when - result <- httpApp.run(request) - - // then - body <- result.body.through(fs2.text.utf8.decode).compile.string - } yield { - result.status shouldBe Ok - result.headers.headers should contain allOf ( - Header.Raw(ci"access-control-allow-origin", "*"), - Header.Raw(ci"content-type", "text/vnd.graphviz; charset=UTF-8") - ) - body shouldBe - """digraph { - | "c414c2da-714c-4b68-b97e-3f31e18053d2" [label="some-host.example.com"]; - | "959619ee-30a2-3bc8-9b79-4384b5f3f05d" [label="192.168.10.34"]; - | "c414c2da-714c-4b68-b97e-3f31e18053d2" -> "c414c2da-714c-4b68-b97e-3f31e18053d2"; - | "c414c2da-714c-4b68-b97e-3f31e18053d2" -> "959619ee-30a2-3bc8-9b79-4384b5f3f05d"; - |}""".stripMargin - } - } - - "have DOT stats filtered by time" in { + "have stats filtered by time" in { // given val timeframeDataset = MeasurementFixtures.TimeframeDataset.gen() val from = timeframeDataset.cutoffTimestamp.instant @@ -376,10 +324,6 @@ class BandwhichdServerApiV1Spec method = Method.GET, uri = Uri.unsafeFromString(s"/v1/stats?from=$from&to=$to"), headers = Headers( - Header.Raw( - ci"accept", - "application/json; q=0.8, text/vnd.graphviz; q=0.9" - ), Header.Raw( ci"origin", "http://localhost:3000" @@ -407,16 +351,33 @@ class BandwhichdServerApiV1Spec result.status shouldBe Ok result.headers.headers should contain allOf ( Header.Raw(ci"access-control-allow-origin", "*"), - Header.Raw(ci"content-type", "text/vnd.graphviz; charset=UTF-8") + Header.Raw(ci"content-type", "application/json") + ) + val jsonBody = io.circe.parser.parse(body).toTry.get + jsonBody shouldBe obj( + "hosts" -> obj( + timeframeDataset.hostId0.uuid.toString -> obj( + "hostname" -> fromString(timeframeDataset.nc.hostname.toString), + "additional_hostnames" -> arr(), + "connections" -> obj( + timeframeDataset.hostId2.uuid.toString -> obj(), + timeframeDataset.hostId3.uuid.toString -> obj() + ) + ) + ), + "unmonitoredHosts" -> obj( + timeframeDataset.hostId2.uuid.toString -> obj( + "host" -> fromString( + timeframeDataset.con2.remoteSocket.value.host.toString + ) + ), + timeframeDataset.hostId3.uuid.toString -> obj( + "host" -> fromString( + timeframeDataset.con3.remoteSocket.value.host.toString + ) + ) + ) ) - body shouldBe - s"""digraph { - | "${timeframeDataset.hostId0.uuid}" [label="${timeframeDataset.nc.hostname}"]; - | "${timeframeDataset.hostId2.uuid}" [label="${timeframeDataset.con2.remoteSocket.value.host}"]; - | "${timeframeDataset.hostId3.uuid}" [label="${timeframeDataset.con3.remoteSocket.value.host}"]; - | "${timeframeDataset.hostId0.uuid}" -> "${timeframeDataset.hostId2.uuid}"; - | "${timeframeDataset.hostId0.uuid}" -> "${timeframeDataset.hostId3.uuid}"; - |}""".stripMargin } } }