getTestAuditLogs() {
+ GetOnlineFeaturesRequestV2 requestSpec =
+ GetOnlineFeaturesRequestV2.newBuilder()
+ .addAllFeatures(
+ Arrays.asList(
+ FeatureReferenceV2.newBuilder()
+ .setFeatureTable("featuretable_1")
+ .setName("feature1")
+ .build(),
+ FeatureReferenceV2.newBuilder()
+ .setFeatureTable("featuretable_1")
+ .setName("feature2")
+ .build()))
+ .build();
+
+ GetOnlineFeaturesResponse responseSpec =
+ GetOnlineFeaturesResponse.newBuilder()
+ .addAllFieldValues(
+ Arrays.asList(
+ FieldValues.newBuilder()
+ .putFields(
+ "featuretable_1:feature_1", Value.newBuilder().setInt32Val(32).build())
+ .build(),
+ FieldValues.newBuilder()
+ .putFields(
+ "featuretable_1:feature2", Value.newBuilder().setInt32Val(64).build())
+ .build()))
+ .build();
+
+ return Arrays.asList(
+ MessageAuditLogEntry.newBuilder()
+ .setComponent("feast-serving")
+ .setVersion("0.9")
+ .setService("ServingService")
+ .setMethod("getOnlineFeatures")
+ .setRequest(requestSpec)
+ .setResponse(responseSpec)
+ .setStatusCode(Status.OK.getCode())
+ .setIdentity("adam@no.such.email")
+ .build(),
+ ActionAuditLogEntry.of(
+ "core", "0.9", LogResource.of(ResourceType.JOB, "kafka-to-redis"), "CREATE"),
+ TransitionAuditLogEntry.of(
+ "core", "0.9", LogResource.of(ResourceType.FEATURE_TABLE, "featuretable_1"), "READY"));
+ }
+
+ @Test
+ public void shouldReturnJSONRepresentationOfAuditLog() {
+ for (AuditLogEntry auditLog : getTestAuditLogs()) {
+ // Check that auditLog's toJSON() returns valid JSON
+ String logJSON = auditLog.toJSON();
+ System.out.println(logJSON);
+ JsonParser parser = new JsonParser();
+
+ // check basic fields are present in JSON representation.
+ JsonObject logObject = parser.parse(logJSON).getAsJsonObject();
+ assertThat(logObject.getAsJsonPrimitive("logType").getAsString(), equalTo(auditLog.logType));
+ assertThat(
+ logObject.getAsJsonPrimitive("kind").getAsString(), equalTo(auditLog.getKind().name()));
+ }
+ }
+}
diff --git a/java/common/src/test/java/feast/common/models/FeaturesTest.java b/java/common/src/test/java/feast/common/models/FeaturesTest.java
new file mode 100644
index 00000000000..180f7e4e697
--- /dev/null
+++ b/java/common/src/test/java/feast/common/models/FeaturesTest.java
@@ -0,0 +1,46 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2020 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.common.models;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsEqual.equalTo;
+
+import feast.proto.serving.ServingAPIProto.FeatureReferenceV2;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FeaturesTest {
+
+ private FeatureReferenceV2 featureReference;
+
+ @Before
+ public void setUp() {
+ featureReference =
+ FeatureReferenceV2.newBuilder()
+ .setFeatureTable("featuretable_1")
+ .setName("feature1")
+ .build();
+ }
+
+ @Test
+ public void shouldReturnFeatureStringRef() {
+ String actualFeatureStringRef = FeatureV2.getFeatureStringRef(featureReference);
+ String expectedFeatureStringRef = "featuretable_1:feature1";
+
+ assertThat(actualFeatureStringRef, equalTo(expectedFeatureStringRef));
+ }
+}
diff --git a/java/datatypes/java/README.md b/java/datatypes/java/README.md
new file mode 100644
index 00000000000..7fc355f7739
--- /dev/null
+++ b/java/datatypes/java/README.md
@@ -0,0 +1,55 @@
+Feast Data Types for Java
+=========================
+
+This module produces Java class files for Feast's data type and gRPC service
+definitions, from Protobuf IDL. These are used across Feast components for wire
+interchange, contracts, etc.
+
+End users of Feast will be best served by our Java SDK which adds higher-level
+conveniences, but the data types are published independently for custom needs,
+without any additional dependencies the SDK may add.
+
+Dependency Coordinates
+----------------------
+
+```xml
+
+ dev.feast
+ datatypes-java
+ 0.26.2
+
+```
+
+Use the version corresponding to the Feast release you have deployed in your
+environment—see the [Feast release notes] for details.
+
+[Feast release notes]: ../../CHANGELOG.md
+
+Using the `.proto` Definitions
+------------------------------
+
+The `.proto` definitions are packaged as resources within the Maven artifact,
+which may be useful to `include` them in dependent Protobuf definitions in a
+downstream project, or for other JVM languages to consume from their builds to
+generate more idiomatic bindings.
+
+Google's Gradle plugin, for instance, [can use protos in dependencies][Gradle]
+either for `include` or to compile with a different `protoc` plugin than Java.
+
+[sbt-protoc] offers similar functionality for sbt/Scala.
+
+[Gradle]: https://github.com/google/protobuf-gradle-plugin#protos-in-dependencies
+[sbt-protoc]: https://github.com/thesamet/sbt-protoc
+
+Releases
+--------
+
+The module is published to Maven Central upon each release of Feast (since
+v0.3.7).
+
+For developers, the publishing process is automated along with the Java SDK by
+[the `publish-java-sdk` build task in Prow][prow task], where you can see how
+it works. Artifacts are staged to Sonatype where a maintainer needs to take a
+release action for them to go live on Maven Central.
+
+[prow task]: https://github.com/feast-dev/feast/blob/17e7dca8238aae4dcbf0ff9f0db5d80ef8e035cf/.prow/config.yaml#L166-L192
diff --git a/java/datatypes/java/pom.xml b/java/datatypes/java/pom.xml
new file mode 100644
index 00000000000..0d165f0cd53
--- /dev/null
+++ b/java/datatypes/java/pom.xml
@@ -0,0 +1,111 @@
+
+
+
+ 4.0.0
+
+ Feast Data Types for Java
+
+ Data types and service contracts used throughout Feast components and
+ their interchanges. These are generated from Protocol Buffers and gRPC
+ definitions included in the package.
+
+ datatypes-java
+
+
+ dev.feast
+ feast-parent
+ ${revision}
+ ../..
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
+ javax.annotation
+
+
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+
+ true
+
+ com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
+
+ grpc-java
+
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
+
+
+
+
+
+ compile
+ compile-custom
+ test-compile
+
+
+
+
+
+
+
+
+
+
+ com.google.guava
+ guava
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+
+ io.grpc
+ grpc-core
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-services
+
+
+ io.grpc
+ grpc-stub
+
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+
+
diff --git a/java/datatypes/java/src/main/proto/feast b/java/datatypes/java/src/main/proto/feast
new file mode 120000
index 00000000000..53364e5f45f
--- /dev/null
+++ b/java/datatypes/java/src/main/proto/feast
@@ -0,0 +1 @@
+../../../../../../protos/feast
\ No newline at end of file
diff --git a/java/datatypes/java/src/main/proto/tensorflow_metadata b/java/datatypes/java/src/main/proto/tensorflow_metadata
new file mode 120000
index 00000000000..225345970c2
--- /dev/null
+++ b/java/datatypes/java/src/main/proto/tensorflow_metadata
@@ -0,0 +1 @@
+../../../../../../protos/tensorflow_metadata
\ No newline at end of file
diff --git a/java/docs/coverage/java/pom.xml b/java/docs/coverage/java/pom.xml
new file mode 100644
index 00000000000..5f794224969
--- /dev/null
+++ b/java/docs/coverage/java/pom.xml
@@ -0,0 +1,87 @@
+
+
+
+ 4.0.0
+
+
+
+
+ dev.feast
+ feast-parent
+ ${revision}
+ ../../..
+
+
+ Feast Coverage Java
+ feast-coverage
+
+
+ true
+
+
+
+
+ dev.feast
+ feast-storage-api
+ ${project.version}
+
+
+
+ dev.feast
+ feast-storage-connector-redis
+ ${project.version}
+
+
+
+ dev.feast
+ feast-serving
+ ${project.version}
+
+
+
+ dev.feast
+ feast-sdk
+ ${project.version}
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ report-aggregate
+ prepare-package
+
+ report-aggregate
+
+
+
+
+
+
+
+
diff --git a/java/infra/docker/serving/Dockerfile b/java/infra/docker/serving/Dockerfile
new file mode 100644
index 00000000000..0aacf856305
--- /dev/null
+++ b/java/infra/docker/serving/Dockerfile
@@ -0,0 +1,53 @@
+# ============================================================
+# Build stage 1: Builder
+# ============================================================
+
+FROM maven:3.6-jdk-11 as builder
+
+WORKDIR /build
+
+COPY pom.xml .
+COPY datatypes/java/pom.xml datatypes/java/pom.xml
+COPY common/pom.xml common/pom.xml
+COPY core/pom.xml core/pom.xml
+COPY serving/pom.xml serving/pom.xml
+COPY storage/api/pom.xml storage/api/pom.xml
+COPY storage/connectors/pom.xml storage/connectors/pom.xml
+COPY storage/connectors/redis/pom.xml storage/connectors/redis/pom.xml
+COPY sdk/java/pom.xml sdk/java/pom.xml
+COPY docs/coverage/java/pom.xml docs/coverage/java/pom.xml
+COPY deps/feast/protos/ deps/feast/protos/
+
+# Setting Maven repository .m2 directory relative to /build folder gives the
+# user to optionally use cached repository when building the image by copying
+# the existing .m2 directory to $FEAST_REPO_ROOT/.m2
+ENV MAVEN_OPTS="-Dmaven.repo.local=/build/.m2/repository -DdependencyLocationsEnabled=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=25 -Dmaven.wagon.http.retryHandler.count=3"
+COPY pom.xml .m2/* .m2/
+RUN mvn dependency:go-offline -DexcludeGroupIds:dev.feast 2>/dev/null || true
+
+COPY . .
+
+ARG VERSION=dev
+RUN mvn --also-make --projects serving -Drevision=$VERSION \
+ -DskipUTs=true --batch-mode clean package
+#
+# Download grpc_health_probe to run health check for Feast Serving
+# https://kubernetes.io/blog/2018/10/01/health-checking-grpc-servers-on-kubernetes/
+#
+RUN wget -q https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.1/grpc_health_probe-linux-amd64 \
+ -O /usr/bin/grpc-health-probe && \
+ chmod +x /usr/bin/grpc-health-probe
+
+# ============================================================
+# Build stage 2: Production
+# ============================================================
+
+FROM amazoncorretto:11 as production
+ARG VERSION=dev
+COPY --from=builder /build/serving/target/feast-serving-$VERSION-exec.jar /opt/feast/feast-serving.jar
+COPY --from=builder /usr/bin/grpc-health-probe /usr/bin/grpc-health-probe
+CMD ["java",\
+ "-Xms1g",\
+ "-Xmx4g",\
+ "-jar",\
+ "/opt/feast/feast-serving.jar"]
diff --git a/java/infra/docker/serving/Dockerfile.dev b/java/infra/docker/serving/Dockerfile.dev
new file mode 100644
index 00000000000..93bbbbb7185
--- /dev/null
+++ b/java/infra/docker/serving/Dockerfile.dev
@@ -0,0 +1,15 @@
+FROM openjdk:11-jre as production
+ARG REVISION=dev
+#
+# Download grpc_health_probe to run health check for Feast Serving
+# https://kubernetes.io/blog/2018/10/01/health-checking-grpc-servers-on-kubernetes/
+#
+RUN wget -q https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/v0.3.1/grpc_health_probe-linux-amd64 \
+ -O /usr/bin/grpc-health-probe && \
+ chmod +x /usr/bin/grpc-health-probe
+ADD $PWD/serving/target/feast-serving-$REVISION-exec.jar /opt/feast/feast-serving.jar
+CMD ["java",\
+ "-Xms1024m",\
+ "-Xmx1024m",\
+ "-jar",\
+ "/opt/feast/feast-serving.jar"]
diff --git a/java/pom.xml b/java/pom.xml
new file mode 100644
index 00000000000..073616b69a4
--- /dev/null
+++ b/java/pom.xml
@@ -0,0 +1,855 @@
+
+
+
+ 4.0.0
+
+ Feast
+ Feature Store for Machine Learning
+ ${github.url}
+
+ dev.feast
+ feast-parent
+ ${revision}
+ pom
+
+
+ datatypes/java
+ storage/api
+ storage/connectors
+ serving
+ sdk/java
+ docs/coverage/java
+ common
+
+
+
+ 0.26.2
+ https://github.com/feast-dev/feast
+
+ UTF-8
+ UTF-8
+
+ 1.30.2
+ 3.12.2
+ 3.12.2
+ 2.3.1.RELEASE
+ 5.2.7.RELEASE
+ 5.3.0.RELEASE
+ 2.9.0.RELEASE
+ 1.111.1
+ 0.8.0
+ 1.9.10
+ 1.3
+ 2.28.2
+
+ 0.26.0
+
+ 2.13.2
+ 2.9.9
+ 2.0.2
+ 2.5.0.RELEASE
+ 1.18.12
+ 1.8.4
+ 2.8.6
+ 1.5.24
+ 3.14.7
+ 3.10
+ 2.3.1
+ 1.3.2
+ 2.0.1.Final
+ 2.8.0
+ 0.20.0
+ 1.6.6
+
+
+
+ ${maven.multiModuleProjectDirectory}
+
+ false
+ false
+ feast.common.auth.providers.http.client
+
+
+
+ Gojek
+ https://www.gojek.com
+
+
+
+
+ Feast Authors
+ ${github.url}
+ Gojek
+ https://www.gojek.com
+
+
+
+
+
+ Apache License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ ${github.url}
+ scm:git:${github.url}.git
+ scm:git:git@github.com:feast-dev/feast.git
+ HEAD
+
+
+
+ GitHub Issues
+ ${github.url}/issues
+
+
+
+
+
+ ossrh
+ https://oss.sonatype.org/content/repositories/snapshots
+
+
+ ossrh
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons.lang3.version}
+
+
+
+
+ com.google.cloud
+ google-cloud-bigquery
+ ${com.google.cloud.version}
+
+
+ com.google.cloud
+ google-cloud-storage
+ ${com.google.cloud.version}
+
+
+
+
+ com.google.cloud
+ google-cloud-nio
+ 0.83.0-alpha
+
+
+
+ io.opencensus
+ opencensus-api
+ ${opencensus.version}
+
+
+ io.opencensus
+ opencensus-contrib-grpc-util
+ ${opencensus.version}
+
+
+ io.opencensus
+ opencensus-contrib-http-util
+ ${opencensus.version}
+
+
+
+
+ io.grpc
+ grpc-core
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-api
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-context
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-all
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-okhttp
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-auth
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-grpclb
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-alts
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-netty
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-netty-shaded
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-protobuf
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-services
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-stub
+ ${grpc.version}
+
+
+ io.grpc
+ grpc-testing
+ ${grpc.version}
+ test
+
+
+
+
+ org.apache.arrow
+ arrow-java-root
+ 5.0.0
+ pom
+
+
+
+
+ org.apache.arrow
+ arrow-vector
+ 5.0.0
+
+
+
+
+ org.apache.arrow
+ arrow-memory
+ 5.0.0
+ pom
+
+
+
+
+ org.apache.arrow
+ arrow-memory-netty
+ 5.0.0
+ runtime
+
+
+
+
+ net.devh
+ grpc-server-spring-boot-starter
+ ${grpc.spring.boot.starter.version}
+
+
+
+
+ io.prometheus
+ simpleclient
+ ${io.prometheus.version}
+
+
+ io.prometheus
+ simpleclient_servlet
+ ${io.prometheus.version}
+
+
+
+
+ org.springframework.security
+ spring-security-core
+ ${spring.security.version}
+
+
+ org.springframework.security
+ spring-security-config
+ ${spring.security.version}
+
+
+ org.springframework.security
+ spring-security-oauth2-resource-server
+ ${spring.security.version}
+
+
+ org.springframework.security
+ spring-security-oauth2-jose
+ ${spring.security.version}
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ ${google.auth.library.oauth2.http.version}
+
+
+
+
+ joda-time
+ joda-time
+ ${joda.time.version}
+
+
+ com.datadoghq
+ java-dogstatsd-client
+ 2.6.1
+
+
+ com.google.guava
+ guava
+ 29.0-jre
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobuf.version}
+
+
+ com.google.protobuf
+ protobuf-java-util
+ ${protobuf.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ provided
+
+
+ com.google.auto.value
+ auto-value-annotations
+ ${auto.value.version}
+
+
+ com.google.auto.value
+ auto-value
+ ${auto.value.version}
+
+
+ com.google.code.gson
+ gson
+ ${gson.version}
+
+
+ io.gsonfire
+ gson-fire
+ ${gson.fire.version}
+
+
+
+ com.github.kstyrc
+ embedded-redis
+ 0.6
+ test
+
+
+
+
+
+ net.bytebuddy
+ byte-buddy
+ ${byte-buddy.version}
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ ${spring.boot.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-logging
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ ${log4jVersion}
+
+
+ org.apache.logging.log4j
+ log4j-core
+ ${log4jVersion}
+
+
+ org.apache.logging.log4j
+ log4j-jul
+ ${log4jVersion}
+
+
+ org.apache.logging.log4j
+ log4j-web
+ ${log4jVersion}
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ ${log4jVersion}
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring.boot.version}
+ pom
+ import
+
+
+ com.squareup.okio
+ okio
+ 1.17.2
+
+
+ javax.xml.bind
+ jaxb-api
+ ${javax.xml.bind.version}
+
+
+ javax.annotation
+ javax.annotation-api
+ ${javax.annotation.version}
+
+
+ javax.validation
+ validation-api
+ ${javax.validation.version}
+
+
+
+
+
+
+
+ kr.motd.maven
+ os-maven-plugin
+ 1.6.2
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+ feast.proto.*:io.grpc.*:org.tensorflow.*
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ 1.26.1
+
+
+
+ ${license.content}
+
+
+ 1.7
+
+
+
+ src/main/java/**/BatchLoadsWithResult.java
+
+
+
+
+
+ ${license.content}
+
+
+
+
+
+
+ spotless-check
+ process-test-classes
+
+ check
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ 8
+
+
+ com.google.auto.value
+ auto-value
+ ${auto.value.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.0.0-M2
+
+
+ org.codehaus.mojo
+ extra-enforcer-rules
+ 1.2
+
+
+
+
+ valid-build-environment
+
+ enforce
+
+
+
+
+ [3.6,4.0)
+
+
+ [11.0,)
+
+
+
+
+
+
+ consistent-dependency-versions
+
+ enforce
+
+
+
+
+
+
+
+
+ no-snapshot-deps-at-release
+
+ enforce
+
+
+
+
+ true
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+ @{argLine} -Xms2048m -Xmx2048m -Djdk.net.URLClassPath.disableClassPathURLCheck=true
+ ${skipUTs}
+
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ 5.5.2
+
+
+
+
+ maven-failsafe-plugin
+ 3.0.0-M5
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.6.2
+
+
+
+
+ integration-tests
+
+ integration-test
+ verify
+
+
+
+
+
+ ${project.groupId}:${project.artifactId}
+
+
+ ${project.build.outputDirectory}
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ true
+
+
+
+ build-info
+
+ build-info
+
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.6.8
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+
+ true
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.1.0
+
+ oss
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+ flatten.clean
+ clean
+
+ clean
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+ ${gpg.passphrase}
+
+
+
+
+
+
+
+
+
+ io.fabric8
+ docker-maven-plugin
+ 0.20.1
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+
+ -Xlint:all
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.1.1
+
+
+
+ org.apache.maven.shared
+ maven-dependency-analyzer
+ 1.11.1
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.1.1
+
+ all
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 1.6.0
+
+ false
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.5
+
+
+
+ prepare-agent
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+
+ repackage
+
+
+
+
+ exec
+
+
+
+ org.xolstice.maven.plugins
+ protobuf-maven-plugin
+ 0.6.1
+
+
+
+
+
diff --git a/java/sdk/java/pom.xml b/java/sdk/java/pom.xml
new file mode 100644
index 00000000000..6b2295b6b33
--- /dev/null
+++ b/java/sdk/java/pom.xml
@@ -0,0 +1,143 @@
+
+
+ 4.0.0
+
+ Feast SDK for Java
+ SDK for registering, storing, and retrieving features
+ feast-sdk
+
+
+ dev.feast
+ feast-parent
+ ${revision}
+ ../..
+
+
+
+
+ 5.5.2
+ 2.28.2
+ 0.33.0
+
+
+
+
+ dev.feast
+ datatypes-java
+ ${project.version}
+
+
+
+ dev.feast
+ feast-common
+ ${project.version}
+
+
+
+
+ io.grpc
+ grpc-netty-shaded
+
+
+ io.grpc
+ grpc-protobuf
+
+
+ io.grpc
+ grpc-stub
+
+
+ io.grpc
+ grpc-testing
+
+
+ com.google.protobuf
+ protobuf-java-util
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+
+
+ io.opentracing.contrib
+ opentracing-grpc
+ 0.2.3
+
+
+ io.opentracing
+ opentracing-api
+ ${opentracing.version}
+
+
+ io.opentracing
+ opentracing-noop
+ ${opentracing.version}
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit.version}
+ test
+
+
+ org.apache.commons
+ commons-lang3
+ 3.6
+ compile
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+ org.mockito
+ mockito-inline
+ ${mockito.version}
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+ true
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
+
diff --git a/java/sdk/java/src/main/java/com/gojek/feast/FeastClient.java b/java/sdk/java/src/main/java/com/gojek/feast/FeastClient.java
new file mode 100644
index 00000000000..94836d88aa4
--- /dev/null
+++ b/java/sdk/java/src/main/java/com/gojek/feast/FeastClient.java
@@ -0,0 +1,211 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 com.gojek.feast;
+
+import feast.proto.serving.ServingAPIProto.FeatureReferenceV2;
+import feast.proto.serving.ServingAPIProto.GetFeastServingInfoRequest;
+import feast.proto.serving.ServingAPIProto.GetFeastServingInfoResponse;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse;
+import feast.proto.serving.ServingServiceGrpc;
+import feast.proto.serving.ServingServiceGrpc.ServingServiceBlockingStub;
+import io.grpc.CallCredentials;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
+import io.opentracing.contrib.grpc.TracingClientInterceptor;
+import io.opentracing.util.GlobalTracer;
+import java.io.File;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import javax.net.ssl.SSLException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressWarnings("WeakerAccess")
+public class FeastClient implements AutoCloseable {
+ Logger logger = LoggerFactory.getLogger(FeastClient.class);
+
+ private static final int CHANNEL_SHUTDOWN_TIMEOUT_SEC = 5;
+
+ private final ManagedChannel channel;
+ private final ServingServiceBlockingStub stub;
+
+ /**
+ * Create a client to access Feast Serving.
+ *
+ * @param host hostname or ip address of Feast serving GRPC server
+ * @param port port number of Feast serving GRPC server
+ * @return {@link FeastClient}
+ */
+ public static FeastClient create(String host, int port) {
+ // configure client with no security config.
+ return FeastClient.createSecure(host, port, SecurityConfig.newBuilder().build());
+ }
+
+ /**
+ * Create a authenticated client that can access Feast serving with authentication enabled.
+ * Supports the {@link CallCredentials} in the {@link feast.common.auth.credentials} package.
+ *
+ * @param host hostname or ip address of Feast serving GRPC server
+ * @param port port number of Feast serving GRPC server
+ * @param securityConfig security options to configure the Feast client. See {@link
+ * SecurityConfig} for options.
+ * @return {@link FeastClient}
+ */
+ public static FeastClient createSecure(String host, int port, SecurityConfig securityConfig) {
+ // Configure client TLS
+ ManagedChannel channel = null;
+ if (securityConfig.isTLSEnabled()) {
+ if (securityConfig.getCertificatePath().isPresent()) {
+ String certificatePath = securityConfig.getCertificatePath().get();
+ // Use custom certificate for TLS
+ File certificateFile = new File(certificatePath);
+ try {
+ channel =
+ NettyChannelBuilder.forAddress(host, port)
+ .useTransportSecurity()
+ .sslContext(GrpcSslContexts.forClient().trustManager(certificateFile).build())
+ .build();
+ } catch (SSLException e) {
+ throw new IllegalArgumentException(
+ String.format("Invalid Certificate provided at path: %s", certificatePath), e);
+ }
+ } else {
+ // Use system certificates for TLS
+ channel = ManagedChannelBuilder.forAddress(host, port).useTransportSecurity().build();
+ }
+ } else {
+ // Disable TLS
+ channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
+ }
+
+ return new FeastClient(channel, securityConfig.getCredentials());
+ }
+
+ /**
+ * Obtain info about Feast Serving.
+ *
+ * @return {@link GetFeastServingInfoResponse} containing Feast version, Serving type etc.
+ */
+ public GetFeastServingInfoResponse getFeastServingInfo() {
+ return stub.getFeastServingInfo(GetFeastServingInfoRequest.newBuilder().build());
+ }
+
+ /**
+ * Get online features from Feast, without indicating project, will use `default`.
+ *
+ * See {@link #getOnlineFeatures(List, List, String)}
+ *
+ * @param featureRefs list of string feature references to retrieve in the following format
+ * featureTable:feature, where 'featureTable' and 'feature' refer to the FeatureTable and
+ * Feature names respectively. Only the Feature name is required.
+ * @param rows list of {@link Row} to select the entities to retrieve the features for.
+ * @return list of {@link Row} containing retrieved data fields.
+ */
+ public List getOnlineFeatures(List featureRefs, List rows) {
+ return getOnlineFeatures(featureRefs, rows, "");
+ }
+
+ /**
+ * Get online features from Feast.
+ *
+ * Example of retrieving online features for the driver FeatureTable, with features driver_id
+ * and driver_name
+ *
+ *
{@code
+ * FeastClient client = FeastClient.create("localhost", 6566);
+ * List requestedFeatureIds = Arrays.asList("driver:driver_id", "driver:driver_name");
+ * List requestedRows =
+ * Arrays.asList(Row.create().set("driver_id", 123), Row.create().set("driver_id", 456));
+ * List retrievedFeatures = client.getOnlineFeatures(requestedFeatureIds, requestedRows);
+ * retrievedFeatures.forEach(System.out::println);
+ * }
+ *
+ * @param featureRefs list of string feature references to retrieve in the following format
+ * featureTable:feature, where 'featureTable' and 'feature' refer to the FeatureTable and
+ * Feature names respectively. Only the Feature name is required.
+ * @param rows list of {@link Row} to select the entities to retrieve the features for
+ * @param project {@link String} Specifies the project override. If specified uses the project for
+ * retrieval. Overrides the projects set in Feature References if also specified.
+ * @return list of {@link Row} containing retrieved data fields.
+ */
+ public List getOnlineFeatures(List featureRefs, List rows, String project) {
+ List features = RequestUtil.createFeatureRefs(featureRefs);
+ // build entity rows and collect entity references
+ HashSet entityRefs = new HashSet<>();
+ List entityRows =
+ rows.stream()
+ .map(
+ row -> {
+ entityRefs.addAll(row.getFields().keySet());
+ return EntityRow.newBuilder()
+ .setTimestamp(row.getEntityTimestamp())
+ .putAllFields(row.getFields())
+ .build();
+ })
+ .collect(Collectors.toList());
+
+ GetOnlineFeaturesResponse response =
+ stub.getOnlineFeaturesV2(
+ GetOnlineFeaturesRequestV2.newBuilder()
+ .addAllFeatures(features)
+ .addAllEntityRows(entityRows)
+ .setProject(project)
+ .build());
+
+ return response.getFieldValuesList().stream()
+ .map(
+ fieldValues -> {
+ Row row = Row.create();
+ for (String fieldName : fieldValues.getFieldsMap().keySet()) {
+ row.set(
+ fieldName,
+ fieldValues.getFieldsMap().get(fieldName),
+ fieldValues.getStatusesMap().get(fieldName));
+ }
+ return row;
+ })
+ .collect(Collectors.toList());
+ }
+
+ protected FeastClient(ManagedChannel channel, Optional credentials) {
+ this.channel = channel;
+ TracingClientInterceptor tracingInterceptor =
+ TracingClientInterceptor.newBuilder().withTracer(GlobalTracer.get()).build();
+
+ ServingServiceBlockingStub servingStub =
+ ServingServiceGrpc.newBlockingStub(tracingInterceptor.intercept(channel));
+
+ if (credentials.isPresent()) {
+ servingStub = servingStub.withCallCredentials(credentials.get());
+ }
+
+ this.stub = servingStub;
+ }
+
+ public void close() throws Exception {
+ if (channel != null) {
+ channel.shutdown().awaitTermination(CHANNEL_SHUTDOWN_TIMEOUT_SEC, TimeUnit.SECONDS);
+ }
+ }
+}
diff --git a/java/sdk/java/src/main/java/com/gojek/feast/RequestUtil.java b/java/sdk/java/src/main/java/com/gojek/feast/RequestUtil.java
new file mode 100644
index 00000000000..69c8f9f737a
--- /dev/null
+++ b/java/sdk/java/src/main/java/com/gojek/feast/RequestUtil.java
@@ -0,0 +1,80 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 com.gojek.feast;
+
+import feast.proto.serving.ServingAPIProto.FeatureReferenceV2;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@SuppressWarnings("WeakerAccess")
+public class RequestUtil {
+
+ /**
+ * Create feature references protos from given string feature reference.
+ *
+ * @param featureRefStrings to create Feature Reference protos from
+ * @return List of parsed {@link FeatureReferenceV2} protos
+ */
+ public static List createFeatureRefs(List featureRefStrings) {
+ if (featureRefStrings == null) {
+ throw new IllegalArgumentException("FeatureReferences cannot be null");
+ }
+
+ List featureRefs =
+ featureRefStrings.stream()
+ .map(refStr -> parseFeatureRef(refStr))
+ .collect(Collectors.toList());
+
+ return featureRefs;
+ }
+
+ /**
+ * Parse a feature reference proto builder from the given featureRefString
+ *
+ * @param featureRefString string feature reference to parse from.
+ * @return a parsed {@link FeatureReferenceV2}
+ */
+ public static FeatureReferenceV2 parseFeatureRef(String featureRefString) {
+ featureRefString = featureRefString.trim();
+ if (featureRefString.isEmpty()) {
+ throw new IllegalArgumentException("Cannot parse a empty feature reference");
+ }
+ if (featureRefString.contains("/")) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported feature reference: Specifying project in string"
+ + " Feature References is not longer supported: %s",
+ featureRefString));
+ }
+ if (!featureRefString.contains(":")) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported feature reference: %s - FeatureTable name and Feature name should be provided in string"
+ + " Feature References, in : format.",
+ featureRefString));
+ }
+
+ String[] featureReferenceParts = featureRefString.split(":");
+ FeatureReferenceV2 featureRef =
+ FeatureReferenceV2.newBuilder()
+ .setFeatureTable(featureReferenceParts[0])
+ .setName(featureReferenceParts[1])
+ .build();
+
+ return featureRef;
+ }
+}
diff --git a/java/sdk/java/src/main/java/com/gojek/feast/Row.java b/java/sdk/java/src/main/java/com/gojek/feast/Row.java
new file mode 100644
index 00000000000..51f820e320a
--- /dev/null
+++ b/java/sdk/java/src/main/java/com/gojek/feast/Row.java
@@ -0,0 +1,161 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 com.gojek.feast;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.util.Timestamps;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse.FieldStatus;
+import feast.proto.types.ValueProto.Value;
+import feast.proto.types.ValueProto.Value.ValCase;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@SuppressWarnings("UnusedReturnValue")
+public class Row {
+ private Timestamp entity_timestamp;
+ private HashMap fields;
+ private HashMap fieldStatuses;
+
+ public static Row create() {
+ Row row = new Row();
+ row.entity_timestamp = Timestamps.fromMillis(System.currentTimeMillis());
+ row.fields = new HashMap<>();
+ row.fieldStatuses = new HashMap<>();
+ return row;
+ }
+
+ public Row setEntityTimestamp(Instant timestamp) {
+ entity_timestamp = Timestamps.fromMillis(timestamp.toEpochMilli());
+ return this;
+ }
+
+ public Timestamp getEntityTimestamp() {
+ return entity_timestamp;
+ }
+
+ public Row setEntityTimestamp(String dateTime) {
+ entity_timestamp = Timestamps.fromMillis(Instant.parse(dateTime).toEpochMilli());
+ return this;
+ }
+
+ public Row set(String fieldName, Object value) {
+ return this.set(fieldName, value, FieldStatus.PRESENT);
+ }
+
+ public Row set(String fieldName, Object value, FieldStatus status) {
+ String valueType = value.getClass().getCanonicalName();
+ switch (valueType) {
+ case "java.lang.Integer":
+ fields.put(fieldName, Value.newBuilder().setInt32Val((int) value).build());
+ break;
+ case "java.lang.Long":
+ fields.put(fieldName, Value.newBuilder().setInt64Val((long) value).build());
+ break;
+ case "java.lang.Float":
+ fields.put(fieldName, Value.newBuilder().setFloatVal((float) value).build());
+ break;
+ case "java.lang.Double":
+ fields.put(fieldName, Value.newBuilder().setDoubleVal((double) value).build());
+ break;
+ case "java.lang.String":
+ fields.put(fieldName, Value.newBuilder().setStringVal((String) value).build());
+ break;
+ case "byte[]":
+ fields.put(
+ fieldName, Value.newBuilder().setBytesVal(ByteString.copyFrom((byte[]) value)).build());
+ break;
+ case "feast.proto.types.ValueProto.Value":
+ fields.put(fieldName, (Value) value);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ String.format(
+ "Type '%s' is unsupported in Feast. Please use one of these value types: Integer, Long, Float, Double, String, byte[].",
+ valueType));
+ }
+
+ fieldStatuses.put(fieldName, status);
+ return this;
+ }
+
+ public Map getFields() {
+ return fields;
+ }
+
+ public Integer getInt(String fieldName) {
+ return getValue(fieldName).map(Value::getInt32Val).orElse(null);
+ }
+
+ public Long getLong(String fieldName) {
+ return getValue(fieldName).map(Value::getInt64Val).orElse(null);
+ }
+
+ public Float getFloat(String fieldName) {
+ return getValue(fieldName).map(Value::getFloatVal).orElse(null);
+ }
+
+ public Double getDouble(String fieldName) {
+ return getValue(fieldName).map(Value::getDoubleVal).orElse(null);
+ }
+
+ public String getString(String fieldName) {
+ return getValue(fieldName).map(Value::getStringVal).orElse(null);
+ }
+
+ public byte[] getByte(String fieldName) {
+ return getValue(fieldName).map(Value::getBytesVal).map(ByteString::toByteArray).orElse(null);
+ }
+
+ public Map getStatuses() {
+ return fieldStatuses;
+ }
+
+ public FieldStatus getStatus(String fieldName) {
+ return fieldStatuses.get(fieldName);
+ }
+
+ @Override
+ public String toString() {
+ List parts = new ArrayList<>();
+ fields.forEach(
+ (key, value) ->
+ parts.add(
+ key
+ + ":"
+ + (value.getValCase().equals(ValCase.VAL_NOT_SET)
+ ? "NULL"
+ : value.toString().trim())));
+ return String.join(", ", parts);
+ }
+
+ private Optional getValue(String fieldName) {
+ if (!fields.containsKey(fieldName)) {
+ throw new IllegalArgumentException(
+ String.format("Row does not contain field '%s'", fieldName));
+ }
+ Value value = fields.get(fieldName);
+ if (value.getValCase().equals(ValCase.VAL_NOT_SET)) {
+ return Optional.empty();
+ }
+ return Optional.of(value);
+ }
+}
diff --git a/java/sdk/java/src/main/java/com/gojek/feast/SecurityConfig.java b/java/sdk/java/src/main/java/com/gojek/feast/SecurityConfig.java
new file mode 100644
index 00000000000..94c779cf440
--- /dev/null
+++ b/java/sdk/java/src/main/java/com/gojek/feast/SecurityConfig.java
@@ -0,0 +1,66 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 com.gojek.feast;
+
+import com.google.auto.value.AutoValue;
+import io.grpc.CallCredentials;
+import java.util.Optional;
+
+/** SecurityConfig captures the security related configuration for FeastClient */
+@AutoValue
+public abstract class SecurityConfig {
+ /**
+ * Enables authentication If specified, the call credentials used to provide credentials to
+ * authenticate with Feast.
+ *
+ * @return credentials
+ */
+ public abstract Optional getCredentials();
+
+ /**
+ * Whether to use TLS transport security is use when connecting to Feast.
+ *
+ * @return true if enabled
+ */
+ public abstract boolean isTLSEnabled();
+
+ /**
+ * If specified and TLS is enabled, provides path to TLS certificate use the verify Service
+ * identity.
+ *
+ * @return certificate path
+ */
+ public abstract Optional getCertificatePath();
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setCredentials(Optional credentials);
+
+ public abstract Builder setTLSEnabled(boolean isTLSEnabled);
+
+ public abstract Builder setCertificatePath(Optional certificatePath);
+
+ public abstract SecurityConfig build();
+ }
+
+ public static SecurityConfig.Builder newBuilder() {
+ return new AutoValue_SecurityConfig.Builder()
+ .setCredentials(Optional.empty())
+ .setTLSEnabled(false)
+ .setCertificatePath(Optional.empty());
+ }
+}
diff --git a/java/sdk/java/src/test/java/com/gojek/feast/FeastClientTest.java b/java/sdk/java/src/test/java/com/gojek/feast/FeastClientTest.java
new file mode 100644
index 00000000000..29185cd153c
--- /dev/null
+++ b/java/sdk/java/src/test/java/com/gojek/feast/FeastClientTest.java
@@ -0,0 +1,168 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 com.gojek.feast;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.Mockito.mock;
+
+import com.google.protobuf.Timestamp;
+import feast.proto.serving.ServingAPIProto.FeatureReferenceV2;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse.FieldStatus;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse.FieldValues;
+import feast.proto.serving.ServingServiceGrpc.ServingServiceImplBase;
+import feast.proto.types.ValueProto.Value;
+import io.grpc.*;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.stub.StreamObserver;
+import io.grpc.testing.GrpcCleanupRule;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class FeastClientTest {
+ private final String AUTH_TOKEN = "test token";
+
+ @Rule public GrpcCleanupRule grpcRule;
+ private AtomicBoolean isAuthenticated;
+
+ private ServingServiceImplBase servingMock =
+ mock(
+ ServingServiceImplBase.class,
+ delegatesTo(
+ new ServingServiceImplBase() {
+ @Override
+ public void getOnlineFeaturesV2(
+ GetOnlineFeaturesRequestV2 request,
+ StreamObserver responseObserver) {
+ if (!request.equals(FeastClientTest.getFakeRequest())) {
+ responseObserver.onError(Status.FAILED_PRECONDITION.asRuntimeException());
+ }
+
+ responseObserver.onNext(FeastClientTest.getFakeResponse());
+ responseObserver.onCompleted();
+ }
+ }));
+
+ private FeastClient client;
+
+ @Before
+ public void setup() throws Exception {
+ this.grpcRule = new GrpcCleanupRule();
+ this.isAuthenticated = new AtomicBoolean(false);
+ // setup fake serving service
+ String serverName = InProcessServerBuilder.generateName();
+ this.grpcRule.register(
+ InProcessServerBuilder.forName(serverName)
+ .directExecutor()
+ .addService(this.servingMock)
+ .build()
+ .start());
+
+ // setup test feast client target
+ ManagedChannel channel =
+ this.grpcRule.register(
+ InProcessChannelBuilder.forName(serverName).directExecutor().build());
+ this.client = new FeastClient(channel, Optional.empty());
+ }
+
+ @Test
+ public void shouldGetOnlineFeatures() {
+ shouldGetOnlineFeaturesWithClient(this.client);
+ }
+
+ private void shouldGetOnlineFeaturesWithClient(FeastClient client) {
+ List rows =
+ client.getOnlineFeatures(
+ Arrays.asList("driver:name", "driver:rating", "driver:null_value"),
+ Arrays.asList(
+ Row.create().set("driver_id", 1).setEntityTimestamp(Instant.ofEpochSecond(100))),
+ "driver_project");
+
+ assertEquals(
+ rows.get(0).getFields(),
+ new HashMap() {
+ {
+ put("driver_id", intValue(1));
+ put("driver:name", strValue("david"));
+ put("driver:rating", intValue(3));
+ put("driver:null_value", Value.newBuilder().build());
+ }
+ });
+ assertEquals(
+ rows.get(0).getStatuses(),
+ new HashMap() {
+ {
+ put("driver_id", FieldStatus.PRESENT);
+ put("driver:name", FieldStatus.PRESENT);
+ put("driver:rating", FieldStatus.PRESENT);
+ put("driver:null_value", FieldStatus.NULL_VALUE);
+ }
+ });
+ }
+
+ private static GetOnlineFeaturesRequestV2 getFakeRequest() {
+ // setup mock serving service stub
+ return GetOnlineFeaturesRequestV2.newBuilder()
+ .addFeatures(
+ FeatureReferenceV2.newBuilder().setFeatureTable("driver").setName("name").build())
+ .addFeatures(
+ FeatureReferenceV2.newBuilder().setFeatureTable("driver").setName("rating").build())
+ .addFeatures(
+ FeatureReferenceV2.newBuilder().setFeatureTable("driver").setName("null_value").build())
+ .addEntityRows(
+ EntityRow.newBuilder()
+ .setTimestamp(Timestamp.newBuilder().setSeconds(100))
+ .putFields("driver_id", intValue(1)))
+ .setProject("driver_project")
+ .build();
+ }
+
+ private static GetOnlineFeaturesResponse getFakeResponse() {
+ return GetOnlineFeaturesResponse.newBuilder()
+ .addFieldValues(
+ FieldValues.newBuilder()
+ .putFields("driver_id", intValue(1))
+ .putStatuses("driver_id", FieldStatus.PRESENT)
+ .putFields("driver:name", strValue("david"))
+ .putStatuses("driver:name", FieldStatus.PRESENT)
+ .putFields("driver:rating", intValue(3))
+ .putStatuses("driver:rating", FieldStatus.PRESENT)
+ .putFields("driver:null_value", Value.newBuilder().build())
+ .putStatuses("driver:null_value", FieldStatus.NULL_VALUE)
+ .build())
+ .build();
+ }
+
+ private static Value strValue(String val) {
+ return Value.newBuilder().setStringVal(val).build();
+ }
+
+ private static Value intValue(int val) {
+ return Value.newBuilder().setInt32Val(val).build();
+ }
+}
diff --git a/java/sdk/java/src/test/java/com/gojek/feast/RequestUtilTest.java b/java/sdk/java/src/test/java/com/gojek/feast/RequestUtilTest.java
new file mode 100644
index 00000000000..1592e20664d
--- /dev/null
+++ b/java/sdk/java/src/test/java/com/gojek/feast/RequestUtilTest.java
@@ -0,0 +1,104 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 com.gojek.feast;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.TextFormat;
+import feast.common.models.FeatureV2;
+import feast.proto.serving.ServingAPIProto.FeatureReferenceV2;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
+
+class RequestUtilTest {
+
+ private static Stream provideValidFeatureRefs() {
+ return Stream.of(
+ Arguments.of(
+ Arrays.asList("driver:driver_id"),
+ Arrays.asList(
+ FeatureReferenceV2.newBuilder()
+ .setFeatureTable("driver")
+ .setName("driver_id")
+ .build())));
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideValidFeatureRefs")
+ void createFeatureRefs_ShouldReturnFeaturesForValidFeatureRefs(
+ List input, List expected) {
+ List actual = RequestUtil.createFeatureRefs(input);
+ // Order of the actual and expected FeatureTables do no not matter
+ actual.sort(Comparator.comparing(FeatureReferenceV2::getName));
+ expected.sort(Comparator.comparing(FeatureReferenceV2::getName));
+ assertEquals(expected.size(), actual.size());
+ for (int i = 0; i < expected.size(); i++) {
+ String expectedString = TextFormat.printer().printToString(expected.get(i));
+ String actualString = TextFormat.printer().printToString(actual.get(i));
+ assertEquals(expectedString, actualString);
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideValidFeatureRefs")
+ void renderFeatureRef_ShouldReturnFeatureRefString(
+ List expected, List input) {
+ input = input.stream().map(ref -> ref.toBuilder().build()).collect(Collectors.toList());
+ List actual =
+ input.stream().map(ref -> FeatureV2.getFeatureStringRef(ref)).collect(Collectors.toList());
+ assertEquals(expected.size(), actual.size());
+ for (int i = 0; i < expected.size(); i++) {
+ assertEquals(expected.get(i), actual.get(i));
+ }
+ }
+
+ private static Stream provideInvalidFeatureRefs() {
+ return Stream.of(Arguments.of(ImmutableList.of("project/feature")));
+ }
+
+ private static Stream provideMissingFeatureTableFeatureRefs() {
+ return Stream.of(Arguments.of(ImmutableList.of("feature")));
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideInvalidFeatureRefs")
+ void createFeatureRefs_ShouldThrowExceptionForProjectInFeatureRefs(List input) {
+ assertThrows(IllegalArgumentException.class, () -> RequestUtil.createFeatureRefs(input));
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideMissingFeatureTableFeatureRefs")
+ void createFeatureRefs_ShouldThrowExceptionForMissingFeatureTableInFeatureRefs(
+ List input) {
+ assertThrows(IllegalArgumentException.class, () -> RequestUtil.createFeatureRefs(input));
+ }
+
+ @ParameterizedTest
+ @NullSource
+ void createFeatureRefs_ShouldThrowExceptionForNullFeatureRefs(List input) {
+ assertThrows(IllegalArgumentException.class, () -> RequestUtil.createFeatureRefs(input));
+ }
+}
diff --git a/java/serving/.dockerignore b/java/serving/.dockerignore
new file mode 100644
index 00000000000..441c3d1a50c
--- /dev/null
+++ b/java/serving/.dockerignore
@@ -0,0 +1,3 @@
+*/.gradle/
+.idea/
+*/build/
\ No newline at end of file
diff --git a/java/serving/.gitignore b/java/serving/.gitignore
new file mode 100644
index 00000000000..6c6b6d8d8f8
--- /dev/null
+++ b/java/serving/.gitignore
@@ -0,0 +1,37 @@
+### Scratch files ###
+scratch*
+
+### Local Environment ###
+*local*.env
+
+### Gradle ###
+.gradle
+**/build/
+!gradle/wrapper/gradle-wrapper.jar
+feast-serving.jar
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+## Feast Temporary Files ##
+/temp/
\ No newline at end of file
diff --git a/java/serving/README.md b/java/serving/README.md
new file mode 100644
index 00000000000..cce8c7d6e29
--- /dev/null
+++ b/java/serving/README.md
@@ -0,0 +1,101 @@
+### Getting Started Guide for Feast Serving Developers
+
+Pre-requisites:
+
+- [Maven](https://maven.apache.org/install.html) build tool version 3.6.x
+- A running Feast Core instance
+- A running Store instance e.g. local Redis Store instance
+
+From the Feast project root directory, run the following Maven command to start Feast Serving gRPC service running on port 6566 locally:
+
+```bash
+# Assumptions:
+# - Local Feast Core is running on localhost:6565
+# Uses configuration from serving/src/main/resources/application.yml
+mvn -pl serving spring-boot:run -Dspring-boot.run.arguments=\
+--feast.core-host=localhost,\
+--feast.core-port=6565
+```
+
+If you have [grpc_cli](https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md) installed, you can check that Feast Serving is running
+```
+grpc_cli ls localhost:6566
+grpc_cli call localhost:6566 GetFeastServingVersion ''
+grpc_cli call localhost:6566 GetFeastServingType ''
+```
+
+```bash
+grpc_cli call localhost:6565 ApplyFeatureSet '
+feature_set {
+ name: "driver"
+ entities {
+ name: "driver_id"
+ value_type: STRING
+ }
+ features {
+ name: "city"
+ value_type: STRING
+ }
+ features {
+ name: "booking_completed_count"
+ value_type: INT64
+ }
+ source {
+ type: KAFKA
+ kafka_source_config {
+ bootstrap_servers: "localhost:9092"
+ }
+ }
+}
+'
+
+grpc_cli call localhost:6565 GetFeatureSets '
+filter {
+ feature_set_name: "driver"
+}
+'
+
+grpc_cli call localhost:6566 GetBatchFeatures '
+feature_sets {
+ name: "driver"
+ feature_names: "booking_completed_count"
+ max_age {
+ seconds: 86400
+ }
+}
+entity_dataset {
+ entity_names: "driver_id"
+ entity_dataset_rows {
+ entity_timestamp {
+ seconds: 1569873954
+ }
+ }
+}
+'
+```
+
+```
+python3 <
+
+
+ 4.0.0
+
+
+ dev.feast
+ feast-parent
+ ${revision}
+
+
+ feast-serving
+ Feast Serving
+ Feature serving API service
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 11
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ false
+
+
+
+ build-info
+
+ build-info
+
+
+
+
+
+
+
+
+
+
+ dev.feast
+ datatypes-java
+ ${project.version}
+
+
+
+
+ dev.feast
+ feast-storage-api
+ ${project.version}
+
+
+
+ dev.feast
+ feast-storage-connector-redis
+ ${project.version}
+
+
+
+ dev.feast
+ feast-storage-connector-bigtable
+ ${project.version}
+
+
+
+ dev.feast
+ feast-storage-connector-cassandra
+ ${project.version}
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-log4j2
+
+
+ org.apache.logging.log4j
+ log4j-web
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ org.springframework.boot
+ spring-boot-test
+ 2.3.1.RELEASE
+ test
+
+
+
+
+ io.grpc
+ grpc-services
+
+
+
+ io.grpc
+ grpc-stub
+
+
+
+ com.google.protobuf
+ protobuf-java-util
+
+
+
+
+ com.google.guava
+ guava
+
+
+
+ joda-time
+ joda-time
+
+
+
+ io.jaegertracing
+ jaeger-client
+ 1.3.2
+
+
+ io.opentracing
+ opentracing-api
+ 0.33.0
+
+
+ io.opentracing
+ opentracing-noop
+ 0.33.0
+
+
+ io.opentracing.contrib
+ opentracing-grpc
+ 0.2.3
+
+
+
+
+ io.prometheus
+ simpleclient
+ 0.8.0
+
+
+
+
+ io.prometheus
+ simpleclient_hotspot
+ 0.8.0
+
+
+
+
+ io.prometheus
+ simpleclient_servlet
+ 0.8.0
+
+
+ io.prometheus
+ simpleclient_spring_boot
+ 0.8.0
+
+
+
+ com.google.auto.value
+ auto-value-annotations
+ 1.6.6
+
+
+
+
+
+ io.grpc
+ grpc-testing
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
+ org.apache.avro
+ avro
+ 1.10.2
+
+
+
+
+ org.apache.arrow
+ arrow-java-root
+ 5.0.0
+ pom
+
+
+
+
+ org.apache.arrow
+ arrow-vector
+ 5.0.0
+
+
+
+
+ org.apache.arrow
+ arrow-memory
+ 5.0.0
+ pom
+
+
+
+
+ org.apache.arrow
+ arrow-memory-netty
+ 5.0.0
+ runtime
+
+
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+
+
+
+ com.github.kstyrc
+ embedded-redis
+ test
+
+
+ jakarta.validation
+ jakarta.validation-api
+ ${jakarta.validation.api.version}
+
+
+ net.devh
+ grpc-server-spring-boot-starter
+ ${grpc.spring.boot.starter.version}
+
+
+ org.testcontainers
+ testcontainers
+ 1.16.2
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ 1.16.2
+ test
+
+
+ org.awaitility
+ awaitility
+ 3.0.0
+ test
+
+
+ com.squareup.okhttp
+ okhttp
+ 2.7.4
+ test
+
+
+
+
+
+ profile-local
+
+
+ !ci
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.1.0
+
+
+ @
+
+ false
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+
+
diff --git a/java/serving/src/main/java/feast/serving/ServingApplication.java b/java/serving/src/main/java/feast/serving/ServingApplication.java
new file mode 100644
index 00000000000..ab036d04d18
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/ServingApplication.java
@@ -0,0 +1,38 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving;
+
+import feast.serving.config.FeastProperties;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+@SpringBootApplication(
+ exclude = {
+ DataSourceAutoConfiguration.class,
+ DataSourceTransactionManagerAutoConfiguration.class,
+ HibernateJpaAutoConfiguration.class
+ })
+@EnableConfigurationProperties(FeastProperties.class)
+public class ServingApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(ServingApplication.class, args);
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/config/FeastProperties.java b/java/serving/src/main/java/feast/serving/config/FeastProperties.java
new file mode 100644
index 00000000000..493f8f9ed62
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/config/FeastProperties.java
@@ -0,0 +1,365 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.config;
+
+// Feast configuration properties that maps Feast configuration from default application.yml file to
+// a Java object.
+// https://www.baeldung.com/configuration-properties-in-spring-boot
+// https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties
+
+import feast.common.logging.config.LoggingProperties;
+import feast.storage.connectors.redis.retriever.RedisClusterStoreConfig;
+import feast.storage.connectors.redis.retriever.RedisStoreConfig;
+import io.lettuce.core.ReadFrom;
+import java.time.Duration;
+import java.util.*;
+import javax.annotation.PostConstruct;
+import javax.validation.*;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.info.BuildProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+
+/** Feast Serving properties. */
+@ComponentScan("feast.common.logging")
+@ConfigurationProperties(prefix = "feast", ignoreInvalidFields = true)
+public class FeastProperties {
+
+ /**
+ * Instantiates a new Feast Serving properties.
+ *
+ * @param buildProperties the build properties
+ */
+ @Autowired
+ public FeastProperties(BuildProperties buildProperties) {
+ setVersion(buildProperties.getVersion());
+ }
+
+ /** Instantiates a new Feast class. */
+ public FeastProperties() {}
+
+ /* Feast Serving build version */
+ @NotBlank private String version = "unknown";
+
+ @NotBlank private String registry;
+
+ public String getRegistry() {
+ return registry;
+ }
+
+ public void setRegistry(final String registry) {
+ this.registry = registry;
+ }
+
+ private String transformationServiceEndpoint;
+
+ public String getTransformationServiceEndpoint() {
+ return transformationServiceEndpoint;
+ }
+
+ public void setTransformationServiceEndpoint(final String transformationServiceEndpoint) {
+ this.transformationServiceEndpoint = transformationServiceEndpoint;
+ }
+
+ /**
+ * Finds and returns the active store
+ *
+ * @return Returns the {@link Store} model object
+ */
+ public Store getActiveStore() {
+ for (Store store : getStores()) {
+ if (activeStore.equals(store.getName())) {
+ return store;
+ }
+ }
+ throw new RuntimeException(
+ String.format("Active store is misconfigured. Could not find store: %s.", activeStore));
+ }
+
+ /**
+ * Set the name of the active store found in the "stores" configuration list
+ *
+ * @param activeStore String name to active store
+ */
+ public void setActiveStore(String activeStore) {
+ this.activeStore = activeStore;
+ }
+
+ /** Name of the active store configuration (only one store can be active at a time). */
+ @NotBlank private String activeStore;
+
+ /**
+ * Collection of store configurations. The active store is selected by the "activeStore" field.
+ */
+ private List stores = new ArrayList<>();
+
+ /* Metric tracing properties. */
+ private TracingProperties tracing;
+
+ /* Feast Audit Logging properties */
+ @NotNull private LoggingProperties logging;
+
+ @Bean
+ LoggingProperties loggingProperties() {
+ return getLogging();
+ }
+
+ /**
+ * Gets Serving store configuration as a list of {@link Store}.
+ *
+ * @return List of stores objects
+ */
+ public List getStores() {
+ return stores;
+ }
+
+ /**
+ * Gets Feast Serving build version.
+ *
+ * @return the build version
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Sets build version
+ *
+ * @param version the build version
+ */
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ /**
+ * Sets the collection of configured stores.
+ *
+ * @param stores List of {@link Store}
+ */
+ public void setStores(List stores) {
+ this.stores = stores;
+ }
+
+ /** Store configuration class for database that this Feast Serving uses. */
+ public static class Store {
+
+ private String name;
+
+ private String type;
+
+ private Map config = new HashMap<>();
+
+ /**
+ * Gets name of this store. This is unique to this specific instance.
+ *
+ * @return the name of the store
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name of this store.
+ *
+ * @param name the name of the store
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Gets the store type. Example are REDIS, REDIS_CLUSTER, BIGTABLE or CASSANDRA
+ *
+ * @return the store type as a String.
+ */
+ public StoreType getType() {
+ return StoreType.valueOf(this.type);
+ }
+
+ /**
+ * Sets the store type
+ *
+ * @param type the type
+ */
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ /**
+ * Gets the configuration to this specific store. This is a map of strings. These options are
+ * unique to the store. Please see protos/feast/core/Store.proto for the store specific
+ * configuration options
+ *
+ * @return Returns the store specific configuration
+ */
+ public Map getConfig() {
+ return config;
+ }
+
+ public RedisClusterStoreConfig getRedisClusterConfig() {
+ return new RedisClusterStoreConfig(
+ this.config.get("connection_string"),
+ ReadFrom.valueOf(this.config.get("read_from")),
+ Duration.parse(this.config.get("timeout")));
+ }
+
+ public RedisStoreConfig getRedisConfig() {
+ return new RedisStoreConfig(
+ this.config.get("host"),
+ Integer.valueOf(this.config.get("port")),
+ Boolean.valueOf(this.config.getOrDefault("ssl", "false")),
+ this.config.getOrDefault("password", ""));
+ }
+
+ /**
+ * Sets the store config. Please protos/feast/core/Store.proto for the specific options for each
+ * store.
+ *
+ * @param config the config map
+ */
+ public void setConfig(Map config) {
+ this.config = config;
+ }
+ }
+
+ public enum StoreType {
+ REDIS,
+ REDIS_CLUSTER;
+ }
+
+ /**
+ * Gets tracing properties
+ *
+ * @return tracing properties
+ */
+ public TracingProperties getTracing() {
+ return tracing;
+ }
+
+ /**
+ * Sets the tracing configuration.
+ *
+ * @param tracing the tracing
+ */
+ public void setTracing(TracingProperties tracing) {
+ this.tracing = tracing;
+ }
+
+ /**
+ * Gets logging properties
+ *
+ * @return logging properties
+ */
+ public LoggingProperties getLogging() {
+ return logging;
+ }
+
+ /**
+ * Sets logging properties
+ *
+ * @param logging the logging properties
+ */
+ public void setLogging(LoggingProperties logging) {
+ this.logging = logging;
+ }
+
+ /** Trace metric collection properties */
+ public static class TracingProperties {
+
+ /** Tracing enabled/disabled */
+ private boolean enabled;
+
+ /** Name of tracer to use (only "jaeger") */
+ private String tracerName;
+
+ /** Service name uniquely identifies this Feast Serving deployment */
+ private String serviceName;
+
+ /**
+ * Is tracing enabled
+ *
+ * @return boolean flag
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Sets tracing enabled or disabled.
+ *
+ * @param enabled flag
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * Gets tracer name ('jaeger')
+ *
+ * @return the tracer name
+ */
+ public String getTracerName() {
+ return tracerName;
+ }
+
+ /**
+ * Sets tracer name.
+ *
+ * @param tracerName the tracer name
+ */
+ public void setTracerName(String tracerName) {
+ this.tracerName = tracerName;
+ }
+
+ /**
+ * Gets the service name. The service name uniquely identifies this Feast serving instance.
+ *
+ * @return the service name
+ */
+ public String getServiceName() {
+ return serviceName;
+ }
+
+ /**
+ * Sets service name.
+ *
+ * @param serviceName the service name
+ */
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+ }
+
+ /**
+ * Validates all FeastProperties. This method runs after properties have been initialized and
+ * individually and conditionally validates each class.
+ */
+ @PostConstruct
+ public void validate() {
+ ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+ Validator validator = factory.getValidator();
+
+ // Validate root fields in FeastProperties
+ Set> violations = validator.validate(this);
+ if (!violations.isEmpty()) {
+ throw new ConstraintViolationException(violations);
+ }
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/config/InstrumentationConfig.java b/java/serving/src/main/java/feast/serving/config/InstrumentationConfig.java
new file mode 100644
index 00000000000..295b263f664
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/config/InstrumentationConfig.java
@@ -0,0 +1,63 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.config;
+
+import io.opentracing.Tracer;
+import io.opentracing.contrib.grpc.TracingServerInterceptor;
+import io.opentracing.noop.NoopTracerFactory;
+import io.prometheus.client.exporter.MetricsServlet;
+import io.prometheus.client.hotspot.DefaultExports;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class InstrumentationConfig {
+
+ private FeastProperties feastProperties;
+
+ @Autowired
+ public InstrumentationConfig(FeastProperties feastProperties) {
+ this.feastProperties = feastProperties;
+ }
+
+ @Bean
+ public ServletRegistrationBean servletRegistrationBean() {
+ DefaultExports.initialize();
+ return new ServletRegistrationBean(new MetricsServlet(), "/metrics");
+ }
+
+ @Bean
+ public Tracer tracer() {
+ if (!feastProperties.getTracing().isEnabled()) {
+ return NoopTracerFactory.create();
+ }
+
+ if (!feastProperties.getTracing().getTracerName().equalsIgnoreCase("jaeger")) {
+ throw new IllegalArgumentException("Only 'jaeger' tracer is supported for now.");
+ }
+
+ return io.jaegertracing.Configuration.fromEnv(feastProperties.getTracing().getServiceName())
+ .getTracer();
+ }
+
+ @Bean
+ public TracingServerInterceptor tracingInterceptor(Tracer tracer) {
+ return TracingServerInterceptor.newBuilder().withTracer(tracer).build();
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/config/ServingApiConfiguration.java b/java/serving/src/main/java/feast/serving/config/ServingApiConfiguration.java
new file mode 100644
index 00000000000..ce4fe134373
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/config/ServingApiConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.config;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.protobuf.ProtobufJsonFormatHttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class ServingApiConfiguration implements WebMvcConfigurer {
+ @Autowired private ProtobufJsonFormatHttpMessageConverter protobufConverter;
+
+ @Bean
+ ProtobufJsonFormatHttpMessageConverter protobufHttpMessageConverter() {
+ return new ProtobufJsonFormatHttpMessageConverter();
+ }
+
+ @Override
+ public void configureMessageConverters(List> converters) {
+ converters.add(protobufConverter);
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java b/java/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java
new file mode 100644
index 00000000000..3a7936cb578
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java
@@ -0,0 +1,79 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2020 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.config;
+
+import feast.serving.registry.LocalRegistryRepo;
+import feast.serving.service.OnlineServingServiceV2;
+import feast.serving.service.OnlineTransformationService;
+import feast.serving.service.ServingServiceV2;
+import feast.serving.specs.FeatureSpecRetriever;
+import feast.serving.specs.RegistryFeatureSpecRetriever;
+import feast.storage.api.retriever.OnlineRetrieverV2;
+import feast.storage.connectors.redis.retriever.*;
+import io.opentracing.Tracer;
+import java.nio.file.Paths;
+import org.slf4j.Logger;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ServingServiceConfigV2 {
+ private static final Logger log = org.slf4j.LoggerFactory.getLogger(ServingServiceConfigV2.class);
+
+ @Bean
+ public ServingServiceV2 registryBasedServingServiceV2(
+ FeastProperties feastProperties, Tracer tracer) {
+ final ServingServiceV2 servingService;
+ final FeastProperties.Store store = feastProperties.getActiveStore();
+
+ OnlineRetrieverV2 retrieverV2;
+ // TODO: Support more store types, and potentially use a plugin model here.
+ switch (store.getType()) {
+ case REDIS_CLUSTER:
+ RedisClientAdapter redisClusterClient =
+ RedisClusterClient.create(store.getRedisClusterConfig());
+ retrieverV2 = new OnlineRetriever(redisClusterClient, new EntityKeySerializerV2());
+ break;
+ case REDIS:
+ RedisClientAdapter redisClient = RedisClient.create(store.getRedisConfig());
+ log.info("Created EntityKeySerializerV2");
+ retrieverV2 = new OnlineRetriever(redisClient, new EntityKeySerializerV2());
+ break;
+ default:
+ throw new RuntimeException(
+ String.format(
+ "Unable to identify online store type: %s for Regsitry Backed Serving Service",
+ store.getType()));
+ }
+
+ final FeatureSpecRetriever featureSpecRetriever;
+ log.info("Created RegistryFeatureSpecRetriever");
+ log.info("Working Directory = " + System.getProperty("user.dir"));
+ final LocalRegistryRepo repo = new LocalRegistryRepo(Paths.get(feastProperties.getRegistry()));
+ featureSpecRetriever = new RegistryFeatureSpecRetriever(repo);
+
+ final String transformationServiceEndpoint = feastProperties.getTransformationServiceEndpoint();
+ final OnlineTransformationService onlineTransformationService =
+ new OnlineTransformationService(transformationServiceEndpoint, featureSpecRetriever);
+
+ servingService =
+ new OnlineServingServiceV2(
+ retrieverV2, tracer, featureSpecRetriever, onlineTransformationService);
+
+ return servingService;
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/config/WebSecurityConfig.java b/java/serving/src/main/java/feast/serving/config/WebSecurityConfig.java
new file mode 100644
index 00000000000..04d3f4b5afb
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/config/WebSecurityConfig.java
@@ -0,0 +1,51 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2020 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * WebSecurityConfig disables auto configuration of Spring HTTP Security and allows security methods
+ * to be overridden
+ */
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+ /**
+ * Allows for custom web security rules to be applied.
+ *
+ * @param http {@link HttpSecurity} for configuring web based security
+ * @throws Exception exception
+ */
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+
+ // Bypasses security/authentication for the following paths
+ http.authorizeRequests()
+ .antMatchers("/actuator/**", "/metrics/**")
+ .permitAll()
+ .anyRequest()
+ .authenticated()
+ .and()
+ .csrf()
+ .disable();
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/controller/HealthServiceController.java b/java/serving/src/main/java/feast/serving/controller/HealthServiceController.java
new file mode 100644
index 00000000000..ef675d4c157
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/controller/HealthServiceController.java
@@ -0,0 +1,59 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.controller;
+
+import feast.proto.serving.ServingAPIProto.GetFeastServingInfoRequest;
+import feast.serving.interceptors.GrpcMonitoringInterceptor;
+import feast.serving.service.ServingServiceV2;
+import io.grpc.health.v1.HealthGrpc.HealthImplBase;
+import io.grpc.health.v1.HealthProto.HealthCheckRequest;
+import io.grpc.health.v1.HealthProto.HealthCheckResponse;
+import io.grpc.health.v1.HealthProto.ServingStatus;
+import io.grpc.stub.StreamObserver;
+import net.devh.boot.grpc.server.service.GrpcService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+// Reference: https://github.com/grpc/grpc/blob/master/doc/health-checking.md
+
+@GrpcService(interceptors = {GrpcMonitoringInterceptor.class})
+public class HealthServiceController extends HealthImplBase {
+ private final ServingServiceV2 servingService;
+
+ @Autowired
+ public HealthServiceController(final ServingServiceV2 servingService) {
+ this.servingService = servingService;
+ }
+
+ @Override
+ public void check(
+ HealthCheckRequest request, StreamObserver responseObserver) {
+ // TODO: Implement proper logic to determine if ServingServiceV2 is healthy e.g.
+ // if it's online service check that it the service can retrieve dummy/random
+ // feature table.
+ // Implement similarly for batch service.
+
+ try {
+ servingService.getFeastServingInfo(GetFeastServingInfoRequest.getDefaultInstance());
+ responseObserver.onNext(
+ HealthCheckResponse.newBuilder().setStatus(ServingStatus.SERVING).build());
+ } catch (Exception e) {
+ responseObserver.onNext(
+ HealthCheckResponse.newBuilder().setStatus(ServingStatus.NOT_SERVING).build());
+ }
+ responseObserver.onCompleted();
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java b/java/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java
new file mode 100644
index 00000000000..048a53fad1b
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java
@@ -0,0 +1,107 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.controller;
+
+import feast.common.logging.interceptors.GrpcMessageInterceptor;
+import feast.proto.serving.ServingAPIProto;
+import feast.proto.serving.ServingAPIProto.GetFeastServingInfoRequest;
+import feast.proto.serving.ServingAPIProto.GetFeastServingInfoResponse;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse;
+import feast.proto.serving.ServingServiceGrpc.ServingServiceImplBase;
+import feast.serving.config.FeastProperties;
+import feast.serving.exception.SpecRetrievalException;
+import feast.serving.interceptors.GrpcMonitoringContext;
+import feast.serving.interceptors.GrpcMonitoringInterceptor;
+import feast.serving.service.ServingServiceV2;
+import feast.serving.util.RequestHelper;
+import io.grpc.Status;
+import io.grpc.stub.StreamObserver;
+import io.opentracing.Span;
+import io.opentracing.Tracer;
+import io.opentracing.contrib.grpc.TracingServerInterceptor;
+import net.devh.boot.grpc.server.service.GrpcService;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.AccessDeniedException;
+
+@GrpcService(
+ interceptors = {
+ TracingServerInterceptor.class,
+ GrpcMessageInterceptor.class,
+ GrpcMonitoringInterceptor.class
+ })
+public class ServingServiceGRpcController extends ServingServiceImplBase {
+
+ private static final Logger log =
+ org.slf4j.LoggerFactory.getLogger(ServingServiceGRpcController.class);
+ private final ServingServiceV2 servingServiceV2;
+ private final String version;
+ private final Tracer tracer;
+
+ @Autowired
+ public ServingServiceGRpcController(
+ ServingServiceV2 servingServiceV2, FeastProperties feastProperties, Tracer tracer) {
+ this.servingServiceV2 = servingServiceV2;
+ this.version = feastProperties.getVersion();
+ this.tracer = tracer;
+ }
+
+ @Override
+ public void getFeastServingInfo(
+ GetFeastServingInfoRequest request,
+ StreamObserver responseObserver) {
+ GetFeastServingInfoResponse feastServingInfo = servingServiceV2.getFeastServingInfo(request);
+ feastServingInfo = feastServingInfo.toBuilder().setVersion(version).build();
+ responseObserver.onNext(feastServingInfo);
+ responseObserver.onCompleted();
+ }
+
+ @Override
+ public void getOnlineFeaturesV2(
+ ServingAPIProto.GetOnlineFeaturesRequestV2 request,
+ StreamObserver responseObserver) {
+ try {
+ // authorize for the project in request object.
+ if (request.getProject() != null && !request.getProject().isEmpty()) {
+ // update monitoring context
+ GrpcMonitoringContext.getInstance().setProject(request.getProject());
+ }
+ RequestHelper.validateOnlineRequest(request);
+ Span span = tracer.buildSpan("getOnlineFeaturesV2").start();
+ GetOnlineFeaturesResponse onlineFeatures = servingServiceV2.getOnlineFeatures(request);
+ if (span != null) {
+ span.finish();
+ }
+ responseObserver.onNext(onlineFeatures);
+ responseObserver.onCompleted();
+ } catch (SpecRetrievalException e) {
+ log.error("Failed to retrieve specs in SpecService", e);
+ responseObserver.onError(
+ Status.NOT_FOUND.withDescription(e.getMessage()).withCause(e).asException());
+ } catch (AccessDeniedException e) {
+ log.info(String.format("User prevented from accessing one of the projects in request"));
+ responseObserver.onError(
+ Status.PERMISSION_DENIED
+ .withDescription(e.getMessage())
+ .withCause(e)
+ .asRuntimeException());
+ } catch (Exception e) {
+ log.warn("Failed to get Online Features", e);
+ responseObserver.onError(e);
+ }
+ }
+}
diff --git a/java/serving/src/main/java/feast/serving/controller/ServingServiceRestController.java b/java/serving/src/main/java/feast/serving/controller/ServingServiceRestController.java
new file mode 100644
index 00000000000..8a198a82019
--- /dev/null
+++ b/java/serving/src/main/java/feast/serving/controller/ServingServiceRestController.java
@@ -0,0 +1,65 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright 2018-2019 The Feast Authors
+ *
+ * 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
+ *
+ * https://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 feast.serving.controller;
+
+import static feast.serving.util.mappers.ResponseJSONMapper.mapGetOnlineFeaturesResponse;
+
+import feast.proto.serving.ServingAPIProto.GetFeastServingInfoRequest;
+import feast.proto.serving.ServingAPIProto.GetFeastServingInfoResponse;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2;
+import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse;
+import feast.serving.config.FeastProperties;
+import feast.serving.service.ServingServiceV2;
+import feast.serving.util.RequestHelper;
+import java.util.List;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ServingServiceRestController {
+
+ private final ServingServiceV2 servingService;
+ private final String version;
+
+ @Autowired
+ public ServingServiceRestController(
+ ServingServiceV2 servingService, FeastProperties feastProperties) {
+ this.servingService = servingService;
+ this.version = feastProperties.getVersion();
+ }
+
+ @RequestMapping(value = "/api/v1/info", produces = "application/json")
+ public GetFeastServingInfoResponse getInfo() {
+ GetFeastServingInfoResponse feastServingInfo =
+ servingService.getFeastServingInfo(GetFeastServingInfoRequest.getDefaultInstance());
+ return feastServingInfo.toBuilder().setVersion(version).build();
+ }
+
+ @RequestMapping(
+ value = "/api/v1/features/online",
+ produces = "application/json",
+ consumes = "application/json")
+ public List