refactor: add parameter verification to the visit counter API (#3546)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.4.x

#### What this PR does / why we need it:
对访问量统计的 API 增加数据合法性校验
点赞同理

see #3530 for more details

how to test it?
1. 访问文章和页面可以统计访问量
2. 使用访问以下 API 添加模拟使用错误数据不会在 Counter 模型添加记录
```shell
curl 'http://localhost:8090/apis/api.halo.run/v1alpha1/trackers/counter' -u 'your-username:your-password'  \
--header 'Content-Type: application/json' \
--data '{
    "group": "fake.halo.run",
    "plural": "posts",
    "name": "fake-name",
    "hostname": "localhost",
    "screen": "1920x1080",
    "language": "zh-CN",
    "url": "/archives/hello-halo",
    "referrer": "http://localhost:8090/"
}'
```
期望出现日志:
```
2023-03-21T12:37:08.391+08:00 DEBUG 7036 --- [task-4] r.h.app.metrics.VisitedEventReconciler   : Skip visit event for: GroupPluralName[group=fake.halo.run, plural=posts, name=fake-name]
```
并且
```
curl 'http://localhost:8090/apis/metrics.halo.run/v1alpha1/counters' -u 'your-username:your-password'
```
不会出现上述错误数据的记录
#### Which issue(s) this PR fixes:

Fixes #3530

#### Does this PR introduce a user-facing change?

```release-note
对访问量统计的 API 增加数据合法性校验
```
pull/3561/head
guqing 2023-03-27 16:04:17 +08:00 committed by GitHub
parent fbe8e627e8
commit fb2bc4252d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 98 additions and 22 deletions

View File

@ -4,16 +4,22 @@ import java.time.Duration;
import java.time.Instant;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.Counter;
import run.halo.app.event.post.VisitedEvent;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.DefaultController;
@ -88,11 +94,6 @@ public class VisitedEventReconciler
Duration.ofMinutes(5));
}
@EventListener(VisitedEvent.class)
public void handlePostPublished(VisitedEvent visitedEvent) {
mergeVisits(visitedEvent);
}
@Override
public void start() {
this.visitedEventController.start();
@ -121,18 +122,53 @@ public class VisitedEventReconciler
return this.running;
}
private void mergeVisits(VisitedEvent event) {
String counterName =
MeterUtils.nameOf(event.getGroup(), event.getPlural(), event.getName());
pooledVisitsMap.compute(counterName, (name, visits) -> {
if (visits == null) {
return 1;
} else {
return visits + 1;
}
});
}
public record VisitCountBucket(String name, int visits) {
}
@Component
@RequiredArgsConstructor
public class VisitedEventListener {
private final SchemeManager schemeManager;
@Async
@EventListener(VisitedEvent.class)
public void onVisited(VisitedEvent visitedEvent) {
mergeVisits(visitedEvent);
}
private void mergeVisits(VisitedEvent event) {
var gpn = new GroupPluralName(event.getGroup(), event.getPlural(), event.getName());
if (!checkVisitSubject(gpn)) {
log.debug("Skip visit event for: {}", gpn);
return;
}
String counterName =
MeterUtils.nameOf(event.getGroup(), event.getPlural(), event.getName());
pooledVisitsMap.compute(counterName, (name, visits) -> {
if (visits == null) {
return 1;
} else {
return visits + 1;
}
});
}
private boolean checkVisitSubject(GroupPluralName groupPluralName) {
Optional<Scheme> schemeOptional = schemeManager.schemes().stream()
.filter(scheme -> {
GroupVersionKind gvk = scheme.groupVersionKind();
return scheme.plural().equals(groupPluralName.plural())
&& gvk.group().equals(groupPluralName.group());
})
.findFirst();
return schemeOptional.map(
scheme -> client.fetch(scheme.groupVersionKind(), groupPluralName.name())
.isPresent()
)
.orElse(false);
}
record GroupPluralName(String group, String plural, String name) {
}
}
}

View File

@ -2,16 +2,22 @@ package run.halo.app.metrics;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.Counter;
import run.halo.app.event.post.DownvotedEvent;
import run.halo.app.event.post.UpvotedEvent;
import run.halo.app.event.post.VotedEvent;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.DefaultController;
@ -77,11 +83,6 @@ public class VotedEventReconciler implements Reconciler<VotedEvent>, SmartLifecy
Duration.ofMinutes(5));
}
@EventListener(VotedEvent.class)
public void handlePostPublished(VotedEvent votedEvent) {
votedEventQueue.addImmediately(votedEvent);
}
@Override
public void start() {
this.votedEventController.start();
@ -98,4 +99,43 @@ public class VotedEventReconciler implements Reconciler<VotedEvent>, SmartLifecy
public boolean isRunning() {
return this.running;
}
@Component
@RequiredArgsConstructor
public class VotedEventListener {
private final SchemeManager schemeManager;
/**
* Add up/down vote event to queue.
*/
@Async
@EventListener(VotedEvent.class)
public void onVoted(VotedEvent event) {
var gpn = new GroupPluralName(event.getGroup(), event.getPlural(), event.getName());
if (!checkSubject(gpn)) {
log.debug("Skip voted event for: {}", gpn);
return;
}
votedEventQueue.addImmediately(event);
}
private boolean checkSubject(
GroupPluralName groupPluralName) {
Optional<Scheme> schemeOptional = schemeManager.schemes().stream()
.filter(scheme -> {
GroupVersionKind gvk = scheme.groupVersionKind();
return scheme.plural().equals(groupPluralName.plural())
&& gvk.group().equals(groupPluralName.group());
})
.findFirst();
return schemeOptional.map(
scheme -> client.fetch(scheme.groupVersionKind(), groupPluralName.name())
.isPresent()
)
.orElse(false);
}
record GroupPluralName(String group, String plural, String name) {
}
}
}