Add first metrics to non-repudiation proxy (#8766)

* Add first metrics to non-repudiation proxy

changelog_begin
changelog_end

Contributes to https://github.com/digital-asset/daml/issues/8635

Add a few key metrics for the non-repudiation proxy, with more to follow,
in particular keeping track of the performance overhead associated with
accessing the underlying database.

All metrics can be seen in com.daml.nonrepudiation.Metrics

Running the conformance tests successfully shows a summary of those
metrics with the expected period (five seconds).

* Address https://github.com/digital-asset/daml/pull/8766#discussion_r575044128
This commit is contained in:
Stefano Baghino 2021-02-15 14:02:35 +01:00 committed by GitHub
parent dbd017ee49
commit a2d87b9396
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 187 additions and 22 deletions

View File

@ -20,7 +20,7 @@ import com.daml.ledger.api.v1.command_service.CommandServiceGrpc.CommandService
import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionService import com.daml.ledger.api.v1.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionService
import com.daml.ledger.resources.{ResourceContext, ResourceOwner} import com.daml.ledger.resources.{ResourceContext, ResourceOwner}
import com.daml.nonrepudiation.client.SigningInterceptor import com.daml.nonrepudiation.client.SigningInterceptor
import com.daml.nonrepudiation.{AlgorithmString, NonRepudiationProxy} import com.daml.nonrepudiation.{AlgorithmString, MetricsReporterOwner, NonRepudiationProxy}
import com.daml.platform.sandbox.config.SandboxConfig import com.daml.platform.sandbox.config.SandboxConfig
import com.daml.platform.sandboxnext.{Runner => Sandbox} import com.daml.platform.sandboxnext.{Runner => Sandbox}
import com.daml.ports.Port import com.daml.ports.Port
@ -77,6 +77,7 @@ final class NonRepudiationProxyConformance
sandboxChannelBuilder, sandboxChannelBuilder,
shutdownTimeout = 5.seconds, shutdownTimeout = 5.seconds,
) )
_ <- MetricsReporterOwner.slf4j[ResourceContext](period = 5.seconds)
transactor <- managedHikariTransactor(postgresDatabase.url, maxPoolSize = 10) transactor <- managedHikariTransactor(postgresDatabase.url, maxPoolSize = 10)
db = Tables.initialize(transactor) db = Tables.initialize(transactor)
_ = db.certificates.put(certificate) _ = db.certificates.put(certificate)

View File

@ -23,6 +23,7 @@ da_scala_library(
"//libs-scala/resources", "//libs-scala/resources",
"//runtime-components/non-repudiation-core", "//runtime-components/non-repudiation-core",
"@maven//:com_google_guava_guava", "@maven//:com_google_guava_guava",
"@maven//:io_dropwizard_metrics_metrics_core",
"@maven//:org_slf4j_slf4j_api", "@maven//:org_slf4j_slf4j_api",
], ],
) )

View File

@ -5,6 +5,8 @@ package com.daml.nonrepudiation
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import com.codahale.metrics.Timer
object CertificateRepository { object CertificateRepository {
trait Read { trait Read {
@ -15,6 +17,11 @@ object CertificateRepository {
def put(certificate: X509Certificate): FingerprintBytes def put(certificate: X509Certificate): FingerprintBytes
} }
final class Timed(timer: Timer, delegate: Read) extends Read {
override def get(fingerprint: FingerprintBytes): Option[X509Certificate] =
timer.time(() => delegate.get(fingerprint))
}
} }
trait CertificateRepository extends CertificateRepository.Read with CertificateRepository.Write trait CertificateRepository extends CertificateRepository.Read with CertificateRepository.Write

View File

@ -0,0 +1,55 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.nonrepudiation
import com.codahale.metrics.{Meter, MetricRegistry, Timer}
object Metrics extends Metrics {
// We only need a singleton right now
// Having multiple registries is useful
// "if you want to organize your metrics in particular reporting groups"
// See: https://metrics.dropwizard.io/4.1.2/manual/core.html
object Registry extends MetricRegistry
}
sealed abstract class Metrics {
private val Prefix = "daml.nonrepudiation"
private def name(suffix: String): String = s"$Prefix.$suffix"
// For further details on the metrics below, see: https://metrics.dropwizard.io/4.1.2/manual/core.html
// Quick reference:
// - meters track rates, keeping both historical mean and exponentially-weighted
// moving average over the last 1, 5 and 15 minutes
// - timers act as meters and also keep an histogram of the time for the
// measured action, giving exponentially more weight to more recent data
// daml.nonrepudiation.processing
// Overall time taken from interception to forwarding to the participant (or rejecting)
val processingTimer: Timer = Metrics.Registry.timer(name("processing"))
// daml.nonrepudiation.get_key
// Time taken to retrieve the key from the certificate store
// Part of the time tracked in daml.nonrepudiation.processing
val getKeyTimer: Timer = Metrics.Registry.timer(name("get_key"))
// daml.nonrepudiation.verify_signature
// Time taken to verify the signature of a command
// Part of the time tracked in daml.nonrepudiation.processing
val verifySignatureTimer: Timer = Metrics.Registry.timer(name("verify_signature"))
// daml.nonrepudiation.add_signed_payload
// Time taken to add the signed payload before ultimately forwarding the command
// Part of the time tracked in daml.nonrepudiation.processing
val addSignedPayloadTimer: Timer = Metrics.Registry.timer(name("add_signed_payload"))
// daml.nonrepudiation.rejections
// Rate of calls that are being rejected before they can be forwarded to the participant
// Historical and exponentially-weighted moving average rate over the latest 1, 5 and 15 minutes
val rejectionsMeter: Meter = Metrics.Registry.meter(name("rejections"))
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.nonrepudiation
import com.codahale.metrics.{ScheduledReporter, Slf4jReporter}
import com.daml.resources._
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
// We don't need access to the underlying resource, we use
// the owner only to manage the reporter's life cycle
sealed abstract class MetricsReporterOwner[Context: HasExecutionContext]
extends AbstractResourceOwner[Context, Unit]
object MetricsReporterOwner {
def slf4j[Context: HasExecutionContext](
period: FiniteDuration
): MetricsReporterOwner[Context] =
new Scheduled(period, Slf4jReporter.forRegistry(Metrics.Registry).build())
private final class Scheduled[Context: HasExecutionContext, Delegate <: ScheduledReporter](
period: FiniteDuration,
reporter: Delegate,
) extends MetricsReporterOwner[Context] {
override def acquire()(implicit context: Context): Resource[Context, Unit] = {
ReleasableResource(Future(reporter.start(period.length, period.unit)))(_ =>
Future(reporter.stop())
)
}
}
}

View File

@ -11,7 +11,7 @@ import io.grpc.{Channel, Server, ServerBuilder}
object NonRepudiationProxy { object NonRepudiationProxy {
def owner[Context: HasExecutionContext]( def owner[Context](
participant: Channel, participant: Channel,
serverBuilder: ServerBuilder[_], serverBuilder: ServerBuilder[_],
certificateRepository: CertificateRepository.Read, certificateRepository: CertificateRepository.Read,
@ -19,13 +19,15 @@ object NonRepudiationProxy {
timestampProvider: Clock, timestampProvider: Clock,
serviceName: String, serviceName: String,
serviceNames: String* serviceNames: String*
): AbstractResourceOwner[Context, Server] = { )(implicit context: HasExecutionContext[Context]): AbstractResourceOwner[Context, Server] = {
val signatureVerification = val signatureVerification =
new SignatureVerificationInterceptor( new SignatureVerificationInterceptor(
certificateRepository, certificateRepository,
signedPayloadRepository, signedPayloadRepository,
timestampProvider, timestampProvider,
) )
ReverseProxy.owner( ReverseProxy.owner(
backend = participant, backend = participant,
serverBuilder = serverBuilder, serverBuilder = serverBuilder,
@ -33,6 +35,7 @@ object NonRepudiationProxy {
.map(service => service -> Seq(signatureVerification)) .map(service => service -> Seq(signatureVerification))
.toMap, .toMap,
) )
} }
} }

View File

@ -0,0 +1,39 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.nonrepudiation
import java.security.Signature
import com.codahale.metrics.Timer
import org.slf4j.LoggerFactory
import scala.util.Try
object SignatureVerification {
private val logger = LoggerFactory.getLogger(classOf[SignatureVerification])
final class Timed(timer: Timer) extends SignatureVerification {
override def apply(payload: Array[Byte], signatureData: SignatureData): Try[Boolean] =
timer.time(() => super.apply(payload, signatureData))
}
}
sealed abstract class SignatureVerification {
import SignatureVerification.logger
def apply(payload: Array[Byte], signatureData: SignatureData): Try[Boolean] =
Try {
logger.trace("Decoding signature bytes from Base64-encoded signature")
logger.trace("Initializing signature verifier")
val verifier = Signature.getInstance(signatureData.algorithm)
verifier.initVerify(signatureData.key)
verifier.update(payload)
logger.trace("Verifying signature '{}'", signatureData.signature.base64)
verifier.verify(signatureData.signature.unsafeArray)
}
}

View File

@ -4,9 +4,10 @@
package com.daml.nonrepudiation package com.daml.nonrepudiation
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.security.{PublicKey, Signature} import java.security.PublicKey
import java.time.Clock import java.time.Clock
import com.codahale.metrics.Timer
import com.daml.grpc.interceptors.ForwardingServerCallListener import com.daml.grpc.interceptors.ForwardingServerCallListener
import io.grpc.Metadata.Key import io.grpc.Metadata.Key
import io.grpc._ import io.grpc._
@ -16,24 +17,35 @@ import scala.util.Try
final class SignatureVerificationInterceptor( final class SignatureVerificationInterceptor(
certificateRepository: CertificateRepository.Read, certificateRepository: CertificateRepository.Read,
signedPayloads: SignedPayloadRepository.Write, signedPayloadRepository: SignedPayloadRepository.Write,
timestampProvider: Clock, timestampProvider: Clock,
) extends ServerInterceptor { ) extends ServerInterceptor {
import SignatureVerificationInterceptor._ import SignatureVerificationInterceptor._
private val timedCertificateRepository =
new CertificateRepository.Timed(Metrics.getKeyTimer, certificateRepository)
private val timedSignedPayloadRepository =
new SignedPayloadRepository.Timed(Metrics.addSignedPayloadTimer, signedPayloadRepository)
private val timedSignatureVerification =
new SignatureVerification.Timed(Metrics.verifySignatureTimer)
override def interceptCall[ReqT, RespT]( override def interceptCall[ReqT, RespT](
call: ServerCall[ReqT, RespT], call: ServerCall[ReqT, RespT],
metadata: Metadata, metadata: Metadata,
next: ServerCallHandler[ReqT, RespT], next: ServerCallHandler[ReqT, RespT],
): ServerCall.Listener[ReqT] = { ): ServerCall.Listener[ReqT] = {
val runningTimer = Metrics.processingTimer.time()
val signatureData = val signatureData =
for { for {
signature <- getHeader(metadata, Headers.SIGNATURE, SignatureBytes.wrap) signature <- getHeader(metadata, Headers.SIGNATURE, SignatureBytes.wrap)
algorithm <- getHeader(metadata, Headers.ALGORITHM, AlgorithmString.wrap) algorithm <- getHeader(metadata, Headers.ALGORITHM, AlgorithmString.wrap)
fingerprint <- getHeader(metadata, Headers.FINGERPRINT, FingerprintBytes.wrap) fingerprint <- getHeader(metadata, Headers.FINGERPRINT, FingerprintBytes.wrap)
key <- getKey(certificateRepository, fingerprint) key <- getKey(timedCertificateRepository, fingerprint)
} yield SignatureData( } yield SignatureData(
signature = signature, signature = signature,
algorithm = algorithm, algorithm = algorithm,
@ -48,11 +60,13 @@ final class SignatureVerificationInterceptor(
metadata = metadata, metadata = metadata,
next = next, next = next,
signatureData = signatureData, signatureData = signatureData,
signedPayloads = signedPayloads, signatureVerification = timedSignatureVerification,
signedPayloads = timedSignedPayloadRepository,
timestampProvider = timestampProvider, timestampProvider = timestampProvider,
runningTimer = runningTimer,
) )
case Left(rejection) => case Left(rejection) =>
rejection.report() rejection.report(runningTimer)
call.close(SignatureVerificationFailed, new Metadata()) call.close(SignatureVerificationFailed, new Metadata())
new ServerCall.Listener[ReqT] {} new ServerCall.Listener[ReqT] {}
} }
@ -84,7 +98,9 @@ object SignatureVerificationInterceptor {
} }
private trait Rejection { private trait Rejection {
def report(): Unit = { def report(timer: Timer.Context): Unit = {
Metrics.rejectionsMeter.mark()
timer.stop()
this match { this match {
case Rejection.Error(reason) => case Rejection.Error(reason) =>
logger.debug(reason) logger.debug(reason)
@ -124,8 +140,10 @@ object SignatureVerificationInterceptor {
metadata: Metadata, metadata: Metadata,
next: ServerCallHandler[ReqT, RespT], next: ServerCallHandler[ReqT, RespT],
signatureData: SignatureData, signatureData: SignatureData,
signatureVerification: SignatureVerification,
signedPayloads: SignedPayloadRepository.Write, signedPayloads: SignedPayloadRepository.Write,
timestampProvider: Clock, timestampProvider: Clock,
runningTimer: Timer.Context,
) extends ForwardingServerCallListener(call, metadata, next) { ) extends ForwardingServerCallListener(call, metadata, next) {
private def castToByteArray(request: ReqT): Either[Rejection, Array[Byte]] = { private def castToByteArray(request: ReqT): Either[Rejection, Array[Byte]] = {
@ -134,15 +152,7 @@ object SignatureVerificationInterceptor {
} }
private def verifySignature(payload: Array[Byte]): Either[Rejection, Boolean] = private def verifySignature(payload: Array[Byte]): Either[Rejection, Boolean] =
Try { signatureVerification(payload, signatureData).toEither.left
logger.trace("Decoding signature bytes from Base64-encoded signature")
logger.trace("Initializing signature verifier")
val verifier = Signature.getInstance(signatureData.algorithm)
verifier.initVerify(signatureData.key)
verifier.update(payload)
logger.trace("Verifying signature '{}'", signatureData.signature.base64)
verifier.verify(signatureData.signature.unsafeArray)
}.toEither.left
.map(Rejection.fromException) .map(Rejection.fromException)
.filterOrElse(identity, Rejection.SignatureVerificationFailed) .filterOrElse(identity, Rejection.SignatureVerificationFailed)
@ -168,12 +178,13 @@ object SignatureVerificationInterceptor {
_ <- addSignedCommand(payload) _ <- addSignedCommand(payload)
} yield { } yield {
val input = new ByteArrayInputStream(payload) val input = new ByteArrayInputStream(payload)
val dup = call.getMethodDescriptor.parseRequest(input) val copy = call.getMethodDescriptor.parseRequest(input)
super.onMessage(dup) runningTimer.stop()
super.onMessage(copy)
} }
result.left.foreach { rejection => result.left.foreach { rejection =>
rejection.report() rejection.report(runningTimer)
call.close(SignatureVerificationFailed, new Metadata()) call.close(SignatureVerificationFailed, new Metadata())
} }
} }

View File

@ -3,6 +3,7 @@
package com.daml.nonrepudiation package com.daml.nonrepudiation
import com.codahale.metrics.Timer
import com.daml.nonrepudiation.SignedPayloadRepository.KeyEncoder import com.daml.nonrepudiation.SignedPayloadRepository.KeyEncoder
object SignedPayloadRepository { object SignedPayloadRepository {
@ -30,6 +31,11 @@ object SignedPayloadRepository {
def put(signedPayload: SignedPayload): Unit def put(signedPayload: SignedPayload): Unit
} }
final class Timed(timer: Timer, delegate: Write) extends Write {
override def put(signedPayload: SignedPayload): Unit =
timer.time[Unit](() => delegate.put(signedPayload))
}
} }
abstract class SignedPayloadRepository[Key](implicit val keyEncoder: KeyEncoder[Key]) abstract class SignedPayloadRepository[Key](implicit val keyEncoder: KeyEncoder[Key])

View File

@ -127,7 +127,14 @@ final class NonRepudiationProxySpec
val (privateKey, certificate) = Setup.generateKeyAndCertificate() val (privateKey, certificate) = Setup.generateKeyAndCertificate()
NonRepudiationProxy NonRepudiationProxy
.owner(channel, proxyBuilder, certificates, signatures, Clock.systemUTC(), Health.Name) .owner(
channel,
proxyBuilder,
certificates,
signatures,
Clock.systemUTC(),
Health.Name,
)
.use { _ => .use { _ =>
the[StatusRuntimeException] thrownBy { the[StatusRuntimeException] thrownBy {
Health.getHealthStatus( Health.getHealthStatus(