From 18c68db6e446c48c5aed0010a1d057368273685c Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 31 Oct 2017 11:09:29 -0700 Subject: PHI-safe logging from PDS UI --- src/main/scala/xyz/driver/core/logging/phi.scala | 71 ++++++++++++++++++++++ .../scala/xyz/driver/core/logging/phiLogging.scala | 57 +++++++++++++++++ .../driver/core/logging/PhiStringContextTest.scala | 32 ++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/main/scala/xyz/driver/core/logging/phi.scala create mode 100644 src/main/scala/xyz/driver/core/logging/phiLogging.scala create mode 100644 src/test/scala/xyz/driver/core/logging/PhiStringContextTest.scala diff --git a/src/main/scala/xyz/driver/core/logging/phi.scala b/src/main/scala/xyz/driver/core/logging/phi.scala new file mode 100644 index 0000000..224d0fe --- /dev/null +++ b/src/main/scala/xyz/driver/core/logging/phi.scala @@ -0,0 +1,71 @@ +package xyz.driver.core.logging + +import java.net.{URI, URL} +import java.nio.file.Path +import java.time.{LocalDate, LocalDateTime} +import java.util.UUID + +import xyz.driver.core.time.Time + +import scala.concurrent.duration.Duration + +object phi { + + class NoPhiString(private[logging] val text: String) { + // scalastyle:off + @inline def +(that: NoPhiString) = new NoPhiString(this.text + that.text) + } + + implicit class NoPhiStringContext(val sc: StringContext) extends AnyVal { + def noPhi(args: NoPhiString*): NoPhiString = { + val phiArgs = args.map(_.text) + new NoPhiString(sc.s(phiArgs: _*)) + } + } + + /** + * Use it with care! + */ + final case class NoPhi[T](private[logging] val value: T) + extends NoPhiString(Option(value).map(_.toString).getOrElse("")) + + // DO NOT ADD! + // 1. phi"$fullName" is easier to write, than phi"${Unsafe(fullName)}" + // If you wrote the second version, it means that you know, what you doing. + // 2. implicit def toPhiString(s: String): PhiString = Unsafe(s) + + implicit def booleanToPhiString(x: Boolean): NoPhiString = NoPhi(x.toString) + + implicit def uriToPhiString(x: URI): NoPhiString = NoPhi(x.toString) + + implicit def urlToPhiString(x: URL): NoPhiString = NoPhi(x.toString) + + implicit def pathToPhiString(x: Path): NoPhiString = NoPhi(x.toString) + + implicit def timeToPhiString(x: Time): NoPhiString = NoPhi(x.toString) + + implicit def localDateTimeToPhiString(x: LocalDateTime): NoPhiString = NoPhi(x.toString) + + implicit def localDateToPhiString(x: LocalDate): NoPhiString = NoPhi(x.toString) + + implicit def durationToPhiString(x: Duration): NoPhiString = NoPhi(x.toString) + + implicit def uuidToPhiString(x: UUID): NoPhiString = NoPhi(x.toString) + + implicit def tuple2ToPhiString[T1, T2](x: (T1, T2))(implicit inner1: T1 => NoPhiString, + inner2: T2 => NoPhiString): NoPhiString = + x match { case (a, b) => noPhi"($a, $b)" } + + implicit def tuple3ToPhiString[T1, T2, T3](x: (T1, T2, T3))(implicit inner1: T1 => NoPhiString, + inner2: T2 => NoPhiString, + inner3: T3 => NoPhiString): NoPhiString = + x match { case (a, b, c) => noPhi"($a, $b, $c)" } + + implicit def optionToPhiString[T](opt: Option[T])(implicit inner: T => NoPhiString): NoPhiString = opt match { + case None => noPhi"None" + case Some(x) => noPhi"Some($x)" + } + + implicit def iterableToPhiString[T](xs: Iterable[T])(implicit inner: T => NoPhiString): NoPhiString = + NoPhi(xs.map(inner(_).text).mkString("Col(", ", ", ")")) +} diff --git a/src/main/scala/xyz/driver/core/logging/phiLogging.scala b/src/main/scala/xyz/driver/core/logging/phiLogging.scala new file mode 100644 index 0000000..27b6ddc --- /dev/null +++ b/src/main/scala/xyz/driver/core/logging/phiLogging.scala @@ -0,0 +1,57 @@ +package xyz.driver.core.logging + +import java.time.{LocalDateTime, ZoneId} + +import org.slf4j.LoggerFactory +import xyz.driver.core.Id +import xyz.driver.core.auth.User +import xyz.driver.core.logging.phi._ + +trait PhiSafeLogger { + + def error(message: NoPhiString): Unit + + def warn(message: NoPhiString): Unit + + def info(message: NoPhiString): Unit + + def debug(message: NoPhiString): Unit + + def trace(message: NoPhiString): Unit + +} + +class DefaultPhiSafeLogger(underlying: org.slf4j.Logger) extends PhiSafeLogger { + + def error(message: NoPhiString): Unit = underlying.error(message.text) + + def warn(message: NoPhiString): Unit = underlying.warn(message.text) + + def info(message: NoPhiString): Unit = underlying.info(message.text) + + def debug(message: NoPhiString): Unit = underlying.debug(message.text) + + def trace(message: NoPhiString): Unit = underlying.trace(message.text) + +} + +trait PhiSafeLogging { + + protected val logger: PhiSafeLogger = new DefaultPhiSafeLogger(LoggerFactory.getLogger(getClass.getName)) + + /** + * Logs the failMessage on an error level, if isSuccessful is false. + * @return isSuccessful + */ + protected def loggedError(isSuccessful: Boolean, failMessage: NoPhiString): Boolean = { + if (!isSuccessful) { + logger.error(failMessage) + } + isSuccessful + } + + protected def logTime(userId: Id[User], label: NoPhiString, obj: NoPhiString): Unit = { + val now = LocalDateTime.now(ZoneId.of("Z")) + logger.info(noPhi"User id=${NoPhi(userId)} performed an action at $label=$now with a $obj") + } +} diff --git a/src/test/scala/xyz/driver/core/logging/PhiStringContextTest.scala b/src/test/scala/xyz/driver/core/logging/PhiStringContextTest.scala new file mode 100644 index 0000000..4ffe7ae --- /dev/null +++ b/src/test/scala/xyz/driver/core/logging/PhiStringContextTest.scala @@ -0,0 +1,32 @@ +package xyz.driver.core.logging + +import org.scalatest.FreeSpecLike +import xyz.driver.core.logging.phi._ + +class PhiStringContextTest extends FreeSpecLike { + + class Foo(x: Int, y: String) { + val z: Boolean = true + } + + case class Bar(y: Boolean) + + implicit def fooToPhiString(foo: Foo): NoPhiString = new NoPhiString(s"Foo(z=${foo.z})") + + "should not compile if there is no PhiString implicit" in assertDoesNotCompile( + """val bar = Bar(true) + |noPhi"bar is $bar"""".stripMargin + ) + + "should compile if there is a PhiString implicit" in assertCompiles( + """val foo = new Foo(1, "test") + |println(noPhi"foo is $foo}")""".stripMargin + ) + + "should not contain private info" in { + val foo = new Foo(42, "test") + val result = noPhi"foo is $foo".text + assert(!result.contains("test")) + assert(!result.contains("42")) + } +} -- cgit v1.2.3