Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(java): Collect and log request-response data #1154

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.readme.example;

import org.springframework.context.annotation.Configuration;

/**
* Configuration class for customizing the strategy to collect user data.
*
* <p>This configuration provides a custom implementation of {@link UserDataCollector},
* which overrides the default behavior provided by the SDK. It allows developers
* to specify their own logic for extracting user-specific information, such as API keys,
* email addresses, or labels, from the incoming HTTP requests.</p>
*
* <p>In this example, the API key is extracted from the HTTP headers using the header
* "X-User-Name", while the email and label fields are hardcoded with custom values.
* Developers can modify this logic to suit their application's requirements.</p>
*
* <p>By defining this bean, Spring Boot's auto-configuration will automatically use
* this custom implementation instead of the default {@link UserDataCollector}.</p>
*/
@Configuration
public class CustomUserDataCollectorConfig {

//Uncomment the code below to have a custom user data collection configuration.
//It automatically overrides the default one


// @Bean
// public UserDataCollector<ServletDataPayloadAdapter> customUserDataCollector() {
// return payloadAdapter -> {
// String apiKey = payloadAdapter.getRequestHeaders().get("x-user-name");
// return UserData.builder()
// .apiKey(apiKey)
// .email("[email protected]")
// .label("owl-label")
// .build();
// };
// }
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.readme.example;

import com.readme.datatransfer.har.HttpStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.*;

@RestController
public class OwlController {
Expand All @@ -34,9 +31,21 @@ public Collection<String> getAllOwl() {
}

@PutMapping("/owl/{owlName}")
public String createOwl(@PathVariable String owlName) {
UUID owlUuid = UUID.randomUUID();
owlStorage.put(owlUuid.toString(), owlName);
return "Owl " + owlName + " is created wit id: " + owlUuid;
public ResponseEntity<String> createOwl(@PathVariable String owlName, @RequestBody String body) {
UUID birdId = UUID.randomUUID();
owlStorage.put(birdId.toString(), owlName);

String responseBody = "Bird " + owlName + " created a bird with id: " + birdId + "\n" +
"Creation request body: \n" + body;

HttpHeaders headers = new HttpHeaders();
headers.add("bird-id", birdId.toString());
headers.add("bird-token", Base64.getEncoder()
.encodeToString(birdId.toString()
.getBytes()));

return ResponseEntity.status(HttpStatus.CREATED.getCode())
.headers(headers)
.body(responseBody);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ readme:
# label:
# source: jsonBody
# fieldName: owl-creator/label

#
#readme:
# readmeApiKey: ${README_API_KEY}
# userdata:
Expand Down
126 changes: 107 additions & 19 deletions packages/java/readme-metrics-spring-boot-starter/README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,134 @@
# Monitoring Library Configuration Guide
## Table of Contents
1. [Overview](#overview)
2. [Configuration](#configuration)
- [Readme API Key](#readme-api-key)
- [User data configuration ](#userdata-configuration)
- [Custom user data collection config](#customizing-user-data-collection)

---

## Overview

This spring-boot-starter provides possibility to integrate Readme Metrics SDK to a Spring Boot application easily.
It provides a convenient way to extract user-specific information (e.g., api-key, email, label) from
incoming HTTP requests. It allows configuring multiple extraction methods, such as HTTP headers, JWT claims, or JSON body fields.
This library provides an easy way to integrate the ReadMe Metrics into a Spring Boot application,
enabling comprehensive monitoring and logging capabilities.
The SDK is designed to collect detailed information from HTTP requests and responses, as well as user-specific data,
for better observability and insights into application behavior.

### Key Features:
1. **Request and Response Data Logging**:
- Collects HTTP request and response details, including headers, body content, and HTTP status codes.
- Ensures minimal impact on the application's core business logic by leveraging efficient wrappers for request and response processing.

2. **User Data Extraction**:
- Logs information about the user making the request, such as `api-key`, `email`, and `label`.
- Supports multiple configurable data extraction methods:
- **HTTP headers**
- **JWT claims**
- **JSON body fields**

---

## Configuration

Add the following properties to your `application.yaml` or `application.properties` file.
Each field (`apiKey`, `email`, `label`) requires two sub-properties:
- `source`: Defines where to extract the data from.
To configure the library, you need to define two main aspects:
1. The `ReadMe API Key`, which is required to send logged data to the ReadMe platform.
2. The `UserData` fields (`apiKey`, `email`, `label`), which define where to extract user-specific information from incoming requests.

### ReadMe API Key configuration

The `ReadMe API Key` is a unique identifier that you receive from the ReadMe platform. It is used to authenticate and authorize data sent to the ReadMe metrics endpoint.
You can configure the `ReadMe API Key` in your `application.yaml` or `application.properties` file using environment variables for security.

**application.yaml:**
```yaml
readme:
readmeApiKey: ${README_API_KEY}
```
**application.properties:**
```properties
readme.readmeApiKey=${README_API_KEY}
```

### UserData configuration

The library allows you to extract user-specific data (`apiKey`, `email`, `label`) from incoming HTTP requests. Each field requires two properties:
- **`source`**: Specifies where to extract the data from.
- Possible values:
- `header`: Extracts data from an HTTP header.
- `jwtClaim`: Extracts data from a JWT token claim.
- `jsonBody`: Extracts data from the JSON body of a request.
- `fieldName`: Specifies the name or key associated with the source.
- `header`: Extracts data from an HTTP header.
- `jwtClaim`: Extracts data from a JWT token claim.
- `jsonBody`: Extracts data from the JSON body of a request.
- **`fieldName`**: The key or field name corresponding to the specified source.

### Example Configuration (YAML)

**application.yaml:**
```yaml
readme:
readmeApiKey: ${readmeApiKey}
userdata:
apiKey:
source: header
fieldName: X-User-Id
email:
source: jwt
source: jwtClaim
fieldName: aud
label:
source: jsonBody
fieldName: user.name
fieldName: user/name
```

### Example Configuration (PROPERTIES)
**application.properties:**
```properties
readme.userdata.apikey.source=header
readme.userdata.apikey.fieldname=X-User-Id

readme.userdata.email.source=jwtClaim
readme.userdata.email.source=jwt
readme.userdata.email.fieldname=aud

readme.userdata.label.source=jsonBody
readme.userdata.label.fieldname=user.name
```
readme.userdata.label.fieldname=user/name
```

### Customizing user data collection

The library provides a default implementation of `UserDataCollector`, which extracts user data based on the configuration
in your YAML or properties file. However, some use cases may require custom logic to extract user-specific information.
For example:
- The user data comes from a unique header format.
- Complex logic is needed to determine user-specific fields.
- Multiple fields need to be combined dynamically.

In such cases, you can configure the library with a custom way of extracting user data information
by creating your own implementation of `UserDataCollector`.

---

#### How to Create a Custom UserDataCollector

To create a custom `UserDataCollector`, define a Spring bean for your implementation.
The library's configuration will automatically use your custom implementation if it is present in the application context.

---

#### Example: Custom Implementation

Below is an example of a custom `UserDataCollector` that extracts the `apiKey` from an HTTP header and assigns static
values for `email` and `label`.

```java
@Configuration
public class CustomUserDataCollectorConfig {

@Bean
public UserDataCollector<ServletDataPayloadAdapter> customUserDataCollector() {
return payloadAdapter -> {
// Extract the apiKey from the request headers
String apiKey = payloadAdapter.getRequestHeaders().get("x-user-name");

// Build the UserData object
return UserData.builder()
.apiKey(apiKey)
.email("[email protected]")
.label("owl-label")
.build();
};
}
}
2 changes: 1 addition & 1 deletion packages/java/readme-metrics-spring-boot-starter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
</dependency>
<dependency>
<groupId>com.readme</groupId>
<artifactId>readme-metrics</artifactId>
<artifactId>metrics-core</artifactId>
<version>${readme-metrics.version}</version>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package com.readme.starter.config;

import com.readme.dataextraction.RequestDataCollector;
import com.readme.dataextraction.UserDataCollector;
import com.readme.config.CoreConfig;

import com.readme.dataextraction.payload.requestresponse.RequestDataCollector;
import com.readme.dataextraction.payload.user.UserDataCollector;
import com.readme.datatransfer.DataSender;
import com.readme.datatransfer.HttpDataSender;
import com.readme.datatransfer.OutgoingLogBodyConstructor;
import com.readme.datatransfer.PayloadDataDispatcher;
import com.readme.starter.datacollection.DataCollectionFilter;
import com.readme.starter.datacollection.ServletDataPayloadAdapter;
import com.readme.starter.datacollection.userinfo.ServletUserDataCollector;
import com.readme.starter.datacollection.userinfo.UserDataExtractor;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
Expand All @@ -24,20 +34,52 @@
* </ul>
*/
@Configuration
@ConditionalOnClass({UserDataProperties.class})
@ComponentScan(basePackages = {"com.readme.starter"})
@AllArgsConstructor
@Slf4j
public class DataCollectionAutoConfiguration {

private ReadmeConfigurationProperties readmeProperties;

@Bean
public FilterRegistrationBean<DataCollectionFilter> metricsFilter(
RequestDataCollector<ServletDataPayloadAdapter> requestDataCollector,
UserDataCollector<ServletDataPayloadAdapter> userDataCollector) {
UserDataCollector<ServletDataPayloadAdapter> userDataCollector,
PayloadDataDispatcher payloadDataDispatcher) {
FilterRegistrationBean<DataCollectionFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new DataCollectionFilter(requestDataCollector, userDataCollector));
registrationBean.setFilter(new DataCollectionFilter(userDataCollector, requestDataCollector, payloadDataDispatcher));
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}

@Bean
@ConditionalOnMissingBean(UserDataCollector.class)
public UserDataCollector<ServletDataPayloadAdapter> userDataCollector(UserDataProperties userDataProperties,
UserDataExtractor<ServletDataPayloadAdapter> extractionService) {
log.info("readme-metrics: Creating of default user data collector");
return new ServletUserDataCollector(userDataProperties, extractionService);
}

@Bean
public DataSender dataSender() {
String readmeApiKey = readmeProperties.getReadmeApiKey();
CoreConfig coreConfig = CoreConfig.builder()
.readmeAPIKey(readmeApiKey)
.build();
OkHttpClient okHttpClient = new OkHttpClient();

return new HttpDataSender(okHttpClient, coreConfig);
}

@Bean
public OutgoingLogBodyConstructor outgoingPayloadConstructor() {
return new OutgoingLogBodyConstructor();
}

@Bean
public PayloadDataDispatcher payloadDataDispatcher(DataSender dataSender,
OutgoingLogBodyConstructor outgoingLogConstructor) {
return new PayloadDataDispatcher(dataSender, outgoingLogConstructor);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.readme.starter.config;

import com.readme.config.FieldMapping;
import com.readme.starter.datacollection.userinfo.FieldMapping;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
Expand Down
Loading
Loading