diff --git a/docs/shared-content-source/docs/modules/develop/pages/blueprints.adoc b/docs/shared-content-source/docs/modules/develop/pages/blueprints.adoc index 7513911fe..fbb40d00d 100644 --- a/docs/shared-content-source/docs/modules/develop/pages/blueprints.adoc +++ b/docs/shared-content-source/docs/modules/develop/pages/blueprints.adoc @@ -248,3 +248,23 @@ You can explicitly verify if the blueprint connects all the streamlets correctly ---- The blueprint is automatically verified when you use application-level targets, like `buildApp` and `runLocal`. + +You can visualize your blueprint as a graph by using: + +[source,bash] +---- + sbt printAppGraph +---- + +This task will print an ASCII graph to the console. + +Or use: + +[source,bash] +---- + sbt saveAppGraph +---- + +Which will save an ASCII graph to the `appGraph.txt` file under the `target/visuals/blueprint` folder of your Cloudflow project. + +You can also override the `appGraphSavePath` setting key to provide a new path for the App Graph file to save. \ No newline at end of file diff --git a/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowApplicationPlugin.scala b/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowApplicationPlugin.scala index 73a15913f..cb6b04d7a 100644 --- a/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowApplicationPlugin.scala +++ b/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowApplicationPlugin.scala @@ -52,5 +52,6 @@ object CloudflowApplicationPlugin extends AutoPlugin { packageOptions in (Compile, packageBin) += Package.ManifestAttributes(new java.util.jar.Attributes.Name("Blueprint") -> blueprintFile.value.getName), verifyBlueprint := verifyBlueprint.value, - buildApp := cloudflowApplicationCR.value) + buildApp := cloudflowApplicationCR.value, + appGraphSavePath := target.value / "visuals" / "blueprint") } diff --git a/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowKeys.scala b/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowKeys.scala index 80557eceb..cab20c824 100644 --- a/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowKeys.scala +++ b/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowKeys.scala @@ -68,6 +68,7 @@ trait CloudflowSettingKeys { val initialDebugPort = settingKey[Int]("Initial port number for debugging in runLocal. It will be increased by one for each Streamlet") val remoteDebugRunLocal = settingKey[Boolean]("Enable/Disable remote debugging for streamlets in runLocal") + val appGraphSavePath = settingKey[File]("The path where an application graph visualization should be saved.") } trait CloudflowTaskKeys { @@ -82,6 +83,7 @@ trait CloudflowTaskKeys { val extraDockerInstructions = taskKey[Seq[sbtdocker.Instruction]]("A list of instructions to add to the dockerfile.") val verifyBlueprint = taskKey[Unit]("Verify Blueprint.") val printAppGraph = taskKey[Unit]("Print graph of all streamlets and how they are connected.") + val saveAppGraph = taskKey[File]("Save graph of all streamlets and how they are connected as a text file.") val build = taskKey[Unit]("Build the image.") val buildAndPublish = taskKey[Unit]("[Deprecated! Use buildApp] Build and publish the image.") val runLocal = taskKey[Unit]("Run the Cloudflow application in a local Sandbox.") diff --git a/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowLocalRunnerPlugin.scala b/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowLocalRunnerPlugin.scala index 65f305f07..2460dd969 100644 --- a/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowLocalRunnerPlugin.scala +++ b/tools/cloudflow-sbt-plugin/src/main/scala/cloudflow/sbt/CloudflowLocalRunnerPlugin.scala @@ -142,7 +142,7 @@ object CloudflowLocalRunnerPlugin extends AutoPlugin { host } - printAppLayout(resolveConnections(appDescriptor)) + println(getAppLayout(resolveConnections(appDescriptor))) printInfo(runtimeDescriptorByProject, tempDir.toFile, topics, localConfig.message) val processes = runtimeDescriptorByProject.zipWithIndex.map { @@ -177,7 +177,8 @@ object CloudflowLocalRunnerPlugin extends AutoPlugin { } } }.value, - printAppGraph := printApplicationGraph.value) + printAppGraph := printApplicationGraph.value, + saveAppGraph := saveApplicationGraph.value) def banner(bannerChar: Char)(name: String)(message: Any): Unit = { val title = s" $name " @@ -310,7 +311,22 @@ object CloudflowLocalRunnerPlugin extends AutoPlugin { logger.error("LocalRunner: ApplicationDescriptor is not present. This is a bug. Please report it.") throw new IllegalStateException("ApplicationDescriptor is not present") } - printAppLayout(resolveConnections(appDescriptor)) + println(getAppLayout(resolveConnections(appDescriptor))) + } + + def saveApplicationGraph: Def.Initialize[Task[File]] = Def.task { + implicit val logger = streams.value.log + val _appDescriptor = applicationDescriptor.value + val appGraphDir = appGraphSavePath.value + val appGraphFile = appGraphDir / "appGraph.txt" + val appDescriptor = _appDescriptor.getOrElse { + logger.error("LocalRunner: ApplicationDescriptor is not present. This is a bug. Please report it.") + throw new IllegalStateException("ApplicationDescriptor is not present") + } + val layoutGraph = getAppLayout(resolveConnections(appDescriptor)) + IO.write(appGraphFile, layoutGraph) + logger.info(s"App graph file is generated: $appGraphFile") + appGraphFile } def resolveConnections(appDescriptor: ApplicationDescriptor): List[(String, String)] = { @@ -342,12 +358,19 @@ object CloudflowLocalRunnerPlugin extends AutoPlugin { }.toList } + @deprecated("Use 'getAppLayout' instead") def printAppLayout(connections: List[(String, String)]): Unit = { val vertices = connections.flatMap { case (a, b) => Seq(a, b) }.toSet val graph = Graph(vertices = vertices, edges = connections) println(GraphLayout.renderGraph(graph)) } + def getAppLayout(connections: List[(String, String)]): String = { + val vertices = connections.flatMap { case (a, b) => Seq(a, b) }.toSet + val graph = Graph(vertices = vertices, edges = connections) + GraphLayout.renderGraph(graph) + } + def scaffoldRuntime( projectId: String, descriptor: ApplicationDescriptor, diff --git a/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/build.sbt b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/build.sbt new file mode 100644 index 000000000..d8745472d --- /dev/null +++ b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/build.sbt @@ -0,0 +1,11 @@ +lazy val helloWorld = (project in file(".")) + .enablePlugins(CloudflowApplicationPlugin, CloudflowAkkaPlugin) + .settings( + scalaVersion := "2.12.11", + name := "hello-world", + version := "0.0.1", + cloudflowDockerBaseImage := "adoptopenjdk/openjdk11:alpine", + libraryDependencies ++= Seq( + "ch.qos.logback" % "logback-classic" % "1.2.3" + ) + ) diff --git a/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/project/plugins.sbt b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/project/plugins.sbt new file mode 100644 index 000000000..4174b1c12 --- /dev/null +++ b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/project/plugins.sbt @@ -0,0 +1,7 @@ +sys.props.get("plugin.version") match { + case Some(x) => addSbtPlugin("com.lightbend.cloudflow" % "sbt-cloudflow" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} + +libraryDependencies += "com.lihaoyi" %% "ujson" % "0.9.5" diff --git a/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/src/main/blueprint/blueprint.conf b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/src/main/blueprint/blueprint.conf new file mode 100644 index 000000000..86125fea0 --- /dev/null +++ b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/src/main/blueprint/blueprint.conf @@ -0,0 +1,5 @@ +blueprint { + streamlets { + hello-world = helloworld.HelloWorldShape + } +} diff --git a/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/src/main/scala/helloworld/HelloWorld.scala b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/src/main/scala/helloworld/HelloWorld.scala new file mode 100644 index 000000000..8c0e8d538 --- /dev/null +++ b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/src/main/scala/helloworld/HelloWorld.scala @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016-2021 Lightbend Inc. + * + * 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 helloworld + +import akka.stream.scaladsl._ +import cloudflow.akkastream._ +import cloudflow.akkastream.scaladsl._ +import cloudflow.streamlets._ + +class HelloWorldShape extends AkkaStreamlet { + val shape = StreamletShape.empty + + def createLogic = new RunnableGraphStreamletLogic() { + def runnableGraph = + Source + .single("Hello, world!") + .map(println) + .to(Sink.ignore) + } +} + diff --git a/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/test b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/test new file mode 100644 index 000000000..016814fb8 --- /dev/null +++ b/tools/cloudflow-sbt-plugin/src/sbt-test/sbt-cloudflow/app-graph-generation/test @@ -0,0 +1,6 @@ +# save an app graph file to the default path +> saveAppGraph +$ exists target/visuals/blueprint/appGraph.txt + +# print an app graph to the console +> printAppGraph