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: Add apollo audit log common solution backend #4958

Closed

Conversation

BlackBear2003
Copy link
Member

@BlackBear2003 BlackBear2003 commented Aug 15, 2023

What's the purpose of this PR

add audit log module, PR for code review and further discussion

Which issue(s) this PR fixes:

Fixes #
#3505

Brief changelog

Add Apollo-Audit module which contains 4 modules of annotation, api, impl, springbootstarter

Follow this checklist to help us incorporate your contribution quickly and easily:

  • [✔️] Read the Contributing Guide before making this pull request.
  • [✔️] Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Write necessary unit tests to verify the code.
  • [✔️] Run mvn clean test to make sure this pull request doesn't break anything.
  • Update the CHANGES log.

@github-actions
Copy link

github-actions bot commented Aug 15, 2023

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@BlackBear2003
Copy link
Member Author

I have read the CLA Document and I hereby sign the CLA

@BlackBear2003
Copy link
Member Author

import apollo-audit-api in biz module, but register that 'api' bean in AdminService module when ApolloAuditAutoConfiguration auto configure. I guess that's the reason of the test failures.

`FieldNewValue` varchar(500) DEFAULT NULL COMMENT '字段新值',
PRIMARY KEY (`Id`),
INDEX `SpanIdIndex` (`SpanId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志数据变动表';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo, table need index

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. think that how user will query by ui
  2. from user's query, how many http api we need to provide, and what there are. include request, response
  3. according the http api's request, try to add index to the table.

key word: page, filter by time, find by traceId, filter by OpName

pom.xml Outdated
<dependency>
<groupId>com.ctrip.framework.apollo.apollo-audit</groupId>
<artifactId>apollo-audit-spring-boot-starter</artifactId>
<version>${revision}</version>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<version>${revision}</version>
<version>${project.version}</version>

revision here will cause some problem to maven, use project.version instead of it.

@Anilople Anilople changed the title Add audit log module feat: Add apollo audit log common solution backend Aug 15, 2023
@Anilople
Copy link
Contributor

Illustrate that how to use this module by the config and annotation

@codecov
Copy link

codecov bot commented Aug 16, 2023

Codecov Report

Merging #4958 (21b061f) into master (84c1d0b) will decrease coverage by 0.10%.
Report is 4 commits behind head on master.
The diff coverage is 84.61%.

❗ Current head 21b061f differs from pull request most recent head c43217e. Consider uploading reports for the commit c43217e to get more accurate results

@@             Coverage Diff              @@
##             master    #4958      +/-   ##
============================================
- Coverage     48.48%   48.39%   -0.10%     
+ Complexity     1726     1724       -2     
============================================
  Files           346      347       +1     
  Lines         10836    10848      +12     
  Branches       1080     1080              
============================================
- Hits           5254     5250       -4     
- Misses         5258     5273      +15     
- Partials        324      325       +1     
Files Changed Coverage Δ
.../com/ctrip/framework/apollo/common/entity/App.java 0.00% <ø> (ø)
...p/framework/apollo/common/entity/AppNamespace.java 0.00% <ø> (ø)
...rip/framework/apollo/common/entity/BaseEntity.java 0.00% <0.00%> (ø)
...mework/apollo/portal/controller/AppController.java 26.82% <ø> (ø)
...ork/apollo/portal/service/AppNamespaceService.java 66.33% <66.66%> (-0.34%) ⬇️
...ip/framework/apollo/portal/service/AppService.java 16.47% <66.66%> (+0.80%) ⬆️
...ortal/audit/ApolloAuditOperatorPortalSupplier.java 75.00% <75.00%> (ø)
...ctrip/framework/apollo/biz/service/AppService.java 50.00% <100.00%> (+1.28%) ⬆️
.../framework/apollo/openapi/PortalOpenApiConfig.java 100.00% <100.00%> (ø)
...k/apollo/portal/component/RestTemplateFactory.java 100.00% <100.00%> (ø)
... and 1 more

... and 3 files with indirect coverage changes

@BlackBear2003
Copy link
Member Author

BlackBear2003 commented Aug 17, 2023

Illustrate that how to use this module by the config and annotation

to explain what this feature will bring to the persistence, this feature consists of two entity classes, AuditLog and AuditLogDataInfluence.

  • AuditLog records the invoke of methods with audit significance, and it is designed based on the idea of distributed link tracing(mainly open-tracing), which can obtain a DAG of the complete user request process through a unique TraceId.
  • AuditLogDataInfluence are informations attached to an AuditLog that records data changes. Currently, the design of this class is to record the database table name of the target entity, the unique ID (to locate the changing entity), the changed Field name, and the old and new values.

Personally, there are still doubts about whether to record the old and new values, because the new value of the previous record is probably the old value of this record, and at the same time it will make it more difficult to automatically capture data changes.

===> need more suggestions here ! <===

now its easier to illustrate that how to use this module by the config and annotation/api:

config side:

Currently, only one configuration option is available, apollo.audit.log.enabled (default to be false).

When adding the apollo-audit-springboot-starter to the library, through this configuration, the automatic configuration function of the entire module can be enabled.

if apollo.audit.log.enabled = true then ApolloAuditAutoConfiguration works,

else then ApolloAuditNoOpAutoConfiguration works. This will register the corresponding NoOp bean to ensure that the application starts and runs smoothly.

annotation/api side:

change log: Align the functionality of the API and annotations

Added the ApolloAuditLogApi interface class as a new function to manually replace the original annotation when using the framework. The methods in the API interface can equally implement the function of annotation. Might do something annotations could not do, like batch delete or else.

How Annotation and Api align
└── annotation
    ├── ApolloAuditLog.java
    ├── ApolloAuditLogDataInfluence.java
    ├── ApolloAuditLogDataInfluenceTable.java
    ├── ApolloAuditLogDataInfluenceTableField.java
    ├── ApolloAuditLogDataInfluenceTableId.java
    └── OpType.java
└── api
    ├── ApolloAuditEntityWrapper.java
    └── ApolloAuditLogApi.java
public interface ApolloAuditLogApi {
  AutoCloseable appendSpan(OpType type, String name, String description);

  void appendSingleDataInfluence(String entityId, String entityName, String fieldName,
      String fieldOldValue, String fieldNewValue);

  <T> void appendDataInfluencesByManagedClass(List<T> entities, OpType type, Class<?> managedClass);

  <T> void appendDataInfluencesByWrapper(List<T> entities, OpType type,
      ApolloAuditEntityWrapper wrapper);
}
public class ApolloAuditEntityWrapper {
  private String entityName;
  private List<Field> dataInfluenceFields;
  private Field entityIdField;
  ...
}

At the same time, an ApolloAuditEntityWrapper class is added to replace the original annotation on the Entity class, and you can manually specify the tableName, unique IdField, and DataInfluencesFields to be recorded.

For trace id, span id, scope and else, mentor told me those are the internal complexity of the framework itself, which does not want to be perceived externally. So I design AutoCloseable appendSpan(OpType type, String name) method to give out a AutoCloseable, it indicates that this resource needs to be closed, and I think this is a better way to hide trace.

like the code shows below, completly use api manually.

public void delete(long id, String operator) {
  App app = appRepository.findById(id).orElse(null);
  if (app == null) {
    return;
  }
  try(AutoCloseable auditScope = apolloAuditLogApi.appendSpan(OpType.DELETE, "app.delete")) {
    app.setDeleted(true);
    app.setDataChangeLastModifiedBy(operator);
    appRepository.save(app);
    ApolloAuditEntityWrapper wrapper = new ApolloAuditEntityWrapper();
    wrapper.entityName("App")
        .entityIdField(app.getClass().getDeclaredField("appId"))
        .addDataInfluenceFields(app.getClass().getDeclaredField("name"));
    apolloAuditLogApi.appendDataInfluencesByWrapper(Collections.singletonList(app),OpType.DELETE, wrapper);
    auditService.audit(App.class.getSimpleName(), id, Audit.OP.DELETE, operator);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

@nobodyiam nobodyiam added this to the 2.2.0 milestone Aug 23, 2023

DROP TABLE IF EXISTS `AuditLog`;

CREATE TABLE `AuditLog` (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apolloconfig/committers For backward compability as more as possible

The solution is that

  • create new table, the new audit log only save in the new table.
  • open this feature by config apollo.audit.log.enabled=true, and user can close it by change to apollo.audit.log.enabled=false
  • create new maven module apollo-audit, other modules use this feature by annotation or interface ApolloAuditLogApi .

Is it ok?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here used @EnableJpaRepositories to manually scan repositories before, but it is actually unnecessary and will affect the scanning of repositories in my audit module.
The JpaRepositoriesAutoConfiguration autoconfiguration class itself automatically scans all packages under the startup classpath. So without this annotation, it can also scan the original repositories.
Therefore, the note itself can be deleted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so so so sorry because when I formatted these test classes, I seem to have changed a lot of the original format...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, the things I changed with the test module were the deletion of unnecessary annotations and the addition of statements to the properties file to start the audit module

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git checkout ${commitId} -- ${file relative path}

use a old commitId, the file will change to old content

@BlackBear2003
Copy link
Member Author

I haven't add many uses of the audit module to other modules, for the reason that it seems decrease the test covery, so what should I do? Add the annotation/api uses in another pull request?

@BlackBear2003
Copy link
Member Author

For this PR, what else needs to be done and what needs to be improved? :)

}

@GetMapping("/logs/data_influences/entity_name/{entityName}/entity_id/{entityId}")
public List<ApolloAuditLogDataInfluence> findDataInfluencesByEntity(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data_influences -> dataInfluences, delete _


@GetMapping("/logs/data_influences/entity_name/{entityName}/entity_id/{entityId}")
public List<ApolloAuditLogDataInfluence> findDataInfluencesByEntity(
@PathVariable String entityName, @PathVariable String entityId, Pageable page) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PathVariable String entityName -> @RequestParam String entityName

OpType type();
String name();
String description() default "no description";
boolean autoLog() default false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use ApolloAuditLogDataInfluence instead


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApolloAuditLogDataInfluenceTableId {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete it, index by traceId finally

</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-audit-spring-boot-starter</artifactId>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only use apollo-audit-api?

Copy link
Member Author

@BlackBear2003 BlackBear2003 Sep 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only use apollo-audit-api?

when running biz-tests, it need to register bean of ApolloAuditLogApi which is provide by auto-configuration. So the dependency's scope is test to making tests pass.
we don't run biz individually, so yes we only need apollo-audit-api. But in tests, we run it individually, so it needs starter for starting app.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git checkout ${commitId} -- ${file relative path}

use a old commitId, the file will change to old content

import com.ctrip.framework.apollo.audit.annotation.OpType;
import java.util.List;

public interface ApolloAuditLogApi {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Controller use ApolloAuditLogApi

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe create 2 api

  • ApolloAuditLogRecordApi
  • ApolloAuditLogQueryApi (for user's query)

then let ApolloAuditLogApi extends them

return logRepository.findAll(pageable).getContent();
}

public List<ApolloAuditLog> findByOpType(String opType, Pageable page) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete this method? only keep findByOpName?

`DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`DataChange_LastModifiedBy` varchar(64) DEFAULT '' COMMENT '最后修改人邮箱前缀',
`DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
PRIMARY KEY (`Id`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KEY DataChange_LastTime (DataChange_LastTime),

`FieldNewValue` varchar(500) DEFAULT NULL COMMENT '字段新值',
PRIMARY KEY (`Id`),
INDEX `SpanIdIndex` (`SpanId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志数据变动表';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. think that how user will query by ui
  2. from user's query, how many http api we need to provide, and what there are. include request, response
  3. according the http api's request, try to add index to the table.

key word: page, filter by time, find by traceId, filter by OpName


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add jpa here? Need annotation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, for entities annotations.Should replaced by

<dependency>
      <groupId>jakarta.persistence</groupId>
      <artifactId>jakarta.persistence-api</artifactId>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
    </dependency>

?


public interface ApolloAuditLogQueryApi {

List<ApolloAuditLog> queryAllLogs(Pageable page);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
List<ApolloAuditLog> queryAllLogs(Pageable page);
List<ApolloAuditLog> queryLogs(Pageable page);


public class DaoApolloAuditLogApi implements ApolloAuditLogApi {
public class JpaApolloAuditLogApi implements ApolloAuditLogApi {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public class JpaApolloAuditLogApi implements ApolloAuditLogApi {
public class ApolloAuditLogApiJpaImpl implements ApolloAuditLogApi {


void appendDataInfluenceWrapper(Class<?> clazz);

void appendDataInfluenceWrapper(Class<?> clazz, ApolloAuditEntityWrapper wrapper);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone use ApolloAuditLogRecordApi, appendDataInfluenceWrapper should be notice?

Can we append the DataInfluence directly?

i.e append the DataInfluence and the definition class in a method.

@EventListener
public void handleEvent(ApolloAuditLogDataInfluenceEvent event) {
Object e = event.getEntity();
api.appendDataInfluences(Collections.singletonList(e), event.isDeleted(), e.getClass());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why need isDeleted here?

@@ -87,14 +86,8 @@ public Object around(ProceedingJoinPoint pjp, ApolloAuditLog auditLog) throws Th

try (AutoCloseable scope = api.appendSpan(auditLog.type(), auditLog.name(),
auditLog.description())) {
dataInfluenceList.forEach(e -> api.appendDataInfluenceWrapper(e.getClass()));
returnVal = pjp.proceed();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we append DataInfluence after method finished?

Maybe after pjp.proceed() finished, just call a method in api will be more simpler?

for example

returnVal = pjp.proceed();
Class<?> xxxEntity = // get from method signaure, argument with annotation
api.appendDataInfluenceList(xxxEntity, dataInfluenceList);

and it work will without appendDataInfluenceWrapper?

@@ -22,3 +22,5 @@

# You may change the following config to activate different database profiles like h2/postgres
spring.profiles.group.github = mysql

apollo.audit.log.enabled = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
apollo.audit.log.enabled = false
# true: enabled the new feature of audit log
# false: disable it
apollo.audit.log.enabled = false

import org.springframework.context.annotation.Configuration;

@Configuration
public class ApolloAuditConfiguration {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move ApolloAuditSpanService and ApolloAuditConfiguration to audit-log module?

The logic in portal or admin service are equal? i.e always add some information to RequestContextHolder.getRequestAttributes(); if those information don't exist.


/**
* http header constants
* @author luke ([email protected])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment

* http header constants
* @author luke ([email protected])
*/
public interface ApolloAuditHttpHeader {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public interface ApolloAuditHttpHeader {
public interface ApolloAuditHttpHeaderContants {

public ApolloAuditScopeManager() {
}

public ApolloAuditScope activate(ApolloAuditSpanContext spanContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If two thread invoke the activate method, the field will be overwrite.

</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why need aspectj here? the aop?

@Bean
@ConditionalOnMissingBean(ApolloAuditHttpInterceptor.class)
public ApolloAuditHttpInterceptor apolloAuditLogHttpTracerInterceptor(ApolloAuditLogApi api) {
return new ApolloAuditHttpInterceptor(api);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • NoOpApolloAuditHttpInterceptor?

@@ -22,3 +22,5 @@

# You may change the following config to activate different database profiles like h2/postgres
spring.profiles.group.github = mysql

apollo.audit.log.enabled = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment like admin service for user

@@ -587,4 +592,9 @@ public ServerConfig createOrUpdateConfigDBConfig(Env env, ServerConfig serverCon
}
}

@Service
public static class AuditLogAPI extends API {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete this class?

@@ -141,6 +144,7 @@ public App create(@Valid @RequestBody AppModel appModel) {

@PreAuthorize(value = "@permissionValidator.isAppAdmin(#appId)")
@PutMapping("/{appId:.+}")
@ApolloAuditLog(type = OpType.UPDATE, name = "user-command.app.update")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@ApolloAuditLog(type = OpType.UPDATE, name = "user-command.app.update")
@ApolloAuditLog(type = OpType.UPDATE, name = "app.update")

if in openapi, use openapi.app.update

)
return d.promise
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

d.reject(result);
}
)
return d.promise
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

}, function (result) {
d.reject(result);
}
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

d.reject(result);
}
)
return d.promise
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

}, function (result) {
d.reject(result);
}
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

d.reject(result);
}
)
return d.promise
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

}, function (result) {
d.reject(result);
}
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

d.reject(result);
}
)
return d.promise
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

}, function (result) {
d.reject(result);
}
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

}
$scope.relatedDataInfluences = $scope.relatedDataInfluences.concat(result);

})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

$scope.showingDetail = {};
$scope.relatedDataInfluences = [];
$scope.relatedDataInfluencePage = 0;
let RelatedDataInfluencePageSize = 10;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'let' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

return d.promise
}
}
}])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon.

@github-actions github-actions bot locked and limited conversation to collaborators Sep 24, 2023
@nobodyiam nobodyiam removed this from the 2.2.0 milestone Nov 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants