Skip to content

Commit

Permalink
internal (#3703): Add core module
Browse files Browse the repository at this point in the history
  • Loading branch information
xerial committed Oct 30, 2024
1 parent 529c58b commit 2d45586
Show file tree
Hide file tree
Showing 11 changed files with 552 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ runner.dialectOverride.allowSignificantIndentation = false
runner.dialectOverride.allowQuestionMarkAsTypeWildcard = false

fileOverride {
"glob:airframe-core*/**/scala/**" {
runner.dialect = scala3
}
"glob:**/scala/**" {
runner.dialect = scala213source3
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 wvlet.airframe.core.log


case class LogSource(fileName: String, line: Int, col: Int):
// We do not include the full path of the source due toe overwhleming log message size
def fileLoc = s"${fileName}:${line}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 wvlet.airframe.core.log

object LoggerMacros:
import scala.quoted.*

inline def sourcePos(): LogSource = ${ sourcePos }

private def sourcePos(using q: Quotes): Expr[wvlet.airframe.core.log.LogSource] =
import q.reflect.*
val pos = Position.ofMacroExpansion
val line = Expr(pos.startLine)
val column = Expr(pos.endColumn)
val src = pos.sourceFile
val srcPath: java.nio.file.Path = java.nio.file.Paths.get(src.path)
val fileName = Expr(srcPath.getFileName().toString)
'{ wvlet.airframe.core.log.LogSource(${ fileName }, ${ line } + 1, ${ column }) }


26 changes: 26 additions & 0 deletions airframe-core/src/main/scala/wvlet/airframe/core/log/Color.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 wvlet.airframe.core.log

import scala.io.AnsiColor

object Color extends AnsiColor:
final val GRAY = "\u001b[90m"
final val BRIGHT_RED = "\u001b[91m"
final val BRIGHT_GREEN = "\u001b[92m"
final val BRIGHT_YELLOW = "\u001b[93m"
final val BRIGHT_BLUE = "\u001b[94m"
final val BRIGHT_MAGENTA = "\u001b[95m"
final val BRIGHT_CYAN = "\u001b[96m"
final val BRIGHT_WHITE = "\u001b[97m"
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
* 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 wvlet.airframe.core.log


import java.io.{PrintWriter, StringWriter}
import java.util.logging.Formatter
import java.util.regex.Pattern
import java.util.logging as jl
import wvlet.airframe.core.log.LogLevel.{DEBUG, ERROR, INFO, TRACE, WARN}

/**
* To implement your own log formatter, implement this formatLog(r: LogRecord) method
*/
trait LogFormatter {
def formatLog(r: LogRecord): String
}

object LogFormatter {
import LogTimestampFormatter.*
import Color.*

private def currentThreadName: String = Thread.currentThread().getName

private val testFrameworkFilter =
Pattern.compile("""\s+at (sbt\.|org\.scalatest\.|wvlet\.airspec\.).*""")
val DEFAULT_STACKTRACE_FILTER: String => Boolean = { (line: String) =>
!testFrameworkFilter.matcher(line).matches()
}
private var stackTraceFilter: String => Boolean = DEFAULT_STACKTRACE_FILTER

/**
* Set stack trace line filter
*
* @param filter
*/
def setStackTraceFilter(filter: String => Boolean): Unit = {
stackTraceFilter = filter
}

def formatStacktrace(e: Throwable): String = {
e match {
case null =>
// Exception cause can be null
""
case _ =>
val trace = new StringWriter()
e.printStackTrace(new PrintWriter(trace))
val stackTraceLines = trace.toString.split("\n")
val filtered =
stackTraceLines
.filter(stackTraceFilter)
.sliding(2)
.collect { case Array(a, b) if a != b => b }

(stackTraceLines.headOption ++ filtered).mkString("\n")
}
}

def withColor(prefix: String, s: String) = {
s"${prefix}${s}${Console.RESET}"
}

def highlightLog(level: LogLevel, message: String): String = {
val color = level match {
case ERROR => Console.RED
case WARN => Console.YELLOW
case INFO => Console.CYAN
case DEBUG => Console.GREEN
case TRACE => Console.MAGENTA
case _ => Console.RESET
}
withColor(color, message)
}

def appendStackTrace(m: String, r: LogRecord, coloring: Boolean = true): String = {
r.cause match {
case Some(ex) if coloring =>
s"${m}\n${highlightLog(r.level, formatStacktrace(ex))}"
case Some(ex) =>
s"${m}\n${formatStacktrace(ex)}"
case None =>
m
}
}

object TSVLogFormatter extends LogFormatter {
override def formatLog(record: LogRecord): String = {
val s = Seq.newBuilder[String]
s += formatTimestampWithNoSpaace(record.getMillis())
s += record.level.toString
s += currentThreadName
s += record.leafLoggerName
s += record.message

val log = s.result().mkString("\t")
record.cause match {
case Some(ex) =>
// Print only the first line of the exception message
s"${log}\n${formatStacktrace(ex).split("\n").head}"
case None =>
log
}
}
}

/**
* Simple log formatter that shows only logger name and message
*/
object SimpleLogFormatter extends LogFormatter {
override def formatLog(r: LogRecord): String = {
val log =
s"[${highlightLog(r.level, r.leafLoggerName)}] ${highlightLog(r.level, r.message)}"
appendStackTrace(log, r)
}
}

/**
* log format for command-line user client (without source code location)
*/
object AppLogFormatter extends LogFormatter {
override def formatLog(r: LogRecord): String = {
val logTag = highlightLog(r.level, r.level.name)
val log =
f"${withColor(Console.BLUE, formatTimestamp(r.getMillis()))} ${logTag}%14s [${withColor(Console.WHITE, r.leafLoggerName)}] ${highlightLog(r.level, r.message)}"
appendStackTrace(log, r)
}
}

/**
* log format for debugging source code
*/
object SourceCodeLogFormatter extends LogFormatter {
override def formatLog(r: LogRecord): String = {
val loc =
r.source
.map(source => s" ${withColor(Console.BLUE, s"- (${source.fileLoc})")}")
.getOrElse("")

val logTag = highlightLog(r.level, r.level.name)
val log =
f"${withColor(Console.BLUE, formatTimestamp(r.getMillis()))} ${logTag}%14s [${withColor(
Console.WHITE,
r.leafLoggerName
)}] ${highlightLog(r.level, r.message)} ${loc}"
appendStackTrace(log, r)
}
}

object ThreadLogFormatter extends LogFormatter {
override def formatLog(r: LogRecord): String = {
val loc =
r.source
.map(source => s" ${withColor(Console.BLUE, s"- (${source.fileLoc})")}")
.getOrElse("")

val logTag = highlightLog(r.level, r.level.name)
val log =
f"${withColor(Console.BLUE, formatTimestamp(r.getMillis()))} [${withColor(BRIGHT_BLUE, currentThreadName)}] ${logTag}%14s [${withColor(
Console.WHITE,
r.leafLoggerName
)}] ${highlightLog(r.level, r.message)} ${loc}"
appendStackTrace(log, r)
}
}

/**
* log format for debugging source code without using ANSI colors
*/
object PlainSourceCodeLogFormatter extends LogFormatter {
override def formatLog(r: LogRecord): String = {
val loc =
r.source
.map(source => s" - (${source.fileLoc})")
.getOrElse("")

val log =
f"${formatTimestamp(r.getMillis())} ${r.level.name}%5s [${r.leafLoggerName}] ${r.message} ${loc}"
appendStackTrace(log, r, coloring = false)
}
}

/**
* Enable source code links in the run/debug console of IntelliJ
*/
object IntelliJLogFormatter extends LogFormatter {
override def formatLog(r: LogRecord): String = {
val loc =
r.source
.map(source => s" ${withColor(Console.BLUE, s"- ${r.getLoggerName()}(${source.fileLoc})")}")
.getOrElse("")

val log =
s"[${highlightLog(r.level, r.level.name)}] ${highlightLog(r.level, r.message)}$loc"
appendStackTrace(log, r)
}
}

/**
* For formatting log as is.
*/
object BareFormatter extends LogFormatter {
override def formatLog(r: LogRecord): String = {
val m = r.message
r.cause match {
case Some(ex) =>
s"${m}\n${formatStacktrace(ex)}"
case None =>
m
}
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 wvlet.airframe.core.log

enum LogLevel:
def name: String = this.toString
case OFF, ERROR, WARN, INFO, DEBUG, TRACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 wvlet.airframe.core.log

case class LogRecord(level: LogLevel, source: Option[LogSource], message: String, cause: Option[Throwable]):
private val timestamp = System.currentTimeMillis()
def getMillis: Long = timestamp
def leafLoggerName: String =
val name = getLoggerName()
leafLoggerNameCache.getOrElseUpdate(
name, {
name match {
case null => ""
case name =>
val pos = name.lastIndexOf('.')
if (pos == -1) {
name
}
else {
name.substring(pos + 1)
}
}
}
)

Loading

0 comments on commit 2d45586

Please sign in to comment.