Logging Configurations

Configuration logging can quickly help you determine which values are being used by the application, and can aid with debugging whenever things go wrong. Since configurations often contain secret values – avoiding having secrets in code being one of the main use cases for configurations – we would like to avoid having these secrets included in any logs. Ciris provides the Secret wrapper type for this purpose. By wrapping your secret configuration values in Secret, we can avoid accidentally including them in logs.

For example, let’s take a look at the following configuration, where the ApiKey is secret. We’re using refinement types to encode validation in the types of our values, refer to the encoding validation section for more information. Note that the ApiKey in the example below would normally be loaded from, for example, a vault service – unless the value itself is not considered a secret, for example, if it was used for testing purposes.

import ciris.Secret
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.string.MatchesRegex
import eu.timepit.refined.types.net.UserPortNumber
import eu.timepit.refined.types.numeric.PosInt
import eu.timepit.refined.types.string.NonEmptyString
import eu.timepit.refined.W

type ApiKey = String Refined MatchesRegex[W.`"[a-zA-Z0-9]{25,40}"`.T]

final case class ApiConfig(
  key: Secret[ApiKey],
  port: UserPortNumber,
  timeoutSeconds: PosInt
)

final case class Config(
  appName: NonEmptyString,
  api: ApiConfig
)

val config =
  Config(
    appName = "my-api",
    api = ApiConfig(
      key = Secret("RacrqvWjuu4KVmnTG9b6xyZMTP7jnX"),
      timeoutSeconds = 10,
      port = 4000
    )
  )

The perhaps easiest way to log the configuration is to use println. As you can see in the example below, the secret configuration value is replaced with a Secret(0a7425a) placeholder when printed, while the remaining values of the configuration are printed with their toString representations as expected.

println(config)
// Config(my-api,ApiConfig(Secret(0a7425a),4000,10))

The value 0a7425a above is a short (first 7 characters) SHA1 hash of the value. This allows you to check whether the expected value is being used or not, without logging the actual value. To calculate the SHA1 short hash for a String, you can for example use sha1sum.

echo -n "RacrqvWjuu4KVmnTG9b6xyZMTP7jnX" | sha1sum | head -c 7
0a7425a

When loading configuration values with type Secret, Ciris will make sure that no sensitive details are included in error messages. In general, potentially sensitive information is only included in results from functions with value in the name: for example, value, sourceValue, toStringWithValue, and toStringWithValues. So make sure you are not accidentally logging the results from such functions.

import ciris.{env, prop}
// import ciris.{env, prop}

env[Secret[Int]]("FILE_ENCODING").
  orElse(prop("file.encoding")).
  value.left.map(_.message)
// res1: scala.util.Either[String,ciris.Secret[Int]] = Left(Missing environment variable [FILE_ENCODING] and system property [file.encoding] with value [<redacted>] cannot be converted to type [Int])

val fileEncoding =
  prop[Secret[String]]("file.encoding")
// fileEncoding: ciris.ConfigEntry[ciris.api.Id,String,String,ciris.Secret[String]] = ConfigEntry(file.encoding, Property)

fileEncoding.sourceValue
// res2: ciris.api.Id[Either[ciris.ConfigError,String]] = Right(UTF8)

fileEncoding.value
// res3: ciris.api.Id[Either[ciris.ConfigError,ciris.Secret[String]]] = Right(Secret(7fa9ad7))

fileEncoding.toStringWithValue
// res4: String = ConfigEntry(file.encoding, Property, Right(Secret(7fa9ad7)))

Logging Improvements

Relying on toString works reasonably well for small configurations, like in the example shown above, but as your configuration grows in size, it can be considerably more difficult to determine which value is which in the output. Making use of toString also means we’re relying on every type having implemented an appropriate toString function, which might not always be the case.

As an alternative, we can make use of the Show type class from cats. However, we would still like to avoid having to manually define the Show behaviour for all the types in the configuration. Luckily, we can use kittens, which in turn uses shapeless to provide generic derivation of type class instances for Show. The Show instances derived by kittens also include the field names of our case classes, so it’s easier to see which configuration value is which in the output. If we’re using refinement types, there’s a refined-cats module which provides Show instances for refinement types.

import cats.Show
import cats.derived._
import cats.implicits._
import ciris.cats._
import eu.timepit.refined.cats.refTypeShow

implicit val showConfig: Show[Config] = {
  import auto.show._
  semi.show
}
println(config.show)
// Config(appName = my-api, api = ApiConfig(key = Secret(0a7425a), port = 4000, timeoutSeconds = 10))

There is also a showPretty derivation in kittens, which uses pretty printing instead of simply rendering everything on one line. If your configuration is somewhat big, it might be preferable to instead use this kind of derivation over the one shown above.

implicit val showConfig: Show[Config] = {
  import auto.showPretty._
  semi.showPretty
}
println(config.show)
// Config(
//   appName = my-api,
//   api = ApiConfig(
//     key = Secret(0a7425a),
//     port = 4000,
//     timeoutSeconds = 10
//   )
// )