Hibernate Envers provides Auditing of JPA entities,
but no such library provides out of box support for Auditing MongoDB entities.
Auditing is a cross-cutting concern, should be kept separate from business logic and available to be applied declaratively.
This project provides a simple solution to Audit MongoDB entities.
Requires Java 21, Spring boot 3.2.0+ and MongoDB 4.2+.
All code responsible for auditing is in com.ksoot.mongodb package.
Auditableis anAnnotationto be used to make MongoDB entity classes eligible for Auditing. The entity classes not annotated withAuditablewill not be audited.AuditingMongoEventListeneris the main class responsible for auditing. While application startup it finds all collections enabled for Auditing and creates Audit collections for same and prepare Audit metadata for each collection such as Version field name, Audit collection name, etc. Then it listens to eligible Entity object changes and creates Audit records.MongoDBConfigis custom configuration class for MongoDB.AuditEventis class to persist and retrieve Audit records. It defines following fieldsid: Unique identifier for Audit record.datetime:OffsetDateTimewhen the change happened.actor: Audit Username if available inSecurityContextHolder, otherwise it will be set asSYSTEM.revision: Autoincrement numeric value for each change to a particular record.type: Type of change to a record such asCREATED,UPDATED,DELETED.collectionName: Source MongoDB collection name.source: Snapshot of Audited record.
MongoDBModuleregisters custom serializer for MongoDB ObjectId, otherwise theidattribute is not properly serialized.MongoAuditPropertiesmaps to configuration properties defined inapplication.propertiesorapplication.yml.AuditHistoryRepositoryprovides implementation of repository method to fetch a Page of Audit history.AuditHistoryControllerprovides implementation of API to get a Page of Audit history
Following are the configuration properties to customize MongoDB Auditing behaviour.
application:
mongodb:
entity-base-packages:
- com.ksoot
- com.org
auditing:
enabled: true
without-transaction: false
prefix:
suffix: _audapplication.mongodb.entity-base-packages: List of packages to scan for MongoDB entities, Default:Main class package name.application.mongodb.auditing.enabled: Whether or not to enable MongoDB Auditing, Default:true. If required Auditing can be disabled by setting it tofalse.application.mongodb.auditing.without-transaction: Whether or not to do Auditing without Transactions, Default:false,application.mongodb.auditing.prefix: Audit collection name prefix, Default:.application.mongodb.auditing.suffix: Audit collection name suffix, Default:_aud.
- Only the entity classes annotated with
Auditablewill be audited. - Audit collection name can either be specified in
Auditableannotation (e.g.@Auditable(name = "audit_logs")) or it will be derived from source collection name, prefixed and suffixed with values defined inapplication.mongodb.auditing.prefixandapplication.mongodb.auditing.suffixrespectively. - If it is required to Audit all collections in a single Audit collection then
Auditableannotation can be used with samenameattribute value for all entity classes. - On application startup it scans all the packages defined in
application.mongodb.entity-base-packagesfor MongoDB entities annotated withAuditable. - For each such entity class it creates Audit collection with name as per settings and prepares Audit metadata.
- It listens to all changes to eligible entity classes and creates Audit records, whenever new records are created, existing records are updated or deleted.
- For newly created records
typeattribute of Audit record will beCREATED, for updated recordstypeattribute will beUPDATEDand for deleted recordstypeattribute will beDELETED. sourceattribute of Audit record will contain the snapshot of the record after update. While deleting the record, thesourcewill only contain_idof deleted record.- It is recommended to use a version field annotated with
@Versionin entity classes to avoid concurrent updates. It expects aLongversion field to differentiates between newly created records and already existing updated record. In absence of version fieldtypeattribute of Audit record will beUPDATEDfor newly created and updated records as well. - Eligible Entity class object's changes are detected in listeners defined in
AuditingMongoEventListenerand Audit records are created as per the change type. On creation or updation of Entity objects
@EventListener(condition = "@auditMetaData.isPresent(#event.getCollectionName())")
public void onAfterSave(final AfterSaveEvent<?> event) {
}On deletion of Entity objects
@EventListener(condition = "@auditMetaData.isPresent(#event.getCollectionName())")
public void onAfterDelete(final AfterDeleteEvent<?> event) {
}- The Audit Username is retrieved from
SecurityContextHolderif available, otherwise it will be set asSYSTEM. - It is highly recommended to put the CRUD operation in a Transaction using Spring's
@Transactional(Refer toService) to update source collection and create audit entry atomically. But If requiredapplication.mongodb.auditing.without-transactioncan be set totruethen Auditing will be done without Transactions. - Spring uses
ApplicationEventMulticasterinternally to publish Entity change events. With Transactions, make sureApplicationEventMulticasteris not configured to useAsyncTaskExecutorto publish events asynchronously, because the Transaction would not be propagated to Entity change listeners and Auditing would fail in this case. - It is recommended to use
OffsetDateTimeorZonedDateTimefordatetimeattribute of Audit record to avoid any timezone related issues. Custom converters and Codecs are configured for the same inMongoDBConfig. - Audit history is logged as follows.
You can copy the classes from com.ksoot.mongodb package to your project and use them as it is
or do any changes as per your requirements.
Clone this repository, import in your favourite IDE as either Maven or Gradle project. Though the application is built using Java 21, but you can update Java version to Java 17 also as follows.
Maven pom.xml
<properties>
<java.version>17</java.version>
</properties>Gradle build.gradle
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}Application is bundled with Spring boot Docker compose.
- If you have docker installed, then simply run the application in
dockerprofile by passingspring.profiles.active=dockeras program argument from your IDE. - Depending on your current working directory in IDE, you may need to change
spring.docker.compose.file=spring-boot-mongodb-auditing/compose.ymltospring.docker.compose.file=compose.ymlinapplication-docker.yml - Make sure the host ports mapped in
Docker compose fileare available or change the ports and do the respective changes in database configurationsapplication-docker.yml
Change to your MongoDB URI in application.yml file as follows.
spring:
data:
mongodb:
uri: <Your MongoDB URI>Important
MongoDB replica set is required for Transactions to work.
Refer to MongoDB Replica Set for more details.
- Access
Swaggerat http://localhost:8080/swagger-ui.html - Access Demo CRUD APIs at http://localhost:8080/swagger-ui/index.html?urls.primaryName=Product
- Create a new Product
curl -X 'POST' \
'http://localhost:8080/v1/products' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "iPhone 15 mini",
"description": "iPhone",
"tags": [
"mobile"
],
"attributes": {
"size": "mini",
"color": "black"
}
}'
- Update existing Product, replace
60f0b0b0e3b9a91e8c7b0b0bwith your Product id
curl -X 'PATCH' \
'http://localhost:8080/v1/products/60f0b0b0e3b9a91e8c7b0b0b' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"description": "iPhone Handset"
}'
- Delete existing Product, replace
60f0b0b0e3b9a91e8c7b0b0bwith your Product id
curl -X 'DELETE' \
'http://localhost:8080/v1/products/60f0b0b0e3b9a91e8c7b0b0b' \
-H 'accept: application/json'
- Access Audit History APIs at http://localhost:8080/swagger-ui/index.html?urls.primaryName=Audit to fetch Audit history of any Product. Records can be filtered by Collection Name, Audit event type, Revisions, Audit Username and Datetime range.
curl -X 'GET' \
'http://localhost:8080/v1/audit-history?collectionName=products&page=0&size=16' \
-H 'accept: */*'
Text search refers to the ability to perform Full-text searches on string content in your documents. MongoDB provides a text search feature that allows to search for documents that contain a specified sequence of words or phrases.
- TextIndexed: The candidate fields for text search should be annotated with
@TextIndexedas defined inProductentity class as follows.@TextIndexedmarks a field to be part of the text index.weightattribute defines the significance of the filed relative to other indexed fields. The value directly influences the documents score. As there can be only one text index per collection all fields marked with@TextIndexedare combined into one single index. - TextScore: A field marked with
@TextScoreis also required, to map the text score that MongoDB assigns to documents during a text search. The text score represents the relevance of a document to a given text search query.
@Document(collection = "products")
public class Product extends AbstractEntity {
@TextIndexed(weight = 3)
private String name;
@TextIndexed(weight = 1)
private String description;
@TextIndexed(weight = 2)
private List<String> tags;
private Map<String, String>
attributes;
@TextScore
@Getter(AccessLevel.PACKAGE)
@Setter(AccessLevel.PACKAGE)
private Float score;
}ProductRepository provides implementation for Full-text search as follows.
public Page<Product> findPage(final List<String> phrases, final Pageable pageRequest) {
Query query = new Query();
if (CollectionUtils.isNotEmpty(phrases)) {
TextCriteria criteria = TextCriteria.forDefaultLanguage();
criteria.matchingAny(phrases.toArray(new String[phrases.size()]));
query = TextQuery.queryText(criteria).sortByScore();
}
final long totalRecords = this.mongoOperations.count(query, Product.class);
if (totalRecords == 0) {
return Page.empty();
} else {
final Pageable pageable =
totalRecords <= pageRequest.getPageSize()
? PageRequest.of(0, pageRequest.getPageSize(), pageRequest.getSort())
: pageRequest;
final List<Product> products = this.mongoOperations.find(query.with(pageable), Product.class);
return new PageImpl<>(products, pageable, totalRecords);
}
}Access Demo Full-text search API at http://localhost:8080/swagger-ui/index.html?urls.primaryName=Product to search for Products.
curl -X 'GET' \
'http://localhost:8080/v1/products?phrases=mobile&page=0&size=16' \
-H 'accept: */*'
Rajveer Singh, In case you find any issues or need any support, please email me at [email protected]
- Refer to Spring batch common components and utilities
spring-batch-commons. - Refer to Spring Batch Job implemented as Spring Cloud Task
spring-boot-batch-cloud-task. - Refer to Spring Batch Job implemented as Spring Rest application
spring-boot-batch-web. - For exception handling refer to
spring-boot-problem-handler. - For Spring Data MongoDB Auditing refer to
spring-boot-mongodb-auditing. - For more details on Spring Batch refer to
Spring Batch Reference. - To deploy on Spring cloud Data Flow refer to
Spring Cloud Data Flow Reference.
