diff --git a/core/src/main/java/com/flipkart/gjex/core/Bundle.java b/core/src/main/java/com/flipkart/gjex/core/Bundle.java index 6fbcb07c..4bff6ddb 100644 --- a/core/src/main/java/com/flipkart/gjex/core/Bundle.java +++ b/core/src/main/java/com/flipkart/gjex/core/Bundle.java @@ -19,11 +19,11 @@ import java.util.List; import java.util.Map; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; import com.flipkart.gjex.core.job.ScheduledJob; import org.glassfish.jersey.server.ResourceConfig; import io.dropwizard.metrics5.health.HealthCheck; -import com.flipkart.gjex.core.filter.Filter; import com.flipkart.gjex.core.service.Service; import com.flipkart.gjex.core.setup.Bootstrap; import com.flipkart.gjex.core.setup.Environment; @@ -59,7 +59,7 @@ public interface Bundle { * Returns Filter instances loaded by this Bundle * @return List containing Filter instances */ - List getFilters(); + List getGrpcFilters(); /** * Returns HealthCheck instances loaded by this Bundle diff --git a/core/src/main/java/com/flipkart/gjex/core/GJEXConfiguration.java b/core/src/main/java/com/flipkart/gjex/core/GJEXConfiguration.java index d185b965..3c44e2d4 100644 --- a/core/src/main/java/com/flipkart/gjex/core/GJEXConfiguration.java +++ b/core/src/main/java/com/flipkart/gjex/core/GJEXConfiguration.java @@ -20,11 +20,13 @@ import com.flipkart.gjex.core.config.DashboardService; import com.flipkart.gjex.core.config.GrpcConfig; import com.flipkart.gjex.core.config.Tracing; +import lombok.Data; import javax.validation.Valid; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; +@Data public class GJEXConfiguration { @Valid @@ -51,45 +53,7 @@ public class GJEXConfiguration { @JsonProperty("ScheduledJobs.executorThreads") private int scheduledJobExecutorThreads; - public GrpcConfig getGrpc() { - return grpc; - } - - public void setGrpc(GrpcConfig grpc) { - this.grpc = grpc; - } - - public ApiService getApiService() { - return apiService; - } - - public void setApiService(ApiService apiService) { - this.apiService = apiService; - } - - public DashboardService getDashboardService() { - return dashboardService; - } - public void setDashboardService(DashboardService dashboardService) { - this.dashboardService = dashboardService; - } - - public Tracing getTracing() { - return tracing; - } - - public void setTracing(Tracing tracing) { - this.tracing = tracing; - } - - public int getScheduledJobExecutorThreads() { - return scheduledJobExecutorThreads; - } - - public void setScheduledJobExecutorThreads(int scheduledJobExecutorThreads) { - this.scheduledJobExecutorThreads = scheduledJobExecutorThreads; - } @Override public String toString() { diff --git a/core/src/main/java/com/flipkart/gjex/core/config/ApiService.java b/core/src/main/java/com/flipkart/gjex/core/config/ApiService.java index 0cb0f3e6..7149ace1 100644 --- a/core/src/main/java/com/flipkart/gjex/core/config/ApiService.java +++ b/core/src/main/java/com/flipkart/gjex/core/config/ApiService.java @@ -16,9 +16,12 @@ package com.flipkart.gjex.core.config; import com.fasterxml.jackson.annotation.JsonProperty; +import com.flipkart.gjex.core.filter.http.HttpFilterConfig; +import lombok.Data; import javax.validation.constraints.Min; +@Data public class ApiService { @Min(1) @@ -38,43 +41,6 @@ public class ApiService { @JsonProperty("scheduledexecutor.threadpool.size") private int scheduledExecutorThreadPoolSize; - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public int getAcceptors() { - return acceptors; - } - - public void setAcceptors(int acceptors) { - this.acceptors = acceptors; - } - - public int getSelectors() { - return selectors; - } - - public void setSelectors(int selectors) { - this.selectors = selectors; - } - - public int getWorkers() { - return workers; - } - - public void setWorkers(int workers) { - this.workers = workers; - } - - public int getScheduledExecutorThreadPoolSize() { - return scheduledExecutorThreadPoolSize; - } - - public void setScheduledExecutorThreadPoolSize(int scheduledExecutorThreadPoolSize) { - this.scheduledExecutorThreadPoolSize = scheduledExecutorThreadPoolSize; - } + @JsonProperty("filterConfig") + private HttpFilterConfig httpFilterConfig; } diff --git a/core/src/main/java/com/flipkart/gjex/core/config/DashboardService.java b/core/src/main/java/com/flipkart/gjex/core/config/DashboardService.java index 041c5b12..2e7d7fc6 100644 --- a/core/src/main/java/com/flipkart/gjex/core/config/DashboardService.java +++ b/core/src/main/java/com/flipkart/gjex/core/config/DashboardService.java @@ -16,7 +16,9 @@ package com.flipkart.gjex.core.config; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +@Data public class DashboardService { @JsonProperty("service.port") @@ -30,36 +32,4 @@ public class DashboardService { @JsonProperty("service.workers") private int workers; - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public int getAcceptors() { - return acceptors; - } - - public void setAcceptors(int acceptors) { - this.acceptors = acceptors; - } - - public int getSelectors() { - return selectors; - } - - public void setSelectors(int selectors) { - this.selectors = selectors; - } - - public int getWorkers() { - return workers; - } - - public void setWorkers(int workers) { - this.workers = workers; - } } diff --git a/core/src/main/java/com/flipkart/gjex/core/config/GrpcConfig.java b/core/src/main/java/com/flipkart/gjex/core/config/GrpcConfig.java index f8d166ad..9b9c2a77 100644 --- a/core/src/main/java/com/flipkart/gjex/core/config/GrpcConfig.java +++ b/core/src/main/java/com/flipkart/gjex/core/config/GrpcConfig.java @@ -16,7 +16,10 @@ package com.flipkart.gjex.core.config; import com.fasterxml.jackson.annotation.JsonProperty; +import com.flipkart.gjex.core.filter.grpc.GrpcFilterConfig; +import lombok.Data; +@Data public class GrpcConfig { @JsonProperty("server.port") @@ -28,27 +31,6 @@ public class GrpcConfig { @JsonProperty("server.executorThreads") private int executorThreads = 0; - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public int getMaxMessageSize() { - return maxMessageSize; - } - - public void setMaxMessageSize(int maxMessageSize) { - this.maxMessageSize = maxMessageSize; - } - - public int getExecutorThreads() { - return executorThreads; - } - - public void setExecutorThreads(int executorThreads) { - this.executorThreads = executorThreads; - } + @JsonProperty("filterConfig") + private GrpcFilterConfig grpcFilterConfig; } diff --git a/core/src/main/java/com/flipkart/gjex/core/config/Tracing.java b/core/src/main/java/com/flipkart/gjex/core/config/Tracing.java index b32ce219..b11f42f4 100644 --- a/core/src/main/java/com/flipkart/gjex/core/config/Tracing.java +++ b/core/src/main/java/com/flipkart/gjex/core/config/Tracing.java @@ -2,20 +2,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; import javax.validation.constraints.NotNull; +@Data public class Tracing { @NotNull @JsonProperty("collector.endpoint") private String collectorEndpoint; - - public String getCollectorEndpoint() { - return collectorEndpoint; - } - - public void setCollectorEndpoint(String collectorEndpoint) { - this.collectorEndpoint = collectorEndpoint; - } } diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/Filter.java b/core/src/main/java/com/flipkart/gjex/core/filter/Filter.java index e77d30e5..3bf16a70 100644 --- a/core/src/main/java/com/flipkart/gjex/core/filter/Filter.java +++ b/core/src/main/java/com/flipkart/gjex/core/filter/Filter.java @@ -15,64 +15,37 @@ */ package com.flipkart.gjex.core.filter; -import com.google.protobuf.GeneratedMessageV3; -import io.grpc.Metadata; -import io.grpc.Status; -import io.grpc.StatusRuntimeException; +import com.flipkart.gjex.core.logging.Logging; /** - * A Filter interface for processing Request, Request-Headers, Response and Response-Headers around gRPC method invocation - * - * @author regu.b + * A Filter interface for processing Request, Request-Headers, Response and Response-Headers + * around gRPC and HTTP method invocation * - * @param Proto V3 message - * @param Proto V3 message + * @author regu.b */ -public interface Filter { +public abstract class Filter { + + /** Lifecycle methods for cleaning up resources used by this Filter*/ + public void destroy(){} - /** Lifecycle methods for initializing and cleaning up resources used by this Filter*/ - default void init() {} - default void destroy() {} + /** + * Call-back to decorate or inspect the Request body/message. This Filter cannot fail processing of the Request body and hence there is no support for indicating failure. + * This method should be viewed almost like a proxy for the Request body. + * @param req the Request body/message + * @param requestParams prams + */ + public void doProcessRequest(Req req, RequestParams requestParams){} - /** - * Function for creating an instance of this {@link Filter} - * Use only this function to get the {@link Filter} instance - */ - Filter getInstance(); + /** + * Call-back to decorate or inspect the Response headers. Implementations may use this method to set additional headers in the response. + * @param responseHeaders the Response Headers + */ + public void doProcessResponseHeaders(M responseHeaders) {} - /** - * Call-back to process Request headers and Filter out processing of the next incoming Request Proto V3 body/message. - * @param requestHeaders Request Headers - * @throws StatusRuntimeException thrown with suitable {@link Status} to indicate reason for failing the request - */ - default void doFilterRequest(ServerRequestParams serverRequestParams, Metadata requestHeaders) throws StatusRuntimeException{} - - /** - * Call-back to decorate or inspect the Reauest Proto V3 body/message. This Filter cannot fail processing of the Request body and hence there is no support for indicating failure. - * This method should be viewed almost like a proxy for the Request body. - * @param request the Request Proto V3 body/message - */ - default void doProcessRequest(Req request) {} - - /** - * Call-back to decorate or inspect the Response headers. Implementations may use this method to set additional headers in the response. - * @param responseHeaders the Response Headers - */ - default void doProcessResponseHeaders(Metadata responseHeaders) {} - - /** - * Call-back to decorate or inspect the Response Proto V3 body/message. This Filter cannot fail processing of the Response body and hence there is no support for indicating failure. - * This method should be viewed almost like a proxy for the Response body. - * @param response the Response Proto V3 body/message - */ - default void doProcessResponse(Res response) {} - - /** - * Returns array of {@link Key} identifiers for headers to be forwarded - * @return array of {@link Key} - */ - @SuppressWarnings("rawtypes") - default Metadata.Key[] getForwardHeaderKeys(){ - return new Metadata.Key[] {}; - } + /** + * Call-back to decorate or inspect the Response body/message. This Filter cannot fail processing of the Response body and hence there is no support for indicating failure. + * This method should be viewed almost like a proxy for the Response body. + * @param response the Response body/message + */ + public void doProcessResponse(Res response) {} } diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/RequestParams.java b/core/src/main/java/com/flipkart/gjex/core/filter/RequestParams.java new file mode 100644 index 00000000..e81bbba0 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/RequestParams.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.filter; + +import lombok.Builder; +import lombok.Getter; + +/** + * Represents the parameters of a request within the GJEX framework. + * This class encapsulates common request parameters such as client IP, resource path, + * and any additional metadata associated with the request. + * + * @param The type of the metadata associated with the request. This allows for flexibility + * in the type of metadata that can be attached to a request, making the class + * adaptable to various needs. + * + * @author ajay.jalgaonkar + */ +@Getter +@Builder +public class RequestParams { + // IP address of the client making the request. + String clientIp; + + // Path of the resource being requested. + String resourcePath; + + // Metadata associated with the request, of generic type M. + M metadata; +} diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/ServerRequestParams.java b/core/src/main/java/com/flipkart/gjex/core/filter/ServerRequestParams.java deleted file mode 100644 index e71e151e..00000000 --- a/core/src/main/java/com/flipkart/gjex/core/filter/ServerRequestParams.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.flipkart.gjex.core.filter; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; - -/** - * Parameters for passing to filters - * @author ajay.jalgaonkar - * - */ - -@AllArgsConstructor -@Getter -@Builder -public class ServerRequestParams { - private String clientIp; - private String methodName; -} diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/grpc/AccessLogGrpcFilter.java b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/AccessLogGrpcFilter.java new file mode 100644 index 00000000..f0125ba4 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/AccessLogGrpcFilter.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.filter.grpc; + +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.logging.Logging; +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; +import lombok.Getter; +import lombok.Setter; +import org.slf4j.Logger; + +/** + * Implements a gRPC filter for logging access to gRPC services. This filter captures and logs + * essential details such as the start time of the request, the client IP, the resource path, + * and the size of the response message. + *

+ * This class extends {@link GrpcFilter} to provide specific functionality for gRPC requests and responses. + * It uses SLF4J for logging, facilitating integration with various logging frameworks. + *

+ * + * @param The request type extending {@link GeneratedMessageV3}, representing the gRPC request message. + * @param The response type extending {@link GeneratedMessageV3}, representing the gRPC response message. + * + * @author ajay.jalgaonkar + */ +public class AccessLogGrpcFilter + extends GrpcFilter implements Logging { + + // The start time of the request processing. + @Getter + @Setter + protected long startTime; + + // Parameters of the request being processed, including client IP and resource path. + protected RequestParams requestParams; + + // Logger instance for logging access log messages. + protected static Logger logger = Logging.loggerWithName("ACCESS-LOG"); + + /** + * Processes the incoming gRPC request by initializing the start time and storing the request parameters. + * + * @param req The incoming gRPC request message. + * @param requestParamsInput Parameters of the request, including client IP and any additional metadata. + */ + @Override + public void doProcessRequest(R req, RequestParams requestParamsInput) { + startTime = System.currentTimeMillis(); + requestParams = requestParamsInput; + } + + /** + * Placeholder method for processing response headers. Currently does not perform any operations. + * + * @param responseHeaders The metadata associated with the gRPC response. + */ + @Override + public void doProcessResponseHeaders(Metadata responseHeaders) {} + + /** + * Processes the outgoing gRPC response by logging relevant request and response details. + * Logs the client IP, requested resource path, size of the response message, and the time taken to process the request. + * + * @param response The outgoing gRPC response message. + */ + @Override + public void doProcessResponse(S response) { + String size = null; + if (response != null){ + size = String.valueOf(response.getSerializedSize()); + } + if (logger.isInfoEnabled()){ + logger.info("{} {} {} {}", + requestParams.getClientIp(), requestParams.getResourcePath(), size, System.currentTimeMillis()-startTime); + } + } + + /** + * Provides an instance of this filter. This method facilitates the creation of new instances of the + * AccessLogGrpcFilter for each gRPC call, ensuring thread safety and isolation of request data. + * + * @return A new instance of {@link AccessLogGrpcFilter}. + */ + @Override + public GrpcFilter getInstance(){ + return new AccessLogGrpcFilter<>(); + } +} diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/ApplicationHeaders.java b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/ApplicationHeaders.java similarity index 95% rename from core/src/main/java/com/flipkart/gjex/core/filter/ApplicationHeaders.java rename to core/src/main/java/com/flipkart/gjex/core/filter/grpc/ApplicationHeaders.java index 404773c3..3d4088e4 100644 --- a/core/src/main/java/com/flipkart/gjex/core/filter/ApplicationHeaders.java +++ b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/ApplicationHeaders.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.flipkart.gjex.core.filter; +package com.flipkart.gjex.core.filter.grpc; import com.flipkart.gjex.core.context.GJEXContext; diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/grpc/GrpcFilter.java b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/GrpcFilter.java new file mode 100644 index 00000000..ecf077c5 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/GrpcFilter.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.filter.grpc; + +import com.flipkart.gjex.core.filter.Filter; +import com.google.protobuf.GeneratedMessageV3; +import io.grpc.Metadata; + +/** + * A Filter interface for processing Request, Request-Headers, Response and Response-Headers around gRPC method invocation + * + * @param Proto V3 message + * @param Proto V3 message + * @author ajay.jalgaonkar + */ +public abstract class GrpcFilter + extends Filter { + + /** Lifecycle methods for initializing and cleaning up resources used by this Filter*/ + public void init(){} + + /** + * Function for creating an instance of this {@link Filter} + * Use only this function to get the {@link Filter} instance + */ + public abstract GrpcFilter getInstance(); + + /** + * Returns array of {@link Metadata.Key} identifiers for headers to be forwarded + * @return array of {@link Metadata.Key} + */ + @SuppressWarnings("rawtypes") + public Metadata.Key[] getForwardHeaderKeys(){ + return new Metadata.Key[] {}; + } +} diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/grpc/GrpcFilterConfig.java b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/GrpcFilterConfig.java new file mode 100644 index 00000000..67c6c850 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/GrpcFilterConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.filter.grpc; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * A gRPC Filter Config for processing filters + * + * @author ajay.jalgaonkar + */ + +@Data +public class GrpcFilterConfig { + @JsonProperty("enableAccessLogs") + private boolean enableAccessLogs = true; +} diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/MethodFilters.java b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/MethodFilters.java similarity index 85% rename from core/src/main/java/com/flipkart/gjex/core/filter/MethodFilters.java rename to core/src/main/java/com/flipkart/gjex/core/filter/grpc/MethodFilters.java index 4201c849..29a67e6e 100644 --- a/core/src/main/java/com/flipkart/gjex/core/filter/MethodFilters.java +++ b/core/src/main/java/com/flipkart/gjex/core/filter/grpc/MethodFilters.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.flipkart.gjex.core.filter; +package com.flipkart.gjex.core.filter.grpc; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -22,7 +22,7 @@ import java.lang.annotation.Target; /** - * Annotation for specifying an array of {@link Filter} instances on gRPC service methods + * Annotation for specifying an array of {@link GrpcFilter} instances on gRPC service methods * @author regu.b * */ @@ -32,5 +32,5 @@ public @interface MethodFilters { // Not parameterizing Filter here as the Language doesnot support it for Annotations @SuppressWarnings("rawtypes") - public Class[] value(); + public Class[] value(); } diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/http/AccessLogHttpFilter.java b/core/src/main/java/com/flipkart/gjex/core/filter/http/AccessLogHttpFilter.java new file mode 100644 index 00000000..be580418 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/http/AccessLogHttpFilter.java @@ -0,0 +1,79 @@ +package com.flipkart.gjex.core.filter.http; + +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.logging.Logging; +import org.slf4j.Logger; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.util.Set; + +/** + * Implements an HTTP filter for logging access requests. This filter captures and logs + * essential request and response details such as client IP, request URI, response status, + * content length, and the time taken to process the request. + *

+ * This class extends {@link HttpFilter} to provide specific logging functionality for HTTP requests. + * It uses SLF4J for logging, allowing for integration with various logging frameworks. + *

+ * + * @author ajay.jalgaonkar + */ +public class AccessLogHttpFilter extends HttpFilter implements Logging { + + // Time when the request processing started. + protected long startTime; + + // Parameters of the request being processed. + protected RequestParams> requestParams; + + + // Logger instance for logging access log messages. + protected static Logger logger = Logging.loggerWithName("ACCESS-LOG"); + + // HTTP header name for content length. + protected static final String CONTENT_LENGTH_HEADER = "Content-Length"; + + + public AccessLogHttpFilter() { + } + + @Override + public HttpFilter getInstance() { + return new AccessLogHttpFilter(); + } + + /** + * Processes the incoming request by initializing the start time and storing the request parameters. + * + * @param req The incoming servlet request. + * @param requestParamsInput Parameters of the request, including client IP and any additional metadata. + */ + @Override + public void doProcessRequest(ServletRequest req, RequestParams> requestParamsInput) { + startTime = System.currentTimeMillis(); + requestParams = requestParamsInput; + } + + /** + * Processes the outgoing response by logging relevant request and response details. + * Logs the client IP, requested URI, response status, content length, and the time taken to process the request. + * + * @param response The outgoing servlet response. + */ + @Override + public void doProcessResponse(ServletResponse response) { + if (logger.isInfoEnabled()) { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + logger.info("{} {} {} {} {}", + requestParams.getClientIp(), + requestParams.getResourcePath(), + httpServletResponse.getStatus(), + httpServletResponse.getHeader(CONTENT_LENGTH_HEADER), + System.currentTimeMillis() - startTime + ); + } + } + +} diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilter.java b/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilter.java new file mode 100644 index 00000000..a548e734 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilter.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.filter.http; + +import com.flipkart.gjex.core.filter.Filter; +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Abstract base class for HTTP filters that process requests and responses around HTTP method invocations. + * This class provides a framework for capturing and manipulating HTTP request and response objects, + * including headers and other metadata. It extends the generic {@link Filter} interface to work specifically + * with HTTP requests and responses. + *

+ * Implementations of this class should provide specific processing logic by overriding the + * {@link #doProcessRequest(ServletRequest, RequestParams)} and {@link #doProcessResponse(ServletResponse)} + * methods. + *

+ * + * @param The ServletRequest object that contains the client's request + * @param The ServletResponse object that contains the filter's response + * @param > The type of metadata associated with the request, typically a set of header names + * @author ajay.jalgaonkar + */ +public abstract class HttpFilter extends Filter> { + + /** + * Function for creating an instance of this {@link Filter} + * Use only this function to get the {@link Filter} instance + */ + public abstract HttpFilter getInstance(); + +} \ No newline at end of file diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilterConfig.java b/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilterConfig.java new file mode 100644 index 00000000..1b962ab1 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilterConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.filter.http; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * An HTTP Filter Config for processing filters + * + * @author ajay.jalgaonkar + */ + +@Data +public class HttpFilterConfig { + @JsonProperty("enableAccessLogs") + private boolean enableAccessLogs = true; +} diff --git a/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilterParams.java b/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilterParams.java new file mode 100644 index 00000000..3ae7efde --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/filter/http/HttpFilterParams.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.filter.http; + +import lombok.Builder; +import lombok.Data; + +/** + * Encapsulates the parameters necessary for creating HTTP filters within the GJEX framework. + * This class serves as a data holder for filter configurations, including the filter instance itself + * and the path specification to which the filter applies. + *

+ * The {@code filter} field holds an instance of a class implementing the {@link javax.servlet.Filter} interface, + * enabling the interception and processing of requests and responses in the web application. + * The {@code pathSpec} field specifies the URL pattern(s) that the filter will be applied to, allowing for + * targeted filtering based on request paths. + *

+ * + * @author ajay.jalgaonkar + */ +@Data +@Builder +public class HttpFilterParams { + // The filter instance to be applied. + private final HttpFilter filter; + + // The URL pattern(s) the filter applies to. + private final String pathSpec; +} \ No newline at end of file diff --git a/core/src/main/java/com/flipkart/gjex/core/logging/Logging.java b/core/src/main/java/com/flipkart/gjex/core/logging/Logging.java index 5caa4922..a8d6d42c 100644 --- a/core/src/main/java/com/flipkart/gjex/core/logging/Logging.java +++ b/core/src/main/java/com/flipkart/gjex/core/logging/Logging.java @@ -16,15 +16,14 @@ package com.flipkart.gjex.core.logging; +import com.flipkart.gjex.Constants; +import com.flipkart.gjex.core.context.GJEXContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.slf4j.helpers.FormattingTuple; import org.slf4j.helpers.MessageFormatter; -import com.flipkart.gjex.Constants; -import com.flipkart.gjex.core.context.GJEXContext; - /** * Convenience logging implementation with default behavior for use by classes in GJEX runtime and applications. * @@ -40,6 +39,10 @@ default String msgWithLogIdent(String msg) { else return logId + msg; } + static Logger loggerWithName(String loggerName){ + return LoggerFactory.getLogger(loggerName); + } + default String getLoggerName() { return this.getClass().getCanonicalName();} default Logger logger() { diff --git a/core/src/main/java/com/flipkart/gjex/core/setup/Bootstrap.java b/core/src/main/java/com/flipkart/gjex/core/setup/Bootstrap.java index 6fa27219..34e5e165 100644 --- a/core/src/main/java/com/flipkart/gjex/core/setup/Bootstrap.java +++ b/core/src/main/java/com/flipkart/gjex/core/setup/Bootstrap.java @@ -15,23 +15,6 @@ */ package com.flipkart.gjex.core.setup; -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; - -import com.flipkart.gjex.core.healthcheck.HealthCheckRegistry; -import io.dropwizard.metrics5.MetricRegistry; -import io.dropwizard.metrics5.jmx.JmxReporter; -import io.dropwizard.metrics5.jvm.BufferPoolMetricSet; -import io.dropwizard.metrics5.jvm.GarbageCollectorMetricSet; -import io.dropwizard.metrics5.jvm.MemoryUsageGaugeSet; -import io.dropwizard.metrics5.jvm.ThreadStatesGaugeSet; import com.fasterxml.jackson.databind.ObjectMapper; import com.flipkart.gjex.core.Application; import com.flipkart.gjex.core.Bundle; @@ -44,7 +27,8 @@ import com.flipkart.gjex.core.config.ConfigurationSourceProvider; import com.flipkart.gjex.core.config.DefaultConfigurationFactoryFactory; import com.flipkart.gjex.core.config.FileConfigurationSourceProvider; -import com.flipkart.gjex.core.filter.Filter; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; +import com.flipkart.gjex.core.healthcheck.HealthCheckRegistry; import com.flipkart.gjex.core.job.ScheduledJob; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.service.Service; @@ -52,8 +36,17 @@ import com.flipkart.gjex.core.util.Pair; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; +import io.dropwizard.metrics5.MetricRegistry; import io.prometheus.metrics.model.registry.PrometheusRegistry; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + /** * The pre-start application container, containing services required to bootstrap a GJEX application * @@ -84,7 +77,7 @@ public class Bootstrap implements Lo private List services; /** List of initialized Filter instances*/ - private List filters; + private List grpcFilters; /** List of initialized ConfigurableTracingSampler instances*/ private List tracingSamplers; @@ -162,8 +155,8 @@ public List getServices() { return services; } - public List getFilters() { - return filters; + public List getFilters() { + return grpcFilters; } public List getTracingSamplers() { @@ -225,17 +218,17 @@ public void setConfigMap(U configMap) { */ public void run(Environment environment) throws Exception { // Identify all Service implementations, start them and register for Runtime shutdown hook - services = new LinkedList(); - filters = new LinkedList(); - tracingSamplers = new LinkedList(); - scheduledJobs = new LinkedList(); + services = new ArrayList<>(); + grpcFilters = new ArrayList(); + tracingSamplers = new ArrayList(); + scheduledJobs = new ArrayList(); // Set the HealthCheckRegsitry to the one initialized by the Environment healthCheckRegistry = environment.getHealthCheckRegistry(); for (Bundle bundle : bundles) { bundle.run(configuration, configMap, environment); services.addAll(bundle.getServices()); - filters.addAll(bundle.getFilters()); + grpcFilters.addAll(bundle.getGrpcFilters()); tracingSamplers.addAll(bundle.getTracingSamplers()); scheduledJobs.addAll(bundle.getScheduledJobs()); // Register all HealthChecks with the HealthCheckRegistry @@ -249,7 +242,7 @@ public void run(Environment environment) throws Exception { throw new RuntimeException(e); } }); - filters.forEach(filter -> { + grpcFilters.forEach(filter -> { try { filter.init(); } catch (Exception e) { @@ -271,7 +264,7 @@ public void run() { // Use stdout here since the logger may have been reset by its JVM shutdown hook. System.out.println("*** Shutting down GJEX server since JVM is shutting down"); services.forEach(Service::stop); - filters.forEach(Filter::destroy); + grpcFilters.forEach(GrpcFilter::destroy); System.out.println("*** Server shut down"); } }); diff --git a/core/src/main/java/com/flipkart/gjex/core/web/DashboardHealthCheckResource.java b/core/src/main/java/com/flipkart/gjex/core/web/DashboardHealthCheckResource.java new file mode 100644 index 00000000..9362d5e1 --- /dev/null +++ b/core/src/main/java/com/flipkart/gjex/core/web/DashboardHealthCheckResource.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) The original author or 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 + * + * http://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.flipkart.gjex.core.web; + +import com.flipkart.gjex.core.healthcheck.HealthCheckRegistry; +import io.dropwizard.metrics5.health.HealthCheck; + +import javax.inject.Named; +import javax.inject.Singleton; +import javax.servlet.ServletContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.SortedMap; + +/** + * Servlet Resource for the Dashboard HealthCheck API + * @author ajay.jalgaonkar + * + */ + +@Singleton +@Path("/") +@Named("DashboardHealthCheckResource") +public class DashboardHealthCheckResource { + + @Context + private ServletContext servletContext; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response performHealthChecks() { + HealthCheckRegistry registry = (HealthCheckRegistry) servletContext + .getAttribute(HealthCheckRegistry.HEALTHCHECK_REGISTRY_NAME); + SortedMap results = registry.runHealthChecks(); + return Response.status(Response.Status.OK).entity(results).build(); + } + +} diff --git a/core/src/main/java/com/flipkart/gjex/core/web/HealthCheckResource.java b/core/src/main/java/com/flipkart/gjex/core/web/HealthCheckResource.java index 904ce8dc..7671a199 100644 --- a/core/src/main/java/com/flipkart/gjex/core/web/HealthCheckResource.java +++ b/core/src/main/java/com/flipkart/gjex/core/web/HealthCheckResource.java @@ -15,7 +15,8 @@ */ package com.flipkart.gjex.core.web; -import java.util.SortedMap; +import com.flipkart.gjex.core.healthcheck.HealthCheckRegistry; +import io.dropwizard.metrics5.health.HealthCheck; import javax.inject.Named; import javax.inject.Singleton; @@ -26,9 +27,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - -import io.dropwizard.metrics5.health.HealthCheck; -import com.flipkart.gjex.core.healthcheck.HealthCheckRegistry; +import java.util.SortedMap; /** * Servlet Resource for the HealthCheck API @@ -38,7 +37,7 @@ @Singleton @Path("/") -@Named +@Named("HealthCheckResource") public class HealthCheckResource { @Context diff --git a/examples/build.gradle b/examples/build.gradle index 756a8712..c67df496 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -48,6 +48,7 @@ dependencies { implementation 'org.projectlombok:lombok:1.18.30' implementation 'org.glassfish.jersey.containers:jersey-container-servlet:2.6' implementation 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:2.9.7' + implementation 'javax.servlet:javax.servlet-api:3.1.0' } protobuf { diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/AuthFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/AuthFilter.java index 4057b626..ff63f589 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/AuthFilter.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/AuthFilter.java @@ -15,27 +15,26 @@ */ package com.flipkart.gjex.examples.helloworld.filter; -import javax.inject.Named; - -import com.flipkart.gjex.core.filter.Filter; -import com.flipkart.gjex.core.filter.ServerRequestParams; +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; import com.flipkart.gjex.core.logging.Logging; - import io.grpc.Metadata; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.examples.helloworld.HelloReply; import io.grpc.examples.helloworld.HelloRequest; +import javax.inject.Named; + /** - * An implementation of the {@link Filter} interface as example that performs naive authentication based on + * An implementation of the {@link GrpcFilter} interface as example that performs naive authentication based on * information contained in the Request headers * * @author regu.b * */ @Named("AuthFilter") -public class AuthFilter implements Filter, Logging { +public class AuthFilter extends GrpcFilter implements Logging { /** Fictitious authentication key*/ @SuppressWarnings("rawtypes") @@ -45,16 +44,16 @@ public class AuthFilter implements Filter, Logging { private final boolean isAuth = false; @Override - public Filter getInstance(){ + public GrpcFilter getInstance(){ return new AuthFilter(); } @Override - public void doFilterRequest(ServerRequestParams serverRequestParams, Metadata requestHeaders) throws StatusRuntimeException { - info("Headers found in the request : " + requestHeaders.toString()); - this.checkAuth(requestHeaders); + public void doProcessRequest(HelloRequest request, RequestParams requestParams) throws StatusRuntimeException { + info("Headers found in the request : " + requestParams.getMetadata().toString()); + this.checkAuth(requestParams.getMetadata()); } - + @SuppressWarnings("rawtypes") @Override public Metadata.Key[] getForwardHeaderKeys() { diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/LoggingFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/LoggingFilter.java index 54836553..3b90c347 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/LoggingFilter.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/filter/LoggingFilter.java @@ -15,7 +15,8 @@ */ package com.flipkart.gjex.examples.helloworld.filter; -import com.flipkart.gjex.core.filter.Filter; +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; import com.flipkart.gjex.core.logging.Logging; import com.google.protobuf.GeneratedMessageV3; import io.grpc.Metadata; @@ -23,23 +24,23 @@ import javax.inject.Named; /** - * An implementation of the {@link Filter} interface as example that simply logs Request information + * An implementation of the {@link GrpcFilter} interface as example that simply logs Request information * @author regu.b */ @Named("LoggingFilter") -public class LoggingFilter implements Filter, Logging { +public class LoggingFilter extends GrpcFilter implements Logging { /** Custom response key to indicate request was logged on the server*/ static final Metadata.Key CUSTOM_HEADER_KEY = Metadata.Key.of("request_response_logged_header_key", Metadata.ASCII_STRING_MARSHALLER); @Override - public Filter getInstance(){ + public GrpcFilter getInstance(){ return new LoggingFilter<>(); } @Override - public void doProcessRequest(Req request) { - info("Logging from filter. Request payload is : " + request.toString()); + public void doProcessRequest(Req req, RequestParams requestParams) { + info("Logging from filter. Request payload is : " + req.toString()); } @Override @@ -51,5 +52,4 @@ public void doProcessResponseHeaders(Metadata responseHeaders) { public void doProcessResponse(Res response) { info("Logging from filter. Response payload is : " + response.toString()); } - } diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java index 96993c3c..598f18f2 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/guice/HelloWorldModule.java @@ -15,13 +15,15 @@ */ package com.flipkart.gjex.examples.helloworld.guice; -import com.flipkart.gjex.core.filter.Filter; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; +import com.flipkart.gjex.core.filter.http.HttpFilterParams; import com.flipkart.gjex.core.tracing.TracingSampler; import com.flipkart.gjex.examples.helloworld.filter.AuthFilter; import com.flipkart.gjex.examples.helloworld.filter.LoggingFilter; import com.flipkart.gjex.examples.helloworld.service.GreeterService; import com.flipkart.gjex.examples.helloworld.tracing.AllWhitelistTracingSampler; import com.flipkart.gjex.examples.helloworld.web.HelloWorldResourceConfig; +import com.flipkart.gjex.examples.helloworld.web.httpfilter.ExampleHttpFilter; import com.google.inject.AbstractModule; import com.google.inject.name.Names; import io.grpc.BindableService; @@ -47,10 +49,11 @@ protected void configure() { // install(new ClientModule(GreeterGrpc.GreeterBlockingStub.class,new ChannelConfig("localhost",9999))); bind(GreeterGrpc.GreeterBlockingStub.class).toInstance(GreeterGrpc.newBlockingStub(channel)); bind(BindableService.class).annotatedWith(Names.named("GreeterService")).to(GreeterService.class); - bind(Filter.class).annotatedWith(Names.named("LoggingFilter")).to(LoggingFilter.class); - bind(Filter.class).annotatedWith(Names.named("AuthFilter")).to(AuthFilter.class); + bind(GrpcFilter.class).annotatedWith(Names.named("LoggingFilter")).to(LoggingFilter.class); + bind(GrpcFilter.class).annotatedWith(Names.named("AuthFilter")).to(AuthFilter.class); bind(TracingSampler.class).to(AllWhitelistTracingSampler.class); bind(ResourceConfig.class).annotatedWith(Names.named("HelloWorldResourceConfig")).to(HelloWorldResourceConfig.class); + bind(HttpFilterParams.class).annotatedWith(Names.named("ExampleHttpFilterParams")) + .toInstance(HttpFilterParams.builder().filter(new ExampleHttpFilter()).pathSpec("/*").build()); } - } diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/GreeterService.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/GreeterService.java index 923e4cfb..7c16f80a 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/GreeterService.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/GreeterService.java @@ -18,15 +18,15 @@ import javax.inject.Inject; import javax.inject.Named; +import com.flipkart.gjex.examples.helloworld.filter.AuthFilter; import io.dropwizard.metrics5.annotation.Timed; -import com.flipkart.gjex.core.filter.ApplicationHeaders; -import com.flipkart.gjex.core.filter.MethodFilters; +import com.flipkart.gjex.core.filter.grpc.ApplicationHeaders; +import com.flipkart.gjex.core.filter.grpc.MethodFilters; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.service.Api; import com.flipkart.gjex.core.task.TaskException; import com.flipkart.gjex.core.tracing.Traced; import com.flipkart.gjex.examples.helloworld.bean.HelloBean; -import com.flipkart.gjex.examples.helloworld.filter.AuthFilter; import com.flipkart.gjex.examples.helloworld.filter.LoggingFilter; import io.grpc.Metadata; @@ -36,8 +36,6 @@ import io.grpc.examples.helloworld.*; import io.grpc.stub.StreamObserver; -import static io.grpc.examples.helloworld.GreeterGrpc.getPingPongMethod; - /** * Sample Grpc service implementation that leverages GJEX features diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/HelloBeanService.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/HelloBeanService.java index 350a89dc..d5668515 100644 --- a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/HelloBeanService.java +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/service/HelloBeanService.java @@ -20,7 +20,7 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; -import com.flipkart.gjex.core.filter.ApplicationHeaders; +import com.flipkart.gjex.core.filter.grpc.ApplicationHeaders; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.task.AsyncResult; import com.flipkart.gjex.core.task.ConcurrentTask; diff --git a/examples/src/main/java/com/flipkart/gjex/examples/helloworld/web/httpfilter/ExampleHttpFilter.java b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/web/httpfilter/ExampleHttpFilter.java new file mode 100644 index 00000000..500c43b0 --- /dev/null +++ b/examples/src/main/java/com/flipkart/gjex/examples/helloworld/web/httpfilter/ExampleHttpFilter.java @@ -0,0 +1,34 @@ +package com.flipkart.gjex.examples.helloworld.web.httpfilter; + +import com.flipkart.gjex.core.filter.http.AccessLogHttpFilter; +import com.flipkart.gjex.core.filter.http.HttpFilter; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Example filter extending {@link com.flipkart.gjex.core.filter.grpc.AccessLogGrpcFilter} + * @author ajay.jalgaonkar + */ +public class ExampleHttpFilter extends AccessLogHttpFilter { + + @Override + public HttpFilter getInstance() { + return new ExampleHttpFilter(); + } + + @Override + public void doProcessResponse(ServletResponse response) { + if (logger.isInfoEnabled()){ + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + logger.info("{} {} {} {} {} {}", + this.getClass().getSimpleName(), + requestParams.getClientIp(), + requestParams.getResourcePath(), + httpServletResponse.getStatus(), + httpServletResponse.getHeader(CONTENT_LENGTH_HEADER), + System.currentTimeMillis()-startTime + ); + } + } +} diff --git a/examples/src/main/resources/hello_world_config.yml b/examples/src/main/resources/hello_world_config.yml index 9e6efc6b..008f57f6 100644 --- a/examples/src/main/resources/hello_world_config.yml +++ b/examples/src/main/resources/hello_world_config.yml @@ -1,6 +1,8 @@ Grpc: server.port: 50051 server.executorThreads : 4 + filterConfig: + enableAccessLogs: true Dashboard: service.port: 9999 @@ -14,6 +16,8 @@ Api: service.selectors: 10 service.workers: 30 scheduledexecutor.threadpool.size: 1 + filterConfig: + enableAccessLogs: true Tracing: collector.endpoint: http://localhost:9411/api/v2/spans @@ -29,4 +33,3 @@ apiProperties: taskProperties: hello.timeout: 200 - diff --git a/examples/src/main/resources/log4j2.xml b/examples/src/main/resources/log4j2.xml index 9c472afd..bd11c5c1 100644 --- a/examples/src/main/resources/log4j2.xml +++ b/examples/src/main/resources/log4j2.xml @@ -7,8 +7,14 @@ + + + + + + diff --git a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/CreateLoggingFilter.java b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/CreateLoggingFilter.java index cd0d07b2..0a851968 100644 --- a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/CreateLoggingFilter.java +++ b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/CreateLoggingFilter.java @@ -16,40 +16,36 @@ package com.flipkart.grpc.jexpress.filter; -import com.flipkart.gjex.core.filter.Filter; -import com.flipkart.gjex.core.logging.Logging; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; import com.flipkart.grpc.jexpress.CreateRequest; import com.flipkart.grpc.jexpress.CreateResponse; -import com.flipkart.grpc.jexpress.GetRequest; -import com.flipkart.grpc.jexpress.GetResponse; import com.google.protobuf.GeneratedMessageV3; +import com.flipkart.gjex.core.filter.RequestParams; import io.grpc.Metadata; +import com.flipkart.gjex.core.logging.Logging; import javax.inject.Named; @Named("CreateLoggingFilter") -public class CreateLoggingFilter implements Filter, Logging { - - public CreateLoggingFilter(){} +public class CreateLoggingFilter + extends GrpcFilter implements Logging { @Override - public Filter getInstance(){ + public GrpcFilter getInstance(){ return new CreateLoggingFilter(); } @Override - public void doProcessRequest(CreateRequest request) { + public void doProcessRequest(CreateRequest request, RequestParams requestParams) { info("Request: " + request); } @Override - public void doProcessResponseHeaders(Metadata reponseHeaders) { - } + public void doProcessResponseHeaders(Metadata responseHeaders) {} @Override public void doProcessResponse(CreateResponse response) { - info("Response:"); + info("Response: " + response); } } diff --git a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/GetLoggingFilter.java b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/GetLoggingFilter.java index f8948557..fb420f53 100644 --- a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/GetLoggingFilter.java +++ b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/filter/GetLoggingFilter.java @@ -15,27 +15,29 @@ */ package com.flipkart.grpc.jexpress.filter; -import com.flipkart.gjex.core.filter.Filter; -import com.flipkart.gjex.core.logging.Logging; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; import com.flipkart.grpc.jexpress.GetRequest; import com.flipkart.grpc.jexpress.GetResponse; import com.google.protobuf.GeneratedMessageV3; import io.grpc.Metadata; +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.logging.Logging; import javax.inject.Named; @Named("GetLoggingFilter") -public class GetLoggingFilter implements Filter, Logging { +public class GetLoggingFilter extends GrpcFilter implements Logging { public GetLoggingFilter(){} @Override - public Filter getInstance(){ + public GrpcFilter getInstance(){ return new GetLoggingFilter(); } @Override - public void doProcessRequest(GetRequest request) { + public void doProcessRequest(GetRequest request, RequestParams requestParams) { info("Request: " + request); } @@ -45,7 +47,7 @@ public void doProcessResponseHeaders(Metadata reponseHeaders) { @Override public void doProcessResponse(GetResponse response) { - info("Response:"); + info("Response: " + response); } } diff --git a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/module/SampleModule.java b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/module/SampleModule.java index 62d29f93..6946c902 100644 --- a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/module/SampleModule.java +++ b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/module/SampleModule.java @@ -11,14 +11,15 @@ import com.google.inject.AbstractModule; import com.google.inject.name.Names; import io.grpc.BindableService; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; public class SampleModule extends AbstractModule { @Override protected void configure() { bind(BindableService.class).annotatedWith(Names.named("SampleService")).to(SampleService.class); bind(HealthCheck.class).to(AllIsWellHealthCheck.class); - bind(Filter.class).annotatedWith(Names.named("GetLoggingFilter")).to(GetLoggingFilter.class); - bind(Filter.class).annotatedWith(Names.named("CreateLoggingFilter")).to(CreateLoggingFilter.class); + bind(GrpcFilter.class).annotatedWith(Names.named("GetLoggingFilter")).to(GetLoggingFilter.class); + bind(GrpcFilter.class).annotatedWith(Names.named("CreateLoggingFilter")).to(CreateLoggingFilter.class); bind(TracingSampler.class).to(AllWhitelistTracingSampler.class); } } diff --git a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/service/SampleService.java b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/service/SampleService.java index 94e83e33..5cf9d3af 100644 --- a/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/service/SampleService.java +++ b/grpc-jexpress-template/src/main/java/com/flipkart/grpc/jexpress/service/SampleService.java @@ -1,6 +1,6 @@ package com.flipkart.grpc.jexpress.service; -import com.flipkart.gjex.core.filter.MethodFilters; +import com.flipkart.gjex.core.filter.grpc.MethodFilters; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.grpc.jexpress.*; import com.flipkart.grpc.jexpress.filter.CreateLoggingFilter; diff --git a/guice/build.gradle b/guice/build.gradle index 2ee4a992..13e851b7 100644 --- a/guice/build.gradle +++ b/guice/build.gradle @@ -68,6 +68,9 @@ dependencies { implementation 'io.zipkin.reporter2:zipkin-sender-okhttp3:2.7.7' implementation 'io.prometheus:prometheus-metrics-exporter-servlet-javax:1.2.0' + + testImplementation libraries.junit4 + testImplementation libraries.assertj } task sourcesJar(type: Jar, dependsOn: classes) { diff --git a/guice/src/main/java/com/flipkart/gjex/grpc/interceptor/FilterInterceptor.java b/guice/src/main/java/com/flipkart/gjex/grpc/interceptor/FilterInterceptor.java index 4a957a56..d7506871 100644 --- a/guice/src/main/java/com/flipkart/gjex/grpc/interceptor/FilterInterceptor.java +++ b/guice/src/main/java/com/flipkart/gjex/grpc/interceptor/FilterInterceptor.java @@ -16,13 +16,14 @@ package com.flipkart.gjex.grpc.interceptor; import com.flipkart.gjex.core.context.GJEXContext; -import com.flipkart.gjex.core.filter.Filter; -import com.flipkart.gjex.core.filter.MethodFilters; -import com.flipkart.gjex.core.filter.ServerRequestParams; +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; +import com.flipkart.gjex.core.filter.grpc.AccessLogGrpcFilter; +import com.flipkart.gjex.core.filter.grpc.GrpcFilterConfig; +import com.flipkart.gjex.core.filter.grpc.MethodFilters; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.util.Pair; import com.flipkart.gjex.grpc.utils.AnnotationUtils; -import com.google.protobuf.GeneratedMessageV3; import io.grpc.BindableService; import io.grpc.Context; import io.grpc.ForwardingServerCall.SimpleForwardingServerCall; @@ -34,23 +35,21 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; -import io.grpc.StatusRuntimeException; import javax.inject.Named; import javax.inject.Singleton; import javax.validation.ConstraintViolationException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; /** - * An implementation of the gRPC {@link ServerInterceptor} that allows custom {@link Filter} instances to be invoked around relevant methods to process Request, Request-Headers, Response and + * An implementation of the gRPC {@link ServerInterceptor} that allows custom {@link GrpcFilter} instances to be invoked around relevant methods to process Request, Request-Headers, Response and * Response-Headers data. * * @author regu.b @@ -63,17 +62,19 @@ public class FilterInterceptor implements ServerInterceptor, Logging { * Map of Filter instances mapped to Service and its method */ @SuppressWarnings("rawtypes") - private Map> filtersMap = new HashMap>(); + private final Map> filtersMap = new HashMap<>(); @SuppressWarnings("rawtypes") - public void registerFilters(List filters, List services) { - Map, Filter> classToInstanceMap = filters.stream() + public void registerFilters(List grpcFilters, List services, + GrpcFilterConfig grpcFilterConfig) { + Map, GrpcFilter> classToInstanceMap = grpcFilters.stream() .collect(Collectors.toMap(Object::getClass, Function.identity())); services.forEach(service -> { List> annotatedMethods = AnnotationUtils.getAnnotatedMethods(service.getClass(), MethodFilters.class); if (annotatedMethods != null) { annotatedMethods.forEach(pair -> { - List filtersForMethod = new LinkedList(); + List filtersForMethod = new ArrayList<>(); + configureAccessLog(grpcFilterConfig, filtersForMethod); Arrays.asList(pair.getValue().getAnnotation(MethodFilters.class).value()).forEach(filterClass -> { if (!classToInstanceMap.containsKey(filterClass)) { throw new RuntimeException("Filter instance not bound for Filter class :" + filterClass.getName()); @@ -82,8 +83,9 @@ public void registerFilters(List filters, List services }); // Key is of the form + "/" + // reflecting the structure followed in the gRPC HandlerRegistry using MethodDescriptor#getFullMethodName() - filtersMap.put((service.bindService().getServiceDescriptor().getName() + "/" + pair.getValue().getName()).toLowerCase(), - filtersForMethod); + String methodSignature = + (service.bindService().getServiceDescriptor().getName() + "/" + pair.getValue().getName()).toLowerCase(); + filtersMap.put(methodSignature, filtersForMethod); }); } }); @@ -91,49 +93,26 @@ public void registerFilters(List filters, List services @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { - List filterReferences = + public Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + List grpcFilterReferences = filtersMap.get(call.getMethodDescriptor().getFullMethodName().toLowerCase()); Metadata forwardHeaders = new Metadata(); - if (filterReferences == null || filterReferences.isEmpty()){ - return new SimpleForwardingServerCallListener(next.startCall( - new SimpleForwardingServerCall(call) { + if (grpcFilterReferences == null || grpcFilterReferences.isEmpty()){ + return new SimpleForwardingServerCallListener(next.startCall( + new SimpleForwardingServerCall(call) { }, headers)) { }; } - List filters = filterReferences.stream().map(Filter::getInstance).collect(Collectors.toList()); - for (Filter filter : filters) { - try { - if (filter != null) { - ServerRequestParams serverRequestParams = - ServerRequestParams.builder() - .clientIp(call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR).toString()) - .methodName(call.getMethodDescriptor().getFullMethodName().toLowerCase()) - .build(); - filter.doFilterRequest(serverRequestParams, headers); - for (Metadata.Key key : filter.getForwardHeaderKeys()) { - Object value = headers.get(key); - if (value != null) { - forwardHeaders.put(key, value); - } - } - } - } catch (StatusRuntimeException se) { - call.close(se.getStatus(), se.getTrailers()); // Closing the call and not letting it to proceed further - return new ServerCall.Listener() { - }; - } - } - + List grpcFilters = grpcFilterReferences.stream().map(GrpcFilter::getInstance).collect(Collectors.toList()); Context contextWithHeaders = forwardHeaders.keys().isEmpty() ? null : Context.current().withValue(GJEXContext.getHeadersKey(), forwardHeaders); - ServerCall.Listener listener = null; - listener = next.startCall(new SimpleForwardingServerCall(call) { + ServerCall.Listener listener = null; + listener = next.startCall(new SimpleForwardingServerCall(call) { @Override - public void sendMessage(final RespT response) { + public void sendMessage(final Res response) { Context previous = attachContext(contextWithHeaders); // attaching headers to gRPC context try { - filters.forEach(filter -> filter.doProcessResponse((GeneratedMessageV3) response)); + grpcFilters.forEach(filter -> filter.doProcessResponse(response)); super.sendMessage(response); } finally { detachContext(contextWithHeaders, previous); // detach headers from gRPC context @@ -144,7 +123,7 @@ public void sendMessage(final RespT response) { public void sendHeaders(final Metadata responseHeaders) { Context previous = attachContext(contextWithHeaders); // attaching headers to gRPC context try { - filters.forEach(filter -> filter.doProcessResponseHeaders(responseHeaders)); + grpcFilters.forEach(filter -> filter.doProcessResponseHeaders(responseHeaders)); super.sendHeaders(responseHeaders); } finally { detachContext(contextWithHeaders, previous); // detach headers from gRPC context @@ -152,7 +131,7 @@ public void sendHeaders(final Metadata responseHeaders) { } }, headers); - return new SimpleForwardingServerCallListener(listener) { + return new SimpleForwardingServerCallListener(listener) { @Override public void onHalfClose() { Context previous = attachContext(contextWithHeaders); // attaching headers to gRPC context @@ -166,10 +145,25 @@ public void onHalfClose() { } @Override - public void onMessage(ReqT request) { + public void onMessage(Req request) { Context previous = attachContext(contextWithHeaders); // attaching headers to gRPC context + RequestParams requestParams = RequestParams.builder() + .clientIp(call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR).toString()) + .resourcePath(call.getMethodDescriptor().getFullMethodName().toLowerCase()) + .metadata(headers) + .build(); try { - filters.forEach(filter -> filter.doProcessRequest((GeneratedMessageV3) request)); + for (GrpcFilter grpcFilter : grpcFilters) { + if (grpcFilter != null) { + grpcFilters.forEach(filter -> filter.doProcessRequest(request,requestParams)); + for (Metadata.Key key : grpcFilter.getForwardHeaderKeys()) { + Object value = headers.get(key); + if (value != null) { + forwardHeaders.put(key, value); + } + } + } + } super.onMessage(request); } finally { detachContext(contextWithHeaders, previous); // detach headers from gRPC context @@ -193,7 +187,7 @@ public void onCancel() { /** * Helper method to handle RuntimeExceptions and convert it into suitable gRPC message. Closes the ServerCall */ - private void handleException(ServerCall call, Exception e) { + private void handleException(ServerCall call, Exception e) { error("Closing gRPC call due to RuntimeException.", e); Status returnStatus = Status.INTERNAL; if (ConstraintViolationException.class.isAssignableFrom(e.getClass())) { @@ -220,4 +214,10 @@ private void detachContext(Context currentContext, Context previousContext) { } } + private void configureAccessLog(GrpcFilterConfig grpcFilterConfig, + @SuppressWarnings("rawtypes") List filtersForMethod){ + if (grpcFilterConfig.isEnableAccessLogs()){ + filtersForMethod.add(new AccessLogGrpcFilter<>()); + } + } } diff --git a/guice/src/main/java/com/flipkart/gjex/grpc/service/ApiServer.java b/guice/src/main/java/com/flipkart/gjex/grpc/service/ApiServer.java index 61d05163..3849e177 100644 --- a/guice/src/main/java/com/flipkart/gjex/grpc/service/ApiServer.java +++ b/guice/src/main/java/com/flipkart/gjex/grpc/service/ApiServer.java @@ -15,20 +15,26 @@ */ package com.flipkart.gjex.grpc.service; -import java.util.LinkedList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import org.eclipse.jetty.server.Server; -import org.glassfish.jersey.server.ResourceConfig; - +import com.flipkart.gjex.core.filter.http.AccessLogHttpFilter; +import com.flipkart.gjex.core.filter.http.HttpFilterConfig; +import com.flipkart.gjex.core.filter.http.HttpFilterParams; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.service.AbstractService; import com.flipkart.gjex.core.service.Service; +import com.flipkart.gjex.http.interceptor.HttpFilterInterceptor; import com.flipkart.gjex.web.ResourceRegistrar; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.glassfish.jersey.server.ResourceConfig; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.servlet.DispatcherType; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; /** * ApiServer is a {@link Service} implementation that manages the GJEX API Jetty Server instance lifecycle @@ -40,20 +46,39 @@ @Named("APIServer") public class ApiServer extends AbstractService implements Logging { - private final Server apiServer; - private final ResourceRegistrar resourceRegistrar; - private List resourceConfigs = new LinkedList(); + private final Server apiServer; + private final ResourceRegistrar resourceRegistrar; + private final ServletContextHandler context; + private HttpFilterInterceptor httpFilterInterceptor; + private List resourceConfigs = new ArrayList<>(); @Inject - public ApiServer(@Named("APIJettyServer") Server apiServer, ResourceRegistrar resourceRegistrar) { + public ApiServer(@Named("APIJettyServer") Server apiServer, + @Named("ApiServletContext") ServletContextHandler context, + @Named("HttpFilterInterceptor") HttpFilterInterceptor httpFilterInterceptor, + ResourceRegistrar resourceRegistrar) { this.apiServer = apiServer; + this.context = context; + this.httpFilterInterceptor = httpFilterInterceptor; this.resourceRegistrar = resourceRegistrar; } public void registerResources(List resourceConfigs) { this.resourceConfigs.addAll(resourceConfigs); } - + + public void registerFilters(List httpFilterParamsList, HttpFilterConfig httpFilterConfig){ + configureAccessLog(httpFilterParamsList, httpFilterConfig); + httpFilterInterceptor.registerFilters(httpFilterParamsList); + context.addFilter(new FilterHolder(httpFilterInterceptor), "/*", EnumSet.of(DispatcherType.REQUEST)); + } + + private void configureAccessLog(List httpFilterParamsList, HttpFilterConfig httpFilterConfig){ + if (httpFilterConfig.isEnableAccessLogs()){ + httpFilterParamsList.add(0, HttpFilterParams.builder().filter(new AccessLogHttpFilter()).pathSpec("/*").build()); + } + } + @Override public void doStart() throws Exception { this.resourceRegistrar.registerResources(this.resourceConfigs); // register any custom web resources added by the GJEX application diff --git a/guice/src/main/java/com/flipkart/gjex/grpc/service/GrpcServer.java b/guice/src/main/java/com/flipkart/gjex/grpc/service/GrpcServer.java index b39c9d3f..c0690cee 100644 --- a/guice/src/main/java/com/flipkart/gjex/grpc/service/GrpcServer.java +++ b/guice/src/main/java/com/flipkart/gjex/grpc/service/GrpcServer.java @@ -16,7 +16,8 @@ package com.flipkart.gjex.grpc.service; import com.flipkart.gjex.core.GJEXConfiguration; -import com.flipkart.gjex.core.filter.Filter; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; +import com.flipkart.gjex.core.filter.grpc.GrpcFilterConfig; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.service.AbstractService; import com.flipkart.gjex.core.service.Service; @@ -24,7 +25,12 @@ import com.flipkart.gjex.grpc.interceptor.FilterInterceptor; import com.flipkart.gjex.grpc.interceptor.StatusMetricInterceptor; import com.flipkart.gjex.grpc.interceptor.TracingInterceptor; -import io.grpc.*; +import io.grpc.BindableService; +import io.grpc.Grpc; +import io.grpc.InsecureServerCredentials; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerInterceptors; import io.grpc.internal.GrpcUtil; import io.grpc.protobuf.services.ProtoReflectionService; @@ -107,8 +113,8 @@ public void doStop() { info("GJEX GrpcServer stopped."); } - public void registerFilters(@SuppressWarnings("rawtypes") List filters, List services) { - this.filterInterceptor.registerFilters(filters, services); + public void registerFilters(@SuppressWarnings("rawtypes") List grpcFilters, List services, GrpcFilterConfig grpcFilterConfig) { + this.filterInterceptor.registerFilters(grpcFilters, services, grpcFilterConfig); } public void registerTracingSamplers(List samplers, List services) { diff --git a/guice/src/main/java/com/flipkart/gjex/grpc/utils/AnnotationUtils.java b/guice/src/main/java/com/flipkart/gjex/grpc/utils/AnnotationUtils.java index c492cc41..2541235e 100644 --- a/guice/src/main/java/com/flipkart/gjex/grpc/utils/AnnotationUtils.java +++ b/guice/src/main/java/com/flipkart/gjex/grpc/utils/AnnotationUtils.java @@ -15,13 +15,13 @@ */ package com.flipkart.gjex.grpc.utils; +import com.flipkart.gjex.core.util.Pair; + import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; -import com.flipkart.gjex.core.util.Pair; - public class AnnotationUtils { /** @@ -33,7 +33,7 @@ public class AnnotationUtils { */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static List> getAnnotatedMethods(Class cls, Class anno) { - List> methods = new LinkedList>(); + List> methods = new ArrayList<>(); for (Method m : cls.getDeclaredMethods()) { if (m.getAnnotation(anno) != null) { methods.add(new Pair(cls,m)); diff --git a/guice/src/main/java/com/flipkart/gjex/guice/GuiceBundle.java b/guice/src/main/java/com/flipkart/gjex/guice/GuiceBundle.java index 6fbb23c3..35352f32 100644 --- a/guice/src/main/java/com/flipkart/gjex/guice/GuiceBundle.java +++ b/guice/src/main/java/com/flipkart/gjex/guice/GuiceBundle.java @@ -15,20 +15,11 @@ */ package com.flipkart.gjex.guice; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import com.flipkart.gjex.core.job.ScheduledJob; -import com.flipkart.gjex.grpc.service.ScheduledJobManager; -import io.grpc.health.v1.HealthGrpc; -import org.glassfish.jersey.server.ResourceConfig; - -import io.dropwizard.metrics5.health.HealthCheck; import com.flipkart.gjex.core.Bundle; import com.flipkart.gjex.core.GJEXConfiguration; -import com.flipkart.gjex.core.filter.Filter; +import com.flipkart.gjex.core.filter.grpc.GrpcFilter; +import com.flipkart.gjex.core.filter.http.HttpFilterParams; +import com.flipkart.gjex.core.job.ScheduledJob; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.service.Service; import com.flipkart.gjex.core.setup.Bootstrap; @@ -36,6 +27,7 @@ import com.flipkart.gjex.core.tracing.TracingSampler; import com.flipkart.gjex.grpc.service.ApiServer; import com.flipkart.gjex.grpc.service.GrpcServer; +import com.flipkart.gjex.grpc.service.ScheduledJobManager; import com.flipkart.gjex.guice.module.ApiModule; import com.flipkart.gjex.guice.module.ConfigModule; import com.flipkart.gjex.guice.module.DashboardModule; @@ -52,10 +44,17 @@ import com.google.inject.Module; import com.google.inject.TypeLiteral; import com.palominolabs.metrics.guice.MetricsInstrumentationModule; - +import io.dropwizard.metrics5.health.HealthCheck; import io.grpc.BindableService; +import io.grpc.health.v1.HealthGrpc; +import org.glassfish.jersey.server.ResourceConfig; import ru.vyarus.guice.validator.ImplicitValidationModule; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + /** * A Guice GJEX Bundle implementation. Multiple Guice Modules may be added to this Bundle. * @@ -68,11 +67,12 @@ public class GuiceBundle implements private List modules; private Injector baseInjector; private List services; - private List filters; + private List grpcFilters; private List healthchecks; private List tracingSamplers; private List scheduledJobs; private List resourceConfigs; + private List httpFilterParamsList; private Optional> configurationClass; private GJEXEnvironmentModule gjexEnvironmentModule; @@ -149,8 +149,8 @@ public void run(T configuration, U configMap, Environment environment) { grpcServer.registerServices(bindableServices); // Add all Grpc Filters to the Grpc Server - filters = getInstances(baseInjector, Filter.class); - grpcServer.registerFilters(filters, bindableServices); + grpcFilters = getInstances(baseInjector, GrpcFilter.class); + grpcServer.registerFilters(grpcFilters, bindableServices, configuration.getGrpc().getGrpcFilterConfig()); // Add all Grpc Filters to the Grpc Server tracingSamplers = getInstances(baseInjector, TracingSampler.class); @@ -172,7 +172,11 @@ public void run(T configuration, U configMap, Environment environment) { ApiServer apiServer = baseInjector.getInstance(ApiServer.class); // Add all custom web resources resourceConfigs = getInstances(baseInjector, ResourceConfig.class); - apiServer.registerResources(resourceConfigs); + apiServer.registerResources(resourceConfigs); + + // Add all custom http filters + httpFilterParamsList = getInstances(baseInjector, HttpFilterParams.class); + apiServer.registerFilters(httpFilterParamsList, configuration.getApiService().getHttpFilterConfig()); } @SuppressWarnings("unchecked") @@ -188,10 +192,10 @@ public List getServices() { } @Override - public List getFilters() { + public List getGrpcFilters() { Preconditions.checkState(baseInjector != null, "Filter(s) are only available after GuiceBundle.run() is called"); - return this.filters; + return this.grpcFilters; } @Override diff --git a/guice/src/main/java/com/flipkart/gjex/guice/module/ApiModule.java b/guice/src/main/java/com/flipkart/gjex/guice/module/ApiModule.java index aea2a36a..a3c84309 100644 --- a/guice/src/main/java/com/flipkart/gjex/guice/module/ApiModule.java +++ b/guice/src/main/java/com/flipkart/gjex/guice/module/ApiModule.java @@ -19,6 +19,7 @@ import com.flipkart.gjex.core.healthcheck.RotationManagementBasedHealthCheck; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.service.Api; +import com.flipkart.gjex.http.interceptor.HttpFilterInterceptor; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.matcher.AbstractMatcher; @@ -112,8 +113,15 @@ public Object invoke(MethodInvocation invocation) throws Throwable { return result; } } - - /** + + @Named("HttpFilterInterceptor") + @Provides + @Singleton + HttpFilterInterceptor getHttpFilterInterceptor(){ + return new HttpFilterInterceptor(); + } + + /** * The Matcher that matches methods with the {@link Api} annotation */ class ApiMethodMatcher extends AbstractMatcher { diff --git a/guice/src/main/java/com/flipkart/gjex/guice/module/DashboardModule.java b/guice/src/main/java/com/flipkart/gjex/guice/module/DashboardModule.java index d6e3410e..7d587f15 100644 --- a/guice/src/main/java/com/flipkart/gjex/guice/module/DashboardModule.java +++ b/guice/src/main/java/com/flipkart/gjex/guice/module/DashboardModule.java @@ -16,43 +16,42 @@ package com.flipkart.gjex.guice.module; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.UnknownHostException; -import java.util.Map; - -import javax.inject.Named; -import javax.inject.Singleton; - -import com.flipkart.gjex.core.web.RotationManagementResource; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature; -import org.glassfish.jersey.servlet.ServletContainer; - -import io.dropwizard.metrics5.jetty9.InstrumentedHandler; -import io.prometheus.metrics.exporter.servlet.javax.PrometheusMetricsServlet; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import com.flipkart.gjex.Constants; import com.flipkart.gjex.core.GJEXConfiguration; +import com.flipkart.gjex.core.healthcheck.HealthCheckRegistry; import com.flipkart.gjex.core.logging.Logging; import com.flipkart.gjex.core.setup.Bootstrap; -import com.flipkart.gjex.core.healthcheck.HealthCheckRegistry; import com.flipkart.gjex.core.tracing.TracingSamplerHolder; +import com.flipkart.gjex.core.web.DashboardHealthCheckResource; import com.flipkart.gjex.core.web.DashboardResource; import com.flipkart.gjex.core.web.HealthCheckResource; +import com.flipkart.gjex.core.web.RotationManagementResource; import com.flipkart.gjex.core.web.TracingResource; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; +import io.dropwizard.metrics5.jetty9.InstrumentedHandler; +import io.prometheus.metrics.exporter.servlet.javax.PrometheusMetricsServlet; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature; +import org.glassfish.jersey.servlet.ServletContainer; + +import javax.inject.Named; +import javax.inject.Singleton; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.Map; /** * DashboardModule is a Guice {@link AbstractModule} implementation used for wiring GJEX Dashboard components. @@ -83,6 +82,7 @@ protected void configure() { @Singleton Server getDashboardJettyServer(@Named("Dashboard.service.port") int port, @Named("DashboardResourceConfig")ResourceConfig resourceConfig, + @Named("DashboardHealthCheckResourceConfig")ResourceConfig dashboardHealthCheckResourceConfig, @Named("Dashboard.service.acceptors") int acceptorThreads, @Named("Dashboard.service.selectors") int selectorThreads, @Named("Dashboard.service.workers") int maxWorkerThreads, @@ -113,9 +113,12 @@ Server getDashboardJettyServer(@Named("Dashboard.service.port") int port, context.getMimeTypes().addMimeMapping("txt", "text/plain;charset=utf-8"); server.setHandler(context); + context.setAttribute(HealthCheckRegistry.HEALTHCHECK_REGISTRY_NAME, this.bootstrap.getHealthCheckRegistry()); + /** Add the Servlet for serving the HealthCheck resource */ + context.addServlet(new ServletHolder(new ServletContainer(dashboardHealthCheckResourceConfig)), "/healthcheck"); + /** Add the Servlet for serving the Dashboard resource */ - ServletHolder servlet = new ServletHolder(new ServletContainer(resourceConfig)); - context.addServlet(servlet, "/admin/*"); + context.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/admin/*"); /** Add the Hystrix metrics stream servlets */ context.addServlet(HystrixMetricsStreamServlet.class, "/stream/hystrix.stream.command.local"); @@ -222,6 +225,16 @@ ResourceConfig getAPIResourceConfig(HealthCheckResource healthCheckResource) { return resourceConfig; } + @Named("DashboardHealthCheckResourceConfig") + @Singleton + @Provides + ResourceConfig getAPIResourceConfig(DashboardHealthCheckResource dashboardHealthCheckResource) { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.register(dashboardHealthCheckResource); + resourceConfig.setApplicationName(Constants.GJEX_CORE_APPLICATION); + return resourceConfig; + } + @Named("RotationManagementResourceConfig") @Singleton @Provides diff --git a/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java b/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java new file mode 100644 index 00000000..3eac84ec --- /dev/null +++ b/guice/src/main/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptor.java @@ -0,0 +1,131 @@ +package com.flipkart.gjex.http.interceptor; + +import com.flipkart.gjex.core.filter.RequestParams; +import com.flipkart.gjex.core.filter.http.HttpFilter; +import com.flipkart.gjex.core.filter.http.HttpFilterParams; +import org.eclipse.jetty.http.pathmap.ServletPathSpec; + +import javax.inject.Named; +import javax.inject.Singleton; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Singleton +@Named("HttpFilterInterceptor") +public class HttpFilterInterceptor implements javax.servlet.Filter { + + private static class ServletPathFiltersHolder { + ServletPathSpec spec; + List filters; + + public ServletPathFiltersHolder(ServletPathSpec spec, List filters) { + this.spec = spec; + this.filters = filters; + } + } + + /** + * Map of Filter instances mapped to Service and its method + */ + @SuppressWarnings("rawtypes") + private Map filtersMap = new HashMap<>(); + private Map> pathSpecToFilterMap = new HashMap<>(); + + public void registerFilters(List httpFilterParamsList) { + for (HttpFilterParams httpFilterParams: httpFilterParamsList){ + if (!filtersMap.containsKey(httpFilterParams.getPathSpec())){ + ServletPathSpec spec = new ServletPathSpec(httpFilterParams.getPathSpec()); + filtersMap.put(httpFilterParams.getPathSpec(), new ServletPathFiltersHolder(spec, + new ArrayList<>())); + } + filtersMap.get(httpFilterParams.getPathSpec()).filters.add(httpFilterParams.getFilter()); + } + } + + public void init(FilterConfig filterConfig) throws ServletException { + for (ServletPathFiltersHolder servletPathFiltersHolder : filtersMap.values()){ + pathSpecToFilterMap.computeIfAbsent(servletPathFiltersHolder.spec, + k-> new ArrayList<>()).addAll(servletPathFiltersHolder.filters); + } + } + + /** + * The core method that processes incoming requests and responses. It captures the request and response objects, + * extracts and builds request parameters including client IP and request headers, and invokes the + * {@link HttpFilter#doProcessRequest(ServletRequest, RequestParams)} method for further processing. + * Finally, it ensures that the response is processed by invoking {@link HttpFilter#doProcessResponse(ServletResponse)}. + * + * @param request The incoming ServletRequest + * @param response The outgoing ServletResponse + * @param chain The filter chain to which the request and response should be passed for further processing + * @throws IOException if an I/O error occurs during the filter chain execution + * @throws ServletException if the request could not be handled + */ + @Override + public final void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + List filters = new ArrayList<>(); + RequestParams.RequestParamsBuilder> requestParamsBuilder = RequestParams.builder(); + try { + if (request instanceof HttpServletRequest){ + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + filters = getMatchingFilters(httpServletRequest.getRequestURI()); + Set headersNames = new HashSet<>(Collections.list(httpServletRequest.getHeaderNames())); + requestParamsBuilder.metadata(headersNames); + requestParamsBuilder.clientIp(getClientIp(request)); + requestParamsBuilder.resourcePath(httpServletRequest.getRequestURI()); + } + RequestParams> requestParams = requestParamsBuilder.build(); + filters.forEach(filter -> filter.doProcessRequest(request, requestParams)); + chain.doFilter(request,response); + } finally { + if (response instanceof HttpServletResponse){ + HttpServletResponse httpServletResponse = (HttpServletResponse)response; + filters.forEach(filter -> filter.doProcessResponseHeaders(new HashSet<>(httpServletResponse.getHeaderNames()))); + } + filters.forEach(filter -> filter.doProcessResponse(response)); + } + } + + /** + * Utility method to extract the real client IP address from the ServletRequest. It checks for the + * "X-Forwarded-For" header to support clients connecting through a proxy. + * + * @param request The ServletRequest object containing the client's request + * @return The real IP address of the client + */ + private String getClientIp(ServletRequest request) { + String remoteAddr = request.getRemoteAddr(); + String xForwardedFor = ((HttpServletRequest) request).getHeader("X-Forwarded-For"); + if (xForwardedFor != null) { + remoteAddr = xForwardedFor.split(",")[0]; + } + return remoteAddr; + } + + private List getMatchingFilters(String path) { + return pathSpecToFilterMap.keySet().stream().filter(key -> key.matches(path)) + .map(k-> pathSpecToFilterMap.get(k)).flatMap(List::stream) + .map(filter -> filter.getInstance()).collect(Collectors.toList()); + } + + @Override + public void destroy() { + + } +} diff --git a/guice/src/test/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptorTest.java b/guice/src/test/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptorTest.java new file mode 100644 index 00000000..7be9a861 --- /dev/null +++ b/guice/src/test/java/com/flipkart/gjex/http/interceptor/HttpFilterInterceptorTest.java @@ -0,0 +1,45 @@ +package com.flipkart.gjex.http.interceptor; + +import com.flipkart.gjex.core.filter.http.AccessLogHttpFilter; +import com.flipkart.gjex.core.filter.http.HttpFilter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.flipkart.gjex.core.filter.http.HttpFilterParams; +import org.eclipse.jetty.http.pathmap.RegexPathSpec; +import org.eclipse.jetty.http.pathmap.ServletPathSpec; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + + +public class HttpFilterInterceptorTest { + + private HttpFilterInterceptor interceptor; + + @Before + public void setUp() { + interceptor = new HttpFilterInterceptor(); + } + + @Test + public void registerFiltersAddsFiltersToMap() { + String pathSpec = "/test/*"; + List filters = new ArrayList<>(); + filters.add(new AccessLogHttpFilter()); + assertEquals(1, filters.size()); + List httpFilterParamsList = new ArrayList<>(); + httpFilterParamsList.add(HttpFilterParams.builder().pathSpec(pathSpec).filter(new AccessLogHttpFilter()).build()); + interceptor.registerFilters(httpFilterParamsList); + } + + @Test + public void testRegexSpec(){ + ServletPathSpec spec = new ServletPathSpec("/test/*"); + assertEquals(true, spec.matches("/test/path")); + } + +} \ No newline at end of file