Last updated Oct 9, 2024

Migrating to the new Jira audit log Java API

In Jira 8.8 we announced the new and improved auditing feature which came with its own API.

As for the audit API that was already present, we’re going to mark it as deprecated in Jira version 8.12, and we will still support it in parallel with the new API to give you time to migrate.

In this article we’re going to focus on the migration to the new auditing API.

How to migrate to the new audit log Java API

First, let’s look at the connection between the legacy and the new entities that the APIs use and how the legacy APIs can be replaced with the new ones. Then, we’ll look at the actual code sample on how to do the migration.

API entities mapping

Legacy audit API entities

New audit API entities

com.atlassian.jira.auditing.AssociatedItem

com.atlassian.audit.entity.AuditResource

com.atlassian.jira.auditing.ChangedValue

com.atlassian.audit.entity.ChangedValue

com.atlassian.jira.auditing.AuditingCategory

com.atlassian.jira.auditing.AuditCategory (since Jira 8.12.2)

com.atlassian.jira.auditing.AuditingFilter

com.atlassian.audit.api.AuditQuery

com.atlassian.jira.auditing.RecordRequest

com.atlassian.audit.entity.AuditEvent

com.atlassian.jira.auditing.AuditRecord

com.atlassian.audit.entity.AuditEntity

APIs mapping

You’ll notice that some APIs can have more than one equivalent. The right API depends on your use case, and the main difference is how the results are provided back to you. For more details, we recommend exploring the Javadoc that comes with the APIs.

Legacy audit API entities

New audit API entities

com.atlassian.jira.auditing.AuditingManager#store

com.atlassian.audit.api.AuditService#audit

com.atlassian.jira.auditing.AuditingManager#getRecords

com.atlassian.audit.api.AuditSearchService#findBy
or
com.atlassian.audit.api.AuditSearchService#stream

com.atlassian.jira.auditing.AuditingManager#countRecords

com.atlassian.audit.api.AuditSearchService#count

com.atlassian.jira.auditing.AuditingService#getRecords

com.atlassian.audit.api.AuditSearchService#findBy
or
com.atlassian.audit.api.AuditSearchService#stream

com.atlassian.jira.auditing.AuditingService#storeRecord

com.atlassian.audit.api.AuditService#audit

com.atlassian.jira.auditing.AuditingService#getTotalNumberOfRecords

com.atlassian.audit.api.AuditSearchService#count

Migration examples

Now, we’re going to look at some code examples on how to migrate to the new API.

Storing a new audit event

Most of the entities used in the legacy API were replaced by new entities used by the new API, so migrating to the new API means you will have to migrate dependent entities as well.

Legacy API

Here is an example of the old way to create an audit event. Note that we omitted the instantiation part of some of the variables to make the code more readable.

1
2
// Creating an affected object
AssociatedItem affectedObject = ...;

// Creating associatedItems
Iterable<AssociatedItem> associatedItems = ...;

// Creating changed values
Iterable<ChangedValue> changedValues = ...;

// Creating the request to publish new audit event
RecordRequest recordRequest = new RecordRequest(AuditingCategory.APPLICATIONS, "some.custom.action.key")
        .forObject(affectedObject)
        .withAssociatedItems(associatedItems)
        .withChangedValues(changedValues);
    
// Publishing the audit event
auditingManager.store(recordRequest);

New API

Adapting the above code to the new API should look like the following:

1
2
// Details about the event coverage area, level and 
AuditType auditType = ...; /* see below */

// Creating an affected object
AuditResource affectedObject = ...; /* see below */

// Creating associatedItems
List<AuditResource> auditResources = ... /* similar to creating the 'affectedObject' above */

// Creating changed values
List<ChangedValue> changedValues = ...; /* see below */
    
// Creating the request to publish new audit event
AuditEvent event = AuditEvent.builder(auditType)
        .affectedObject(affectedObject) /* equivalent of affectedObject from legacy API example */
        // It is important to append the extra affectedObjects only after the affectedObject was added
        // or if you do it in one go with the AuditEvent.Builder#affectedObjects method, make sure the affectedObject is the first item in the list
        .appendAffectedObjects(auditResources) /* equivalent of associatedItems from the legacy API example */
        .changedValues(changedValues)
        .build();
    
// Publishing the audit event
auditService.audit(event);

Now let’s look closer at how to create the variables passed to the AuditEvent builder.

Audit type

By using the AuditType coverage area and level your events will be even more customizable. In addition to providing an action and a category like in the legacy API, you will be able to provide coverage area (event product area descriptor) and level (event verbosity level) to better group the events provided by your code. More than that, the AuditType supports translation for the event summary (action) and coverage.

1
2
AuditType auditType = AuditType.fromI18nKeys(
                CoverageArea.ECOSYSTEM,  /* recommended area for ecosystem events */
                AuditCategory.APPLICATIONS, /* on top of Jira predefined values, it can be any string value */
                "some.custom.action.key", /* an action internationalization key */
                CoverageLevel.FULL) /* the level of coverage necessary to show this event */
        .withCategoryTranslation("translation for category key passed above") /* optional, equivalent of AuditingCategory#getId return value in the legacy API */
        .withActionTranslation("translation for action key passed above") /* optional, equivalent of RecordRequest#getSummary return value in the legacy API */
        .build();

The ECOSYSTEM coverage area is recommendend when publishing events from plug-ins since it will define a better separation between Jira and 3rd party events.

Affected object

AuditResource builder will take the same parameters values you had to provide before for the AssociatedItem implementation of the get... methods.

1
2
AuditResource affectedObject = AuditResource.builder(name, type) /* 'name' and 'type' are the equivalents of AssociatedItem#getObjectName and AssociatedItem.Type#name return values in the legacy API */
        .id(id) /* equivalent of AssociatedItem#getObjectId return value in the legacy API */
        .build();
Changed values

Changed values look more or less the same as the ones from the legacy API. The only change is that the builder now requires the internationalization key to support other locales.

1
2
List<ChangedValue> changedValues = singletonList(
    ChangedValue.fromI18nKeys(i18nKey)
        .withKeyTranslation("translation for the i18nKey") /* optional, equivalent of the return value of `ChangedValue.getName()` in the legacy API */
        .from(fromValue) /* equivalent of the return value of `ChangedValue#getFrom` in the legacy API */
        .to(toValue) /* equivalent of the return value of `ChangedValue#getTo` in the legacy API */
        .build());

Searching for audit events

Using the new API for searching is more straightforward in comparison to the old one. In the old API the configuration for the query was provided either to the AuditingFilter or to the AuditingManager#getRecords while searching.

In the new API, the same configurations are held either by the AuditQuery or the PageRequest. One nice feature that will stand out from the beginning is that the results are returned in a paginated format for easier processing.

An example of migration can be seen below. Note that some of the fields are nullable so they are not required all the time, allowing you to adjust the querying based on your specific needs.

Legacy API

1
2
// Create query
AuditingFilter auditingFilter = AuditingFilter.builder()
        .filter(filter)
        .fromTimestamp(fromTimestamp)
        .toTimestamp(toTimestamp)
        .userIds(userIds)
        .build();

// Query
Records records = auditingManager.getRecords(
        maxId,
        sinceId,
        count,
        offset,
        auditingFilter);

// Get actual results from the response
List<AuditRecord> results = records.getResults();

New API

1
2
// Create query
AuditQuery auditQuery = AuditQuery.builder()
        .maxId(maxId) /* similar to the value from legacy API example */
        .minId(sinceId) /* similar to the value from legacy API example */
        .searchText(filter) /* similar to the value from legacy API example */
        .usersIds(userIds) /* similar to the values from legacy API example */
        .from(fromTimestamp) /* similar to the value from legacy API example */
        .to(toTimestamp) /* similar to the value from legacy API example */
        .build();
PageRequest<AuditEntityCursor> auditEntityCursorPageRequest = new PageRequest.Builder<>()
        .offset(offset) /* similar to the value from legacy API example */
        .limit(limit) /* number of results, up to PageRequest.Builder.MAX_PAGE_LIMIT */
        .build();

// Query
Page<AuditEntity, AuditEntityCursor> auditEntityCursorPage = auditSearchService.findBy(
        auditQuery,
        auditEntityCursorPageRequest);

// Get actual results from the response
List<AuditEntity> results = auditEntityCursorPage.getValues();

As you can see, the new code makes use of the same information to search as the legacy API, it is just organized differently.

REST API

This article covers the Java APIs. For the REST API migration information, please refer to the Audit log improvements update.

Rate this page: