SBT add projectID to logs in multiproject build - scala

In an SBT multi-project build, when you run a task on an aggregator project and it runs the tasks in every aggregated subproject then all the logs from each subproject get output together in one big stream.
This makes it hard to debug build issues in a multi-project build as all the logs get mixed together. Is there a way to output the projectID on each log line so that you can quickly identify which subproject the log came from?
Here is an example project:
name := "my-multiproject-build"
lazy val ProjectOne = project
lazy val ProjectTwo = project
lazy val root = project.in( file(".") ).aggregate(ProjectOne, ProjectTwo)
(what happens by default)
sbt package
[info] Packaging ProjectOne.jar ...
[info] Done packaging.
[info] Packaging ProjectTwo.jar ...
[info] Done packaging.
(what I want)
sbt package
[info] [ProjectOne] Packaging ProjectOne.jar ...
[info] [ProjectOne] Done packaging.
[info] [ProjectTwo] Packaging ProjectTwo.jar ...
[info] [ProjectTwo] Done packaging.
I tried looking into SBT custom loggers, but unfortunately the documentation is a bit sparse and I'm by no means an SBT expert.

Like Rich said, there is currently no extension point to customize sbt's logging format. But if you don't mind relying on internal APIs you can get close to what you want, depending on which version of sbt you are using.
Basically you would have to replace the default logManager rather than adding extraLoggers (the API is similar though).
sbt 0.13.x
Our job here looks simpler. We can reuse BufferedLogger to avoid the boilerplate involved in delegating everything to a ConsoleLogger:
logManager := LogManager.withScreenLogger { (_, state) =>
val console = ConsoleLogger(state.globalLogging.console)
new BufferedLogger(console) {
val project = projectID.value.name
override def log(level: Level.Value, message: => String): Unit =
console.log(level, s"[$project] $message")
}
}
sbt 1.0.x
The logging API was changed here to provide event logging. We now have to provide a log4j Appender which is more flexible, but makes our job more difficult. We can't reuse the classes from sbt.internal where the logging implementation has moved, because they are all private, sealed, final, etc. The only thing I could think of short of duplicating the functionality of ConsoleAppender was to hack the output stream:
logManager := LogManager.defaultManager(
ConsoleOut.printStreamOut(new PrintStream(System.out) {
val project = projectID.value.name
override def println(str: String): Unit = {
val (lvl, msg) = str.span(_ != ']')
super.println(s"$lvl] [$project$msg")
}
}))
Note that there is no guarantee println will be called instead of some other print method.
I don't know if it's possible to use a log4j configuration file to customize the format.

Looking through the SBT code, I don't think this is possible nicely.
Here's a build.sbt which does most of what you want.
import sbt.Level
name := "my-multiproject-build"
lazy val ProjectOne = project
lazy val ProjectTwo = project
lazy val root = project.in( file(".") ).aggregate(ProjectOne, ProjectTwo)
val wrapLogger = (project: Project, inner: AbstractLogger) => {
new AbstractLogger {
override def log(level: Level.Value, message: => String): Unit = {
inner.log(
level,
"[" + project.id + "] " + message
)
}
override def setTrace(flag: Int): Unit = inner.setTrace(flag)
override def setLevel(newLevel: Level.Value): Unit = {
// MultiLogger keeps setting this to debug
inner.setLevel(Level.Info)
}
override def setSuccessEnabled(flag: Boolean): Unit = inner.setSuccessEnabled(flag)
override def logAll(events: Seq[LogEvent]): Unit = {
events.foreach(log)
}
override def control(event: _root_.sbt.ControlEvent.Value, message: => String): Unit
= inner.control(event, message)
override def successEnabled: Boolean = inner.successEnabled
override def getLevel = inner.getLevel
override def getTrace: Int = inner.getTrace
override def trace(t: => Throwable): Unit = inner.trace(t)
override def success(message: => String): Unit = inner.success(message)
}
}
extraLoggers in ProjectOne := {
val currentFunction = extraLoggers.value
(key: ScopedKey[_]) => {
val logger = wrapLogger(ProjectOne, ConsoleLogger())
logger.setLevel(Level.Info)
logger +: currentFunction(key)
}
}
extraLoggers in ProjectTwo := {
val currentFunction = extraLoggers.value
(key: ScopedKey[_]) => {
val logger = wrapLogger(ProjectTwo, ConsoleLogger())
logger.setLevel(Level.Info)
logger +: currentFunction(key)
}
}
The output is now duplicated for project-specific logs: once with the project name prepended and once without it.
The output looks like:
[info] Done packaging.
[info] [ProjectTwo] Done packaging.
[info] Done updating.
[info] [ProjectOne] Done updating.
The ConsoleLogger is constructed at MainLogging.defaultScreen and there are no extension points which let you manipulate the log messages that I can see.
If SBT had used a logging library like logback or log4j2, rather than reinventing the wheel with its own logging framework, this would have been possible. :-(

Related

SBT 0.13 Build.scala References to undefined settings

I am new to SBT and I have been trying to build a custom task for this build.
I have a simple build project:
import sbt._
import Keys._
object JsonBuild extends Build{
lazy val barTask = taskKey[Unit]("some simple task")
val afterTestTask1 = barTask := { println("tests ran!") }
val afterTestTask2 = barTask <<= barTask.dependsOn(test in Test)
lazy val myBarTask = taskKey[Unit]("some simple task")
//val afterMyBarTask1 = myBarTask := { println("tests ran!") }
lazy val afterMyBarTask2 = myBarTask <<= (myBarTask).dependsOn(test in Test) map { _ => println("tests ran!") }
//settings ++ Seq(afterMyBarTask2)
override lazy val settings = super.settings ++ Seq(afterMyBarTask2)
}
I keep getting the error:
References to undefined settings:
{.}/*:myBarTask from {.}/*:myBarTask (C:\Users\haques\Documents\workspace\SBT\jsonParser\project\Build.scala:13)
{.}/test:test from {.}/*:myBarTask (C:\Users\haques\Documents\workspace\SBT\jsonParser\project\Build.scala:13)
Did you mean test:test ?
I have googled around and I cannot find a solution.
Can you explain why it is not working?
lazy val myBarTask = taskKey[Unit]("some simple task")
override lazy val settings = super.settings ++ Seq(myBarTask := { (test in Test).value; println("tests ran!") } )
myBarTask is undefined when you call dependsOn. you should define it before using dependsOn. also value call on key (task/setting) is now preferred way to depend on other keys. you can still use your version, but define myBarTask
This has been bothering.
I did a bit more reading.
I think I know why the above code does not work.
lazy val afterMyBarTask2 = myBarTask <<= (myBarTask).dependsOn(test in Test) map { _ => println("tests ran!") }
When I write (myBarTask).dependsOn(test in Test), the project scope for test is chosen by SBT as ThisBuild.
{.}/test:test from {.}/*:myBarTask (C:\Users\haques\Documents\workspace\SBT\jsonParser\project\Build.scala:13)
ThisBuild project scope does not have the setting test in configuration Test.
Only projects have the setting test.
The key I think that setting is added by some default SBT plugin to the projects settings.
You check what scopes settings exist in SBT by using the inspect command.
If you type in the SBT REPL:
{.}/test:test
The output is:
inspect {.}/test:test
[info] No entry for key.
SBT correctly suggests:
test:test which is:
{file:/C:/Users/haques/Documents/workspace/SBT/jsonParser/}jsonparser/test:test
If the project is not specified in the project scope axis, SBT chooses the current project by default.
Every SBT project if not specified has its own project settings.

sbt runs IMain and play makes errors

I've build a litte object, which can interpret scala code on the fly and catches a value out of it.
object Interpreter {
import scala.tools.nsc._
import scala.tools.nsc.interpreter._
class Dummy
val settings = new Settings
settings.usejavacp.value = false
settings.embeddedDefaults[Dummy] // to make imain useable with sbt.
val imain = new IMain(settings)
def run(code: String, returnId: String) = {
this.imain.beQuietDuring{
this.imain.interpret(code)
}
val ret = this.imain.valueOfTerm(returnId)
this.imain.reset()
ret
}
}
object Main {
def main(args: Array[String]) {
println(Interpreter.run("val x = 1", "x"))
}
}
In a pure sbt environment or called by the scala interpreter this code works fine! But if I run this in a simple play (version 2.2.2) application, it gets a null pointer at val ret = this.imain.valueOfTerm(returnId).
play uses also a modified sbt, therefor it should probably work. What does play do that this code doesn't work anymore? Any ideas how to get this code to work in play?
Note
That's the used build.sbt:
name := "Test"
version := "1.0"
scalaVersion := "2.10.3"
libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value
Alternatively I tried this implementation, but it doesen't solve the problem either:
object Interpreter2 {
import scala.tools.nsc._
import scala.tools.nsc.interpreter._
import play.api._
import play.api.Play.current
val settings: Settings = {
lazy val urls = java.lang.Thread.currentThread.getContextClassLoader match {
case cl: java.net.URLClassLoader => cl.getURLs.toList
case _ => sys.error("classloader is not a URLClassLoader")
}
lazy val classpath = urls map {_.toString}
val tmp = new Settings()
tmp.bootclasspath.value = classpath.distinct mkString java.io.File.pathSeparator
tmp
}
val imain = new IMain(settings)
def run(code: String, returnId: String) = {
this.imain.beQuietDuring {
this.imain.interpret(code)
}
val ret = this.imain.valueOfTerm(returnId)
this.imain.reset()
ret
}
}
Useful links I found to make this second implementation:
scala.tools.nsc.IMain within Play 2.1
How to set up classpath for the Scala interpreter in a managed environment?
https://groups.google.com/forum/#!topic/scala-user/wV86VwnKaVk
https://github.com/gourlaysama/play-repl-example/blob/master/app/REPL.scala#L18
https://gist.github.com/mslinn/7205854
After spending a few hours on this issue myself, here is a solution that I came up with. It works both inside SBT and outside. It is also expected to work in a variety of managed environments (like OSGi):
private def getClasspathUrls(classLoader: ClassLoader, acc: List[URL]): List[URL] = {
classLoader match {
case null => acc
case cl: java.net.URLClassLoader => getClasspathUrls(classLoader.getParent, acc ++ cl.getURLs.toList)
case c => LOGGER.error("classloader is not a URLClassLoader and will be skipped. ClassLoader type that was skipped is " + c.getClass)
getClasspathUrls(classLoader.getParent, acc)
}
}
val classpathUrls = getClasspathUrls(this.getClass.getClassLoader, List())
val classpathElements = classpathUrls map {url => url.toURI.getPath}
val classpath = classpathElements mkString java.io.File.pathSeparator
val settings = new Settings
settings.bootclasspath.value = classpath
val imain = new IMain(settings)
// use imain to interpret code. It should be able to access all your application classes as well as dependent libraries.
It's because play uses the "fork in run" feature from sbt. This feature starts a new JVM and this causes that this failure appears:
[info] Failed to initialize compiler: object scala.runtime in compiler mirror not found.
[info] ** Note that as of 2.8 scala does not assume use of the java classpath.
[info] ** For the old behavior pass -usejavacp to scala, or if using a Settings
[info] ** object programatically, settings.usejavacp.value = true.
See: http://www.scala-sbt.org/release/docs/Detailed-Topics/Forking

Custom extraLogger not getting [success] messages in sbt?

I'm using the following code to hook into SBT's logging system to send the logging messages to another process accessible via a server setting:
extraLoggers := {
val clientLogger = FullLogger {
new Logger {
def log(level: Level.Value, message: => String): Unit =
if(level >= Level.Info) server.value.send(Json.arr("print", level.toString(), message))
def success(message: => String): Unit = server.value.send(Json.arr("print", "info", message))
def trace(t: => Throwable): Unit = server.value.send(Json.arr("print", "error", t.toString))
}
}
val currentFunction = extraLoggers.value
(key: ScopedKey[_]) => clientLogger +: currentFunction(key)
}
When I look at the output being spewed out on the other server process, I don't see the messages with green [success] tags appearing. Everything else (i.e. all the [info] messages and the red [error] messages) appear just fine.
Printing out clientLogger.successEnabled gives me true.
What am I doing wrong?
DISCLAIMER: Please use with care as the answer might be incomplete or even totally wrong.
After consulting the sources of sbt my understanding is that extraLoggers is a setting that is only "A function that provides additional loggers for a given setting." and these additional loggers are additional to StandardMain.console.
If it were possible, you would therefore have to set logManager to have a reference to extraLoggers and your own custom sbt.ConsoleOut in build.sbt, e.g.
logManager := LogManager.defaults(extraLoggers.value, new ConsoleOut {
val lockObject = System.out
def print(s: String): Unit = synchronized { print(s) }
def println(s: String): Unit = synchronized { println(s) }
def println(): Unit = synchronized {
System.out.println()
}
})
It won't however work since sbt.ConsoleOut is a sealed trait and hence there is no way to use it outside the file it was defined.
Having said that, I believe, in sbt 0.13.1, it's not possible to "intercept" the [success] message that's printed out when showSuccess is true as it comes out from ConsoleOut that's outside your control.
What you can do with extraLoggers is to have your own custom logging for tasks and streams.value.log.success("Succezz") should work.
The sample build.sbt with extraLoggers and a t task to demo the custom logger.
extraLoggers := {
val clientLogger = FullLogger {
new Logger {
def log(level: Level.Value, message: => String): Unit =
if(level >= Level.Info) println(s"+++ $message at $level")
def success(message: => String): Unit = println(s"+++ success: $message")
def trace(t: => Throwable): Unit = println(s"+++ trace: throwable: $t")
}
}
val currentFunction = extraLoggers.value
(key: ScopedKey[_]) => clientLogger +: currentFunction(key)
}
val t = taskKey[Unit]("Show extraLoggers")
t := {
println(s"Using extraLoggers")
val s: TaskStreams = streams.value
val log = s.log
log.debug("Saying hi...")
log.info("Hello!")
log.error("Error")
log.success("Succezz")
}
With the file, executing the t task gives the following output:
$ sbt
[info] Loading global plugins from /Users/jacek/.sbt/0.13/plugins
[info] Loading project definition from /Users/jacek/sandbox/sbt-0.13.1-extra-loggers/project
[info] Set current project to sbt-0-13-1-extra-loggers (in build file:/Users/jacek/sandbox/sbt-0.13.1-extra-loggers/)
[sbt-0-13-1-extra-loggers]> about
[info] This is sbt 0.13.1
[info] The current project is {file:/Users/jacek/sandbox/sbt-0.13.1-extra-loggers/}sbt-0-13-1-extra-loggers 0.1-SNAPSHOT
[info] The current project is built against Scala 2.10.3
[info] Available Plugins: com.typesafe.sbt.SbtGit, com.typesafe.sbt.SbtProguard, growl.GrowlingTests, org.sbtidea.SbtIdeaPlugin, np.Plugin, com.timushev.sbt.updates.UpdatesPlugin
[info] sbt, sbt plugins, and build definitions are using Scala 2.10.3
[sbt-0-13-1-extra-loggers]> t
Using extraLoggers
[info] Hello!
+++ Hello! at info
[error] Error
+++ Error at error
[success] Succezz
+++ success: Succezz
[success] Total time: 0 s, completed Dec 16, 2013 10:30:48 PM

Getting an SBT task to depend on the OneJar task

I'm having trouble getting my new SBT task 'install' to depend on the OneJar task. Here's my Build.scala file:
import sbt._
import Keys._
import com.github.retronym.SbtOneJar._
object BuildBroBuild extends Build {
val install = TaskKey[Unit]("install", "Installs the JAR and a launcher script into your homedir")
private def installTask = task {
println("Hello world!")
}
override lazy val settings = super.settings ++
Seq(install <<= (oneJar in Global)(installTask dependsOn(_)))
lazy val root = Project(id = "buildbro",
base = file("."),
settings = Project.defaultSettings)
}
And here's the error I'm getting:
[error] Reference to undefined setting:
[error]
[error] */*:one-jar from {.}/*:install
[error] Did you mean *:one-jar ?
[error]
Does anybody know what this means? I believe I have to scope the oneJar TaskKey in a different way. Thanks for any help you can offer.
I think something like this should work:
object BuildBroBuild extends Build {
val install = TaskKey[Unit]("install", "Installs the JAR and a launcher script into your homedir")
private lazy val installTask = install <<= (oneJar, streams) map { case (a, s) => {
// 'a' is the output from the onejar task (ie, the artifact)
println("Hello world!")
}
override lazy val settings = super.settings ++
Seq(installTask)
lazy val root = Project(id = "buildbro",
base = file("."),
settings = Project.defaultSettings)
}
Here, we are taking the output of the oneJar task (as well as streams, which allows for logging, etc) as input for our new task.

sbt 0.11 run task examples needed

My projects are still using sbt 0.7.7 and I find it very convenient to have utility classes that I can run from the sbt prompt. I can also combine this with properties that are separately maintained - typically for environment related values that changes from hosts to hosts. This is an example of my project definition under the project/build directory:
class MyProject(info: ProjectInfo) extends DefaultProject(info) {
//...
lazy val extraProps = new BasicEnvironment {
// use the project's Logger for any properties-related logging
def log = MyProject.this.log
def envBackingPath = path("paths.properties")
// define some properties that will go in paths.properties
lazy val inputFile = property[String]
}
lazy val myTask = task { args =>
runTask(Some("foo.bar.MyTask"),
runClasspath, extraProps.inputFile.value :: args.toList).dependsOn(compile)
describedAs "my-task [options]"
}
}
I can then use my task as my-task option1 option2 under the sbt shell.
I've read the new sbt 0.11 documentation at https://github.com/harrah/xsbt/wiki including the sections on Tasks and TaskInputs and frankly I'm still struggling on how to accomplish what I did on 0.7.7.
It seems the extra properties could simply be replaced a separate environment.sbt, that tasks have to be defined in project/build.scala before being set in build.sbt. It also looks like there is completion support, which looks very interesting.
Beyond that I'm somewhat overwhelmed. How do I accomplish what I did with the new sbt?
You can define a task like this :
val myTask = InputKey[Unit]("my-task")
And your setting :
val inputFile = SettingKey[String]("input-file", "input file description")
You can also define a new configuration like :
lazy val ExtraProps = config("extra-props") extend(Compile)
add this config to your project and use it to set settings for this configuration :
lazy val root = Project("root", file(".")).config( ExtraProps ).settings(
inputFile in ExtraProps := ...
...
myTask in ExtraPops <<= inputTask { (argTask:TaskKey[Seq[String]]) =>
(argTask, inputFile) map { (args:Seq[String], iFile[String]) =>
...
}
}
).dependsOn(compile)
then launch your task with extra-props:my-task