Skip to content

Adding new MetricRepository to read and write data via REST#445

Open
cmachgodaddy wants to merge 7 commits into
awslabs:masterfrom
gdcorp-dna:master
Open

Adding new MetricRepository to read and write data via REST#445
cmachgodaddy wants to merge 7 commits into
awslabs:masterfrom
gdcorp-dna:master

Conversation

@cmachgodaddy

Copy link
Copy Markdown

Issue #, if available:

Description of changes:

Adding new MetricRepository to read and write data via REST

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@cmachgodaddy

Copy link
Copy Markdown
Author

@shehzad-qureshi @eycho-am @sseb @sseb Can you review this PR?

@rdsharma26

Copy link
Copy Markdown
Contributor

Thank you for the pull request. Can you please share more details about your use case? Can you also attach any testing evidence that showcases how the changes work?

@rdsharma26

Copy link
Copy Markdown
Contributor

@cmachgodaddy Can you provide an update on my comment above?

@cmachgodaddy

Copy link
Copy Markdown
Author

Hi @rdsharma26 , sorry for the late response. My use case was as bellow:
I have a Dynamo DB table, which has a metric table, which contains all of the metric values output from the Deeque so far. And those metrics written by our API to the Dynamo. So, I added this repo as REST, instead of a direct Dynamo ingression.

Another reason it has to be API is our existing metric has its own schema which is not compatible with the schema that Deequ's Anomaly Detection expected. So providing this API as a abstraction layer has the flexibility to convert the schema from one format to another.

I have used this REST repo for a while now, and in production, and it works quite well. Here is how I used it

    val ENDPOINT = apiHelper.get.dlmsURL
    val PATH = s"/v1/accounts/${getAccountId()}/databases/${config.database}/tables/${config.table}/xxx"
    val headers = Map(
      "Content-Type" -> "application/json",
      "Authorization" -> s"sso-jwt xxx"
    )

    val writeRequest = new DefaultRequest[Void]("execute-api")
    writeRequest.setHttpMethod(HttpMethodName.POST)
    writeRequest.setEndpoint(URI.create(ENDPOINT))
    writeRequest.setResourcePath(PATH)
    writeRequest.setHeaders(headers.asJava)

    val readRequest = new DefaultRequest[Void]("execute-api")
    readRequest.setHttpMethod(HttpMethodName.GET)
    readRequest.setEndpoint(URI.create(ENDPOINT))
    readRequest.setResourcePath(PATH)
    readRequest.setHeaders(headers.asJava)
    val repo = new RestMetricsRepository(readRequest, writeRequest)

And, here is how I run the Anomaly Detection job, and send data to Dynamo

      val verificationResult = VerificationSuite()
        .onData(dataDF)
        .useRepository(repo)
        .saveOrAppendResult(todaysKey)
        .addAnomalyCheck(strategyObj, metricObj)
        .run()

These should be in the PR, btw.

I can't snapshot the results in my Dynamo, since it contains some sensitive data. But, the changes in this PR should be very straightforward? it based off the existing patterns of the FileSystem and the InMemory repos.

Let me know if you need anything else to get this PR in?

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Generated by AI (model: us.anthropic.claude-opus-4-6-v1, prompt: 38d3f74c) — may not be fully accurate. Reply if this doesn't help.

Comment thread pom.xml


<dependency>
<groupId>com.amazonaws</groupId>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aws-java-sdk-core dependency should have <scope>provided</scope> or <scope>compile</scope> with an <optional>true</optional> tag. Adding it as a hard compile dependency forces all Deequ users to pull in the AWS SDK even if they don't use RestMetricsRepository. This is inconsistent with how other optional integrations (e.g., Iceberg) are handled in this project.

import com.amazon.deequ.analyzers.runners.AnalyzerContext
import com.amazon.deequ.metrics.Metric
import com.amazon.deequ.repository._
import com.amazonaws.http.{AmazonHttpClient, DefaultErrorResponseHandler,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several imports reference classes that don't exist in aws-java-sdk-core version 1.12.128 or use incorrect package paths:

  • com.amazonaws.http.AmazonHttpClient — this is an internal/private class not part of the public SDK API and may not be available depending on the SDK version.
  • com.amazonaws.http.DefaultErrorResponseHandler — same issue.
  • com.amazonaws.http.ExecutionContext — same issue.
  • com.google.common.collect.ImmutableList — Guava is a transitive dependency of Spark but relying on it directly is fragile.

Using internal AWS SDK classes makes this code brittle and likely to break across SDK versions. Consider using a standard HTTP client (e.g., java.net.HttpURLConnection, Apache HttpClient, or the AWS SDK's high-level AmazonApiGateway client) instead.


writeRequest.setContent(new ByteArrayInputStream(serializedResult.getBytes("UTF-8")))

apiHelper.writeHttpRequest(writeRequest)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The writeRequest.setContent(...) call mutates the writeRequest object that was passed into the constructor. If save is called multiple times, the content from the previous call is overwritten. More critically, this only serializes the current AnalysisResult — it does not merge with previously saved results. Unlike FileSystemMetricsRepository which reads existing data and appends, this implementation will lose previously saved results for different ResultKeys unless the backing API handles accumulation. This is a semantic difference from other MetricsRepository implementations that could surprise users.

* Get a AnalyzerContext saved using exactly the same resultKey if present
*/
override def loadByKey(resultKey: ResultKey): Option[AnalyzerContext] = {
load().get().find(_.resultKey == resultKey).map(_.analyzerContext)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadByKey calls load().get() which makes a full HTTP GET request and deserializes all results, then filters client-side. This is potentially very expensive for large metric histories. At minimum, document this behavior.

import java.time.{LocalDate, ZoneOffset}
import java.util.concurrent.ConcurrentHashMap
import scala.util.{Failure, Success}
import scala.collection.JavaConverters._

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deprecated import scala.collection.JavaConversions._ should be replaced with scala.collection.JavaConverters._ (or scala.jdk.CollectionConverters._ for Scala 2.13+). JavaConversions provides implicit conversions that can cause subtle bugs and is deprecated.

private val mapRepo = new ConcurrentHashMap[ResultKey, AnalysisResult]()

override def writeHttpRequest(writeRequest: Request[Void]): Unit = {
val contentString = Option(IOUtils.toString(writeRequest.getContent, "UTF-8"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock writeHttpRequest stores each AnalysisResult individually by ResultKey, but readHttpRequest returns all stored results. This means the test's save method only persists a single AnalysisResult per call (the serialized JSON from AnalysisResultSerde.serialize wraps it in a Seq of one element). This matches the production code's behavior but confirms the concern: the production save method does NOT accumulate results — each save overwrites the endpoint with only the latest result. The tests pass because the mock accumulates in a ConcurrentHashMap, but the real HTTP endpoint would receive only one result per POST. This is a fundamental design issue that should be addressed or clearly documented.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

This PR has been inactive for 60 days. It will be closed in 14 days if there is no further activity. If you are still working on this, please push an update or comment to keep it open.

@github-actions github-actions Bot added the stale label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants