mirror of https://github.com/halo-dev/halo
feat: add post and single page statistics (#2476)
#### What type of PR is this? /kind feature /area core /milestone 2.0 /kind api-change #### What this PR does / why we need it: 新增文章和自定义页面统计功能: 1. 浏览量统计,通过在指定模板页面插入 tracker js 上报浏览量数据并写日志到文件同时通过 Metrics 暴露数据 2. 浏览量日志异步写文件 3. 文章列表和自定义页面列表数据增加 stats 属性 4. 主题文章列表及详情、自定义页面详情增加 stats 属性 5. 新增仪表盘统计数据展示, endpoint: `/apis/api.console.halo.run/v1alpha1/stats` ```java private Integer visits; private Integer comments; private Integer approvedComments; private Integer upvotes; private Integer users; ``` #### Which issue(s) this PR fixes: Fixes #2430 #### Special notes for your reviewer: how to test it? 1. 使用一个主题,创建几篇文章和一些自定义页面,访问他们,并在详情页面的 head 总看到一个 `halo-tracker.js` 的 script 标签  2. 访问10次以上可以看到 halo work下有一个 `logs/visits.log`,里面有访问记录,它是10条为一批异步刷新到磁盘 3. 主题端文章列表及详情和自定义页面的详情可以取到一个stats字段 4. `/apis/api.console.halo.run/v1alpha1/stats` 此 endpoint 可以得到总的访问量和评论量,这些数据也是异步的会有一分钟延迟 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note None ```pull/2488/head
parent
a3aba94102
commit
eaa18573f0
|
@ -4,6 +4,7 @@ import org.springframework.boot.SpringApplication;
|
|||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.infra.properties.JwtProperties;
|
||||
|
||||
|
@ -15,6 +16,7 @@ import run.halo.app.infra.properties.JwtProperties;
|
|||
* @author guqing
|
||||
* @date 2017-11-14
|
||||
*/
|
||||
@EnableScheduling
|
||||
@SpringBootApplication(scanBasePackages = "run.halo.app", exclude =
|
||||
IntegrationAutoConfiguration.class)
|
||||
@EnableConfigurationProperties({HaloProperties.class, JwtProperties.class})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.config;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.pf4j.PluginManager;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
@ -202,9 +203,10 @@ public class ExtensionConfiguration {
|
|||
}
|
||||
|
||||
@Bean
|
||||
Controller commentController(ExtensionClient client) {
|
||||
Controller commentController(ExtensionClient client, MeterRegistry meterRegistry,
|
||||
SchemeManager schemeManager) {
|
||||
return new ControllerBuilder("comment-controller", client)
|
||||
.reconciler(new CommentReconciler(client))
|
||||
.reconciler(new CommentReconciler(client, meterRegistry, schemeManager))
|
||||
.extension(new Comment())
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -28,4 +28,7 @@ public class ListedPost {
|
|||
|
||||
@Schema(required = true)
|
||||
private List<Contributor> contributors;
|
||||
|
||||
@Schema(required = true)
|
||||
private Stats stats;
|
||||
}
|
||||
|
|
|
@ -20,4 +20,7 @@ public class ListedSinglePage {
|
|||
|
||||
@Schema(required = true)
|
||||
private List<Contributor> contributors;
|
||||
|
||||
@Schema(required = true)
|
||||
private Stats stats;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* Stats value object.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class Stats {
|
||||
|
||||
Integer visit;
|
||||
|
||||
Integer upvote;
|
||||
|
||||
Integer totalComment;
|
||||
|
||||
Integer approvedComment;
|
||||
}
|
|
@ -24,7 +24,9 @@ import run.halo.app.content.PostQuery;
|
|||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.PostSorter;
|
||||
import run.halo.app.content.Stats;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
import run.halo.app.core.extension.Tag;
|
||||
|
@ -33,6 +35,8 @@ import run.halo.app.extension.ListResult;
|
|||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link PostService}.
|
||||
|
@ -44,10 +48,13 @@ import run.halo.app.infra.ConditionStatus;
|
|||
public class PostServiceImpl implements PostService {
|
||||
private final ContentService contentService;
|
||||
private final ReactiveExtensionClient client;
|
||||
private final CounterService counterService;
|
||||
|
||||
public PostServiceImpl(ContentService contentService, ReactiveExtensionClient client) {
|
||||
public PostServiceImpl(ContentService contentService, ReactiveExtensionClient client,
|
||||
CounterService counterService) {
|
||||
this.contentService = contentService;
|
||||
this.client = client;
|
||||
this.counterService = counterService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -67,6 +74,19 @@ public class PostServiceImpl implements PostService {
|
|||
);
|
||||
}
|
||||
|
||||
Stats fetchStats(Post post) {
|
||||
Assert.notNull(post, "The post must not be null.");
|
||||
String name = post.getMetadata().getName();
|
||||
Counter counter =
|
||||
counterService.getByName(MeterUtils.nameOf(Post.class, name));
|
||||
return Stats.builder()
|
||||
.visit(counter.getVisit())
|
||||
.upvote(counter.getUpvote())
|
||||
.totalComment(counter.getApprovedComment())
|
||||
.approvedComment(counter.getApprovedComment())
|
||||
.build();
|
||||
}
|
||||
|
||||
Predicate<Post> postListPredicate(PostQuery query) {
|
||||
Predicate<Post> paramPredicate = post ->
|
||||
contains(query.getCategories(), post.getSpec().getCategories())
|
||||
|
@ -132,6 +152,7 @@ public class PostServiceImpl implements PostService {
|
|||
.map(p -> {
|
||||
ListedPost listedPost = new ListedPost();
|
||||
listedPost.setPost(p);
|
||||
listedPost.setStats(fetchStats(post));
|
||||
return listedPost;
|
||||
})
|
||||
.flatMap(lp -> setTags(post.getSpec().getTags(), lp))
|
||||
|
|
|
@ -24,6 +24,8 @@ import run.halo.app.content.SinglePageQuery;
|
|||
import run.halo.app.content.SinglePageRequest;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.content.SinglePageSorter;
|
||||
import run.halo.app.content.Stats;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.core.extension.Snapshot;
|
||||
|
@ -32,6 +34,8 @@ import run.halo.app.extension.ListResult;
|
|||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.Condition;
|
||||
import run.halo.app.infra.ConditionStatus;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
/**
|
||||
* Single page service implementation.
|
||||
|
@ -45,9 +49,13 @@ public class SinglePageServiceImpl implements SinglePageService {
|
|||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public SinglePageServiceImpl(ContentService contentService, ReactiveExtensionClient client) {
|
||||
private final CounterService counterService;
|
||||
|
||||
public SinglePageServiceImpl(ContentService contentService, ReactiveExtensionClient client,
|
||||
CounterService counterService) {
|
||||
this.contentService = contentService;
|
||||
this.client = client;
|
||||
this.counterService = counterService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -179,6 +187,7 @@ public class SinglePageServiceImpl implements SinglePageService {
|
|||
.map(sp -> {
|
||||
ListedSinglePage listedSinglePage = new ListedSinglePage();
|
||||
listedSinglePage.setPage(singlePage);
|
||||
listedSinglePage.setStats(fetchStats(singlePage));
|
||||
return listedSinglePage;
|
||||
})
|
||||
.flatMap(lsp ->
|
||||
|
@ -194,6 +203,19 @@ public class SinglePageServiceImpl implements SinglePageService {
|
|||
.defaultIfEmpty(singlePage);
|
||||
}
|
||||
|
||||
Stats fetchStats(SinglePage singlePage) {
|
||||
Assert.notNull(singlePage, "The singlePage must not be null.");
|
||||
String name = singlePage.getMetadata().getName();
|
||||
Counter counter =
|
||||
counterService.getByName(MeterUtils.nameOf(SinglePage.class, name));
|
||||
return Stats.builder()
|
||||
.visit(counter.getVisit())
|
||||
.upvote(counter.getUpvote())
|
||||
.totalComment(counter.getApprovedComment())
|
||||
.approvedComment(counter.getApprovedComment())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Flux<Contributor> listContributors(List<String> usernames) {
|
||||
if (usernames == null) {
|
||||
return Flux.empty();
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package run.halo.app.core.extension;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import java.util.Collection;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
/**
|
||||
* A counter for number of requests by extension resource name.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@GVK(group = "metrics.halo.run", version = "v1alpha1", kind = "Counter", plural = "counters",
|
||||
singular = "counter")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Counter extends AbstractExtension {
|
||||
|
||||
private Integer visit;
|
||||
|
||||
private Integer upvote;
|
||||
|
||||
private Integer totalComment;
|
||||
|
||||
private Integer approvedComment;
|
||||
|
||||
/**
|
||||
* Populate counter data from {@link Meter}s.
|
||||
*
|
||||
* @param meters counter meters
|
||||
*/
|
||||
public <T extends Meter> void populateFrom(Collection<T> meters) {
|
||||
populateDefaultValue();
|
||||
for (Meter meter : meters) {
|
||||
if (meter instanceof io.micrometer.core.instrument.Counter meterCounter) {
|
||||
if (MeterUtils.isVisitCounter(meterCounter)) {
|
||||
this.visit = (int) meterCounter.count();
|
||||
} else if (MeterUtils.isUpvoteCounter(meterCounter)) {
|
||||
this.upvote = (int) meterCounter.count();
|
||||
} else if (MeterUtils.isTotalCommentCounter(meterCounter)) {
|
||||
this.totalComment = (int) meterCounter.count();
|
||||
} else if (MeterUtils.isApprovedCommentCounter(meterCounter)) {
|
||||
this.approvedComment = (int) meterCounter.count();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void populateDefaultValue() {
|
||||
this.visit = 0;
|
||||
this.upvote = 0;
|
||||
this.totalComment = 0;
|
||||
this.approvedComment = 0;
|
||||
}
|
||||
}
|
|
@ -6,10 +6,8 @@ import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
|||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
|
@ -26,6 +24,7 @@ import run.halo.app.core.extension.Comment;
|
|||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
import run.halo.app.infra.utils.IpAddressUtils;
|
||||
|
||||
|
||||
|
@ -106,7 +105,7 @@ public class CommentEndpoint implements CustomEndpoint {
|
|||
.flatMap(commentRequest -> {
|
||||
Comment comment = commentRequest.toComment();
|
||||
comment.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
||||
comment.getSpec().setUserAgent(userAgentFrom(request));
|
||||
comment.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));
|
||||
return commentService.create(comment);
|
||||
})
|
||||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
|
@ -118,26 +117,10 @@ public class CommentEndpoint implements CustomEndpoint {
|
|||
.flatMap(replyRequest -> {
|
||||
Reply reply = replyRequest.toReply();
|
||||
reply.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
||||
reply.getSpec().setUserAgent(userAgentFrom(request));
|
||||
reply.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));
|
||||
return replyService.create(commentName, reply);
|
||||
})
|
||||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user-agent from server request.
|
||||
*
|
||||
* @param request server request
|
||||
* @return user-agent string if found, otherwise "unknown"
|
||||
*/
|
||||
public static String userAgentFrom(ServerRequest request) {
|
||||
HttpHeaders httpHeaders = request.headers().asHttpHeaders();
|
||||
// https://en.wikipedia.org/wiki/User_agent
|
||||
String userAgent = httpHeaders.getFirst(HttpHeaders.USER_AGENT);
|
||||
if (StringUtils.isBlank(userAgent)) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA
|
||||
userAgent = httpHeaders.getFirst("Sec-CH-UA");
|
||||
}
|
||||
return StringUtils.defaultString(userAgent, "unknown");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
/**
|
||||
* Stats endpoint.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class StatsEndpoint implements CustomEndpoint {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
public StatsEndpoint(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.console.halo.run/v1alpha1/Stats";
|
||||
return SpringdocRouteBuilder.route()
|
||||
.GET("stats", this::getStats, builder -> builder.operationId("getStats")
|
||||
.description("Get stats.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder()
|
||||
.implementation(Stats.class)
|
||||
)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
Mono<ServerResponse> getStats(ServerRequest request) {
|
||||
return client.list(Counter.class, null, null)
|
||||
.reduce(Stats.emptyStats(), (stats, counter) -> {
|
||||
stats.setVisits(stats.getVisits() + counter.getVisit());
|
||||
stats.setComments(stats.getComments() + counter.getTotalComment());
|
||||
stats.setApprovedComments(
|
||||
stats.getApprovedComments() + counter.getApprovedComment());
|
||||
stats.setUpvotes(stats.getUpvotes() + counter.getUpvote());
|
||||
return stats;
|
||||
})
|
||||
.flatMap(stats -> client.list(User.class,
|
||||
user -> user.getMetadata().getDeletionTimestamp() == null,
|
||||
null)
|
||||
.count()
|
||||
.map(count -> {
|
||||
stats.setUsers(count.intValue());
|
||||
return stats;
|
||||
}))
|
||||
.flatMap(stats -> client.list(Post.class, post -> !post.isDeleted(), null)
|
||||
.count()
|
||||
.map(count -> {
|
||||
stats.setPosts(count.intValue());
|
||||
return stats;
|
||||
})
|
||||
)
|
||||
.flatMap(stats -> ServerResponse.ok().bodyValue(stats));
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Stats {
|
||||
private Integer visits;
|
||||
private Integer comments;
|
||||
private Integer approvedComments;
|
||||
private Integer upvotes;
|
||||
private Integer users;
|
||||
private Integer posts;
|
||||
|
||||
/**
|
||||
* Creates an empty stats that populated initialize value.
|
||||
*
|
||||
* @return stats with initialize value.
|
||||
*/
|
||||
public static Stats emptyStats() {
|
||||
Stats stats = new Stats();
|
||||
stats.setVisits(0);
|
||||
stats.setComments(0);
|
||||
stats.setApprovedComments(0);
|
||||
stats.setUpvotes(0);
|
||||
stats.setUsers(0);
|
||||
stats.setPosts(0);
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.fn.builders.schema.Builder;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
import run.halo.app.infra.utils.IpAddressUtils;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
import run.halo.app.metrics.VisitLogWriter;
|
||||
|
||||
/**
|
||||
* Metrics counter endpoint.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class TrackerEndpoint implements CustomEndpoint {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
private final VisitLogWriter visitLogWriter;
|
||||
|
||||
public TrackerEndpoint(MeterRegistry meterRegistry, VisitLogWriter visitLogWriter) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.visitLogWriter = visitLogWriter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.halo.run/v1alpha1/Tracker";
|
||||
return SpringdocRouteBuilder.route()
|
||||
.POST("trackers/counter", this::increase,
|
||||
builder -> builder.operationId("count")
|
||||
.description("Count an extension resource visits.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(CounterRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Double.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> increase(ServerRequest request) {
|
||||
return request.bodyToMono(CounterRequest.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(new IllegalArgumentException("Counter request body must not be empty")))
|
||||
.map(counterRequest -> {
|
||||
String counterName =
|
||||
MeterUtils.nameOf(counterRequest.group(), counterRequest.plural(),
|
||||
counterRequest.name());
|
||||
|
||||
Counter counter = MeterUtils.visitCounter(meterRegistry, counterName);
|
||||
counter.increment();
|
||||
// async write visit log
|
||||
writeVisitLog(request, counterRequest);
|
||||
return (int) counter.count();
|
||||
})
|
||||
.flatMap(count -> ServerResponse.ok().bodyValue(count));
|
||||
}
|
||||
|
||||
public record CounterRequest(String group, String plural, String name, String hostname,
|
||||
String screen, String language, String referrer) {
|
||||
/**
|
||||
* Construct counter request.
|
||||
* group and session uid can be empty.
|
||||
*/
|
||||
public CounterRequest {
|
||||
Assert.notNull(plural, "The plural must not be null.");
|
||||
Assert.notNull(name, "The name must not be null.");
|
||||
group = StringUtils.defaultString(group);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeVisitLog(ServerRequest request, CounterRequest counterRequest) {
|
||||
String logMessage = logMessage(request, counterRequest);
|
||||
visitLogWriter.log(logMessage);
|
||||
}
|
||||
|
||||
private String logMessage(ServerRequest request, CounterRequest counterRequest) {
|
||||
String ipAddress = IpAddressUtils.getIpAddress(request);
|
||||
String hostname = counterRequest.hostname();
|
||||
String screen = counterRequest.screen();
|
||||
String language = counterRequest.language();
|
||||
String referrer = counterRequest.referrer();
|
||||
String userAgent = HaloUtils.userAgentFrom(request);
|
||||
String counterName =
|
||||
MeterUtils.nameOf(counterRequest.group(), counterRequest.plural(),
|
||||
counterRequest.name());
|
||||
return String.format(
|
||||
"subject=[%s], ipAddress=[%s], hostname=[%s], screen=[%s], language=[%s], "
|
||||
+ "referrer=[%s], userAgent=[%s]", counterName, ipAddress, hostname, screen,
|
||||
language, referrer, userAgent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return new GroupVersion("api.halo.run", "v1alpha1");
|
||||
}
|
||||
}
|
|
@ -1,17 +1,27 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.lang.Nullable;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
/**
|
||||
* Reconciler for {@link Comment}.
|
||||
|
@ -22,9 +32,14 @@ import run.halo.app.infra.utils.JsonUtils;
|
|||
public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
||||
public static final String FINALIZER_NAME = "comment-protection";
|
||||
private final ExtensionClient client;
|
||||
private final MeterRegistry meterRegistry;
|
||||
private final SchemeManager schemeManager;
|
||||
|
||||
public CommentReconciler(ExtensionClient client) {
|
||||
public CommentReconciler(ExtensionClient client, MeterRegistry meterRegistry,
|
||||
SchemeManager schemeManager) {
|
||||
this.client = client;
|
||||
this.meterRegistry = meterRegistry;
|
||||
this.schemeManager = schemeManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -37,6 +52,7 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
}
|
||||
addFinalizerIfNecessary(comment);
|
||||
reconcileStatus(request.name());
|
||||
reconcileCommentCount();
|
||||
return new Result(true, Duration.ofMinutes(1));
|
||||
})
|
||||
.orElseGet(() -> new Result(false, null));
|
||||
|
@ -95,6 +111,67 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
});
|
||||
}
|
||||
|
||||
private void reconcileCommentCount() {
|
||||
Map<Ref, List<RefCommentTuple>> map = client.list(Comment.class, null, null)
|
||||
.stream()
|
||||
.map(comment -> {
|
||||
boolean approved =
|
||||
Objects.equals(true, comment.getSpec().getApproved())
|
||||
&& !isDeleted(comment);
|
||||
return new RefCommentTuple(comment.getSpec().getSubjectRef(),
|
||||
comment.getMetadata().getName(), approved);
|
||||
})
|
||||
.collect(Collectors.groupingBy(RefCommentTuple::ref));
|
||||
map.forEach((ref, pairs) -> {
|
||||
GroupVersionKind groupVersionKind = groupVersionKind(ref);
|
||||
if (groupVersionKind == null) {
|
||||
return;
|
||||
}
|
||||
// approved total count
|
||||
long approvedTotalCount = pairs.stream()
|
||||
.filter(refCommentPair -> refCommentPair.approved)
|
||||
.count();
|
||||
// total count
|
||||
int totalCount = pairs.size();
|
||||
|
||||
schemeManager.fetch(groupVersionKind).ifPresent(scheme -> {
|
||||
String counterName = MeterUtils.nameOf(ref.getGroup(), scheme.plural(),
|
||||
ref.getName());
|
||||
// meter for total comment count
|
||||
calcTotalComments(totalCount, counterName);
|
||||
// meter for approved comment count
|
||||
calcApprovedComments(approvedTotalCount, counterName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void calcTotalComments(int totalCount, String counterName) {
|
||||
Counter totalCommentCounter =
|
||||
MeterUtils.totalCommentCounter(meterRegistry, counterName);
|
||||
double totalCountMeter = totalCommentCounter.count();
|
||||
double totalIncrement = totalCount - totalCountMeter;
|
||||
if (totalCountMeter + totalIncrement >= 0) {
|
||||
totalCommentCounter.increment(totalIncrement);
|
||||
} else {
|
||||
totalCommentCounter.increment(totalCountMeter * -1);
|
||||
}
|
||||
}
|
||||
|
||||
private void calcApprovedComments(long approvedTotalCount, String counterName) {
|
||||
Counter approvedCommentCounter =
|
||||
MeterUtils.approvedCommentCounter(meterRegistry, counterName);
|
||||
double approvedComments = approvedCommentCounter.count();
|
||||
double increment = approvedTotalCount - approvedCommentCounter.count();
|
||||
if (approvedComments + increment >= 0) {
|
||||
approvedCommentCounter.increment(increment);
|
||||
} else {
|
||||
approvedCommentCounter.increment(approvedComments * -1);
|
||||
}
|
||||
}
|
||||
|
||||
record RefCommentTuple(Ref ref, String name, boolean approved) {
|
||||
}
|
||||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String commentName) {
|
||||
client.fetch(Comment.class, commentName).ifPresent(comment -> {
|
||||
cleanUpResources(comment);
|
||||
|
@ -111,6 +188,27 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
.equals(reply.getSpec().getCommentName()),
|
||||
null)
|
||||
.forEach(client::delete);
|
||||
// decrement total comment count
|
||||
Ref subjectRef = comment.getSpec().getSubjectRef();
|
||||
GroupVersionKind groupVersionKind = groupVersionKind(subjectRef);
|
||||
if (groupVersionKind == null) {
|
||||
return;
|
||||
}
|
||||
schemeManager.fetch(groupVersionKind)
|
||||
.ifPresent(scheme -> {
|
||||
String counterName = MeterUtils.nameOf(subjectRef.getGroup(), scheme.plural(),
|
||||
subjectRef.getName());
|
||||
MeterUtils.totalCommentCounter(meterRegistry, counterName)
|
||||
.increment(-1);
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private GroupVersionKind groupVersionKind(Ref ref) {
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
return new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind());
|
||||
}
|
||||
|
||||
Comparator<Reply> defaultReplyComparator() {
|
||||
|
|
|
@ -6,6 +6,7 @@ import org.springframework.lang.NonNull;
|
|||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Menu;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
|
@ -62,5 +63,7 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
schemeManager.register(Policy.class);
|
||||
schemeManager.register(Attachment.class);
|
||||
schemeManager.register(PolicyTemplate.class);
|
||||
// metrics.halo.run
|
||||
schemeManager.register(Counter.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,11 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -31,4 +34,20 @@ public class HaloUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user-agent from server request.
|
||||
*
|
||||
* @param request server request
|
||||
* @return user-agent string if found, otherwise "unknown"
|
||||
*/
|
||||
public static String userAgentFrom(ServerRequest request) {
|
||||
HttpHeaders httpHeaders = request.headers().asHttpHeaders();
|
||||
// https://en.wikipedia.org/wiki/User_agent
|
||||
String userAgent = httpHeaders.getFirst(HttpHeaders.USER_AGENT);
|
||||
if (StringUtils.isBlank(userAgent)) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-CH-UA
|
||||
userAgent = httpHeaders.getFirst("Sec-CH-UA");
|
||||
}
|
||||
return StringUtils.defaultString(userAgent, "unknown");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* Counter meter handler for {@link Counter}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CounterMeterHandler implements DisposableBean {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public CounterMeterHandler(ReactiveExtensionClient client, MeterRegistry meterRegistry) {
|
||||
this.client = client;
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize counter meters from {@link Counter}.
|
||||
*
|
||||
* @param event application ready event
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public Mono<Void> onApplicationReady(ApplicationReadyEvent event) {
|
||||
return client.list(Counter.class, null, null)
|
||||
.map(counter -> {
|
||||
String name = counter.getMetadata().getName();
|
||||
// visit counter
|
||||
io.micrometer.core.instrument.Counter visitCounter =
|
||||
MeterUtils.visitCounter(meterRegistry, name);
|
||||
visitCounter.increment(nullSafe(counter.getVisit()));
|
||||
|
||||
// upvote counter
|
||||
io.micrometer.core.instrument.Counter upvoteCounter =
|
||||
MeterUtils.upvoteCounter(meterRegistry, name);
|
||||
upvoteCounter.increment(nullSafe(counter.getUpvote()));
|
||||
|
||||
// total comment counter
|
||||
io.micrometer.core.instrument.Counter totalCommentCounter =
|
||||
MeterUtils.totalCommentCounter(meterRegistry, name);
|
||||
totalCommentCounter.increment(nullSafe(counter.getTotalComment()));
|
||||
|
||||
// approved comment counter
|
||||
io.micrometer.core.instrument.Counter approvedCommentCounter =
|
||||
MeterUtils.approvedCommentCounter(meterRegistry, name);
|
||||
approvedCommentCounter.increment(nullSafe(counter.getApprovedComment()));
|
||||
return counter;
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
int nullSafe(Integer value) {
|
||||
return Objects.requireNonNullElse(value, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize memory counter meter to the database every minute.
|
||||
*/
|
||||
@Scheduled(cron = "0 0/1 * * * ?")
|
||||
public void counterPersistenceTask() {
|
||||
log.debug("Regularly synchronize counter meters to the database.");
|
||||
save().block();
|
||||
}
|
||||
|
||||
Mono<Void> save() {
|
||||
Map<String, List<Meter>> nameMeters = meterRegistry.getMeters().stream()
|
||||
.filter(meter -> meter instanceof io.micrometer.core.instrument.Counter)
|
||||
.filter(counter -> {
|
||||
Meter.Id id = counter.getId();
|
||||
return id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()) != null;
|
||||
})
|
||||
.collect(Collectors.groupingBy(meter -> meter.getId().getName()));
|
||||
Stream<Mono<Counter>> monoStream = nameMeters.entrySet().stream()
|
||||
.map(entry -> {
|
||||
String name = entry.getKey();
|
||||
List<Meter> meters = entry.getValue();
|
||||
return client.fetch(Counter.class, name)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
Counter counter = emptyCounter(name);
|
||||
return client.create(counter);
|
||||
}))
|
||||
.flatMap(counter -> {
|
||||
Counter oldCounter = JsonUtils.deepCopy(counter);
|
||||
counter.populateFrom(meters);
|
||||
if (oldCounter.equals(counter)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.just(counter);
|
||||
})
|
||||
.flatMap(client::update);
|
||||
});
|
||||
return Flux.fromStream(monoStream)
|
||||
.flatMap(Function.identity())
|
||||
.then();
|
||||
}
|
||||
|
||||
static Counter emptyCounter(String name) {
|
||||
Counter counter = new Counter();
|
||||
counter.setMetadata(new Metadata());
|
||||
counter.getMetadata().setName(name);
|
||||
counter.setUpvote(0);
|
||||
counter.setTotalComment(0);
|
||||
counter.setApprovedComment(0);
|
||||
counter.setVisit(0);
|
||||
return counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
log.debug("Persist counter meters to database before destroy...");
|
||||
save().block();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import run.halo.app.core.extension.Counter;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface CounterService {
|
||||
|
||||
Counter getByName(String counterName);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.util.Collection;
|
||||
import org.springframework.stereotype.Service;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
||||
/**
|
||||
* Counter service implementation.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Service
|
||||
public class CounterServiceImpl implements CounterService {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public CounterServiceImpl(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Counter getByName(String counterName) {
|
||||
Tag commonTag = MeterUtils.METRICS_COMMON_TAG;
|
||||
Collection<io.micrometer.core.instrument.Counter> counters = meterRegistry.find(counterName)
|
||||
.tag(commonTag.getKey(),
|
||||
valueMatch -> commonTag.getValue().equals(valueMatch))
|
||||
.counters();
|
||||
|
||||
Counter counter = emptyCounter(counterName);
|
||||
counter.populateFrom(counters);
|
||||
return counter;
|
||||
}
|
||||
|
||||
private Counter emptyCounter(String name) {
|
||||
Counter counter = new Counter();
|
||||
counter.setMetadata(new Metadata());
|
||||
counter.getMetadata().setName(name);
|
||||
return counter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
/**
|
||||
* Meter utils.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class MeterUtils {
|
||||
|
||||
public static final Tag METRICS_COMMON_TAG = Tag.of("metrics.halo.run", "true");
|
||||
public static final String SCENE = "scene";
|
||||
public static final String VISIT_SCENE = "visit";
|
||||
public static final String UPVOTE_SCENE = "upvote";
|
||||
public static final String TOTAL_COMMENT_SCENE = "total_comment";
|
||||
public static final String APPROVED_COMMENT_SCENE = "approved_comment";
|
||||
|
||||
/**
|
||||
* Build a counter name.
|
||||
*
|
||||
* @param group extension group
|
||||
* @param plural extension plural
|
||||
* @param name extension name
|
||||
* @return counter name
|
||||
*/
|
||||
public static String nameOf(String group, String plural, String name) {
|
||||
if (StringUtils.isBlank(group)) {
|
||||
return String.join("/", plural, name);
|
||||
}
|
||||
return String.join(".", plural, group) + "/" + name;
|
||||
}
|
||||
|
||||
public static <T extends AbstractExtension> String nameOf(Class<T> clazz, String name) {
|
||||
GVK annotation = clazz.getAnnotation(GVK.class);
|
||||
return nameOf(annotation.group(), annotation.plural(), name);
|
||||
}
|
||||
|
||||
public static Counter visitCounter(MeterRegistry registry, String name) {
|
||||
return counter(registry, name, Tag.of(SCENE, VISIT_SCENE));
|
||||
}
|
||||
|
||||
public static Counter upvoteCounter(MeterRegistry registry, String name) {
|
||||
return counter(registry, name, Tag.of(SCENE, UPVOTE_SCENE));
|
||||
}
|
||||
|
||||
public static Counter totalCommentCounter(MeterRegistry registry, String name) {
|
||||
return counter(registry, name, Tag.of(SCENE, TOTAL_COMMENT_SCENE));
|
||||
}
|
||||
|
||||
public static Counter approvedCommentCounter(MeterRegistry registry, String name) {
|
||||
return counter(registry, name, Tag.of(SCENE, APPROVED_COMMENT_SCENE));
|
||||
}
|
||||
|
||||
public static boolean isVisitCounter(Counter counter) {
|
||||
String sceneValue = counter.getId().getTag(SCENE);
|
||||
if (StringUtils.isBlank(sceneValue)) {
|
||||
return false;
|
||||
}
|
||||
return VISIT_SCENE.equals(sceneValue);
|
||||
}
|
||||
|
||||
public static boolean isUpvoteCounter(Counter counter) {
|
||||
String sceneValue = counter.getId().getTag(SCENE);
|
||||
if (StringUtils.isBlank(sceneValue)) {
|
||||
return false;
|
||||
}
|
||||
return UPVOTE_SCENE.equals(sceneValue);
|
||||
}
|
||||
|
||||
public static boolean isTotalCommentCounter(Counter counter) {
|
||||
String sceneValue = counter.getId().getTag(SCENE);
|
||||
if (StringUtils.isBlank(sceneValue)) {
|
||||
return false;
|
||||
}
|
||||
return TOTAL_COMMENT_SCENE.equals(sceneValue);
|
||||
}
|
||||
|
||||
public static boolean isApprovedCommentCounter(Counter counter) {
|
||||
String sceneValue = counter.getId().getTag(SCENE);
|
||||
if (StringUtils.isBlank(sceneValue)) {
|
||||
return false;
|
||||
}
|
||||
return APPROVED_COMMENT_SCENE.equals(sceneValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link Counter} for halo extension.
|
||||
*
|
||||
* @param registry meter registry
|
||||
* @param name counter name,build by {@link #nameOf(String, String, String)}
|
||||
* @return counter find by name from registry if exists, otherwise create a new one.
|
||||
*/
|
||||
private static Counter counter(MeterRegistry registry, String name, Tag... tags) {
|
||||
Tags withTags = Tags.of(METRICS_COMMON_TAG).and(tags);
|
||||
Counter counter = registry.find(name)
|
||||
.tags(withTags)
|
||||
.counter();
|
||||
if (counter == null) {
|
||||
return registry.counter(name, withTags);
|
||||
}
|
||||
return counter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
|
||||
/**
|
||||
* Visit log writer.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class VisitLogWriter implements InitializingBean, DisposableBean {
|
||||
private static final String LOG_FILE_NAME = "visits.log";
|
||||
private static final String LOG_FILE_LOCATION = "logs";
|
||||
private final AsyncLogWriter asyncLogWriter;
|
||||
private volatile boolean interruptThread = false;
|
||||
|
||||
private final Path logFilePath;
|
||||
|
||||
private VisitLogWriter(HaloProperties haloProperties) throws IOException {
|
||||
Path logsPath = haloProperties.getWorkDir()
|
||||
.resolve(LOG_FILE_LOCATION);
|
||||
if (!Files.exists(logsPath)) {
|
||||
Files.createDirectories(logsPath);
|
||||
}
|
||||
this.logFilePath = logsPath.resolve(LOG_FILE_NAME);
|
||||
this.asyncLogWriter = new AsyncLogWriter(logFilePath);
|
||||
}
|
||||
|
||||
public synchronized void log(String logMsg) {
|
||||
asyncLogWriter.put(logMsg);
|
||||
}
|
||||
|
||||
public Path getLogFilePath() {
|
||||
return logFilePath;
|
||||
}
|
||||
|
||||
void start() {
|
||||
log.debug("Starting write visit log...");
|
||||
Thread thread = new Thread(() -> {
|
||||
while (!interruptThread) {
|
||||
asyncLogWriter.writeLog();
|
||||
}
|
||||
}, "visits-log-writer");
|
||||
thread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
asyncLogWriter.close();
|
||||
interruptThread = true;
|
||||
}
|
||||
|
||||
static class AsyncLogWriter {
|
||||
private static final int MAX_LOG_SIZE = 10000;
|
||||
private static final int BATCH_SIZE = 10;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Condition fullCondition = lock.newCondition();
|
||||
private final Condition emptyCondition = lock.newCondition();
|
||||
private final BufferedOutputStream writer;
|
||||
private final Deque<String> logQueue;
|
||||
private final AtomicInteger logBatch = new AtomicInteger(0);
|
||||
|
||||
public AsyncLogWriter(Path logFilePath) {
|
||||
OutputStream outputStream;
|
||||
try {
|
||||
outputStream = Files.newOutputStream(logFilePath,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
this.writer = new BufferedOutputStream(outputStream);
|
||||
this.logQueue = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
public void writeLog() {
|
||||
lock.lock();
|
||||
try {
|
||||
// queue is empty, wait for new log
|
||||
while (logQueue.isEmpty()) {
|
||||
try {
|
||||
emptyCondition.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
String logMessage = logQueue.poll();
|
||||
writeToDisk(logMessage);
|
||||
log.debug("Consumption visit log message: [{}]", logMessage);
|
||||
// signal log producer
|
||||
fullCondition.signal();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void writeToDisk(String logMsg) {
|
||||
String format = String.format("%s %s\n", Instant.now(), logMsg);
|
||||
lock.lock();
|
||||
try {
|
||||
writer.write(format.getBytes(), 0, format.length());
|
||||
int size = logBatch.incrementAndGet();
|
||||
if (size >= BATCH_SIZE) {
|
||||
writer.flush();
|
||||
logBatch.set(0);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Record access log failure: ", ExceptionUtils.getRootCause(e));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String logMessage) {
|
||||
lock.lock();
|
||||
try {
|
||||
while (logQueue.size() == MAX_LOG_SIZE) {
|
||||
try {
|
||||
log.debug("Queue full, producer thread waiting...");
|
||||
fullCondition.await();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
// add log message to queue tail
|
||||
logQueue.add(logMessage);
|
||||
log.info("Production a log messages [{}]", logMessage);
|
||||
// signal consumer thread
|
||||
emptyCondition.signal();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
// ignore this
|
||||
}
|
||||
FileUtils.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,7 +68,7 @@ public class GlobalHeadInjectionProcessor extends AbstractElementModelProcessor
|
|||
getTemplateHeadProcessors(appCtx);
|
||||
for (TemplateHeadProcessor processor : templateHeadProcessors) {
|
||||
processor.process(context, modelToInsert, structureHandler)
|
||||
.subscribe();
|
||||
.block();
|
||||
}
|
||||
|
||||
// add to target model
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.model.IModelFactory;
|
||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.infra.properties.HaloProperties;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
|
||||
/**
|
||||
* Get {@link GroupVersionKind} and {@code plural} from the view model to construct tracker
|
||||
* script tag and insert it into the head tag.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class HaloTrackerProcessor implements TemplateHeadProcessor {
|
||||
|
||||
private final HaloProperties haloProperties;
|
||||
|
||||
public HaloTrackerProcessor(HaloProperties haloProperties) {
|
||||
this.haloProperties = haloProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler) {
|
||||
final IModelFactory modelFactory = context.getModelFactory();
|
||||
return Mono.just(getTrackerScript(context))
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.map(trackerScript -> {
|
||||
model.add(modelFactory.createText(trackerScript));
|
||||
return trackerScript;
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
private String getTrackerScript(ITemplateContext context) {
|
||||
String resourceName = (String) context.getVariable("name");
|
||||
String externalUrl = haloProperties.getExternalUrl().getPath();
|
||||
Object groupVersionKind = context.getVariable("groupVersionKind");
|
||||
Object plural = context.getVariable("plural");
|
||||
if (groupVersionKind == null || plural == null) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
if (!(groupVersionKind instanceof GroupVersionKind gvk)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
return trackerScript(externalUrl, gvk.group(), (String) plural, resourceName);
|
||||
}
|
||||
|
||||
private String trackerScript(String externalUrl, String group, String plural, String name) {
|
||||
String jsSrc = PathUtils.combinePath(externalUrl, "/halo-tracker.js");
|
||||
return """
|
||||
<script async defer src="%s" data-group="%s" data-plural="%s" data-name="%s"></script>
|
||||
""".formatted(jsSrc, group, plural, name);
|
||||
}
|
||||
}
|
|
@ -26,13 +26,13 @@ import run.halo.app.content.comment.ReplyRequest;
|
|||
import run.halo.app.content.comment.ReplyService;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.core.extension.endpoint.CommentEndpoint;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
import run.halo.app.infra.utils.HaloUtils;
|
||||
import run.halo.app.infra.utils.IpAddressUtils;
|
||||
import run.halo.app.theme.finders.CommentFinder;
|
||||
import run.halo.app.theme.finders.vo.CommentVo;
|
||||
|
@ -145,7 +145,7 @@ public class CommentFinderEndpoint implements CustomEndpoint {
|
|||
.flatMap(commentRequest -> {
|
||||
Comment comment = commentRequest.toComment();
|
||||
comment.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
||||
comment.getSpec().setUserAgent(CommentEndpoint.userAgentFrom(request));
|
||||
comment.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));
|
||||
return commentService.create(comment);
|
||||
})
|
||||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
|
@ -157,7 +157,7 @@ public class CommentFinderEndpoint implements CustomEndpoint {
|
|||
.flatMap(replyRequest -> {
|
||||
Reply reply = replyRequest.toReply();
|
||||
reply.getSpec().setIpAddress(IpAddressUtils.getIpAddress(request));
|
||||
reply.getSpec().setUserAgent(CommentEndpoint.userAgentFrom(request));
|
||||
reply.getSpec().setUserAgent(HaloUtils.userAgentFrom(request));
|
||||
return replyService.create(commentName, reply);
|
||||
})
|
||||
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
|
||||
|
|
|
@ -10,9 +10,12 @@ import org.apache.commons.lang3.ObjectUtils;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
|
@ -22,6 +25,7 @@ import run.halo.app.theme.finders.vo.CategoryVo;
|
|||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.Contributor;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.finders.vo.StatsVo;
|
||||
import run.halo.app.theme.finders.vo.TagVo;
|
||||
|
||||
/**
|
||||
|
@ -47,16 +51,19 @@ public class PostFinderImpl implements PostFinder {
|
|||
|
||||
private final ContributorFinder contributorFinder;
|
||||
|
||||
private final CounterService counterService;
|
||||
|
||||
public PostFinderImpl(ReactiveExtensionClient client,
|
||||
ContentService contentService,
|
||||
TagFinder tagFinder,
|
||||
CategoryFinder categoryFinder,
|
||||
ContributorFinder contributorFinder) {
|
||||
ContributorFinder contributorFinder, CounterService counterService) {
|
||||
this.client = client;
|
||||
this.contentService = contentService;
|
||||
this.tagFinder = tagFinder;
|
||||
this.categoryFinder = categoryFinder;
|
||||
this.contributorFinder = contributorFinder;
|
||||
this.counterService = counterService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -116,10 +123,23 @@ public class PostFinderImpl implements PostFinder {
|
|||
}
|
||||
List<PostVo> postVos = list.get()
|
||||
.map(this::getPostVo)
|
||||
.peek(this::populateStats)
|
||||
.toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), postVos);
|
||||
}
|
||||
|
||||
private void populateStats(PostVo postVo) {
|
||||
Counter counter =
|
||||
counterService.getByName(MeterUtils.nameOf(Post.class, postVo.getMetadata()
|
||||
.getName()));
|
||||
StatsVo statsVo = StatsVo.builder()
|
||||
.visit(counter.getVisit())
|
||||
.upvote(counter.getUpvote())
|
||||
.comment(counter.getApprovedComment())
|
||||
.build();
|
||||
postVo.setStats(statsVo);
|
||||
}
|
||||
|
||||
private PostVo getPostVo(@NonNull Post post) {
|
||||
List<TagVo> tags = tagFinder.getByNames(post.getSpec().getTags());
|
||||
List<CategoryVo> categoryVos = categoryFinder.getByNames(post.getSpec().getCategories());
|
||||
|
@ -129,6 +149,7 @@ public class PostFinderImpl implements PostFinder {
|
|||
postVo.setCategories(categoryVos);
|
||||
postVo.setTags(tags);
|
||||
postVo.setContributors(contributors);
|
||||
populateStats(postVo);
|
||||
return postVo;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,16 +8,20 @@ import java.util.function.Function;
|
|||
import java.util.function.Predicate;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import run.halo.app.content.ContentService;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.Finder;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.finders.vo.ContentVo;
|
||||
import run.halo.app.theme.finders.vo.Contributor;
|
||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||
import run.halo.app.theme.finders.vo.StatsVo;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link SinglePage}.
|
||||
|
@ -39,11 +43,14 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
|
||||
private final ContributorFinder contributorFinder;
|
||||
|
||||
private final CounterService counterService;
|
||||
|
||||
public SinglePageFinderImpl(ReactiveExtensionClient client, ContentService contentService,
|
||||
ContributorFinder contributorFinder) {
|
||||
ContributorFinder contributorFinder, CounterService counterService) {
|
||||
this.client = client;
|
||||
this.contentService = contentService;
|
||||
this.contributorFinder = contributorFinder;
|
||||
this.counterService = counterService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -58,6 +65,7 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
SinglePageVo pageVo = SinglePageVo.from(page);
|
||||
pageVo.setContributors(contributors);
|
||||
pageVo.setContent(content(pageName));
|
||||
populateStats(pageVo);
|
||||
return pageVo;
|
||||
}
|
||||
|
||||
|
@ -79,16 +87,29 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
if (list == null) {
|
||||
return new ListResult<>(0, 0, 0, List.of());
|
||||
}
|
||||
List<SinglePageVo> postVos = list.get()
|
||||
List<SinglePageVo> pageVos = list.get()
|
||||
.map(sp -> {
|
||||
List<Contributor> contributors =
|
||||
contributorFinder.getContributors(sp.getStatus().getContributors());
|
||||
SinglePageVo pageVo = SinglePageVo.from(sp);
|
||||
pageVo.setContributors(contributors);
|
||||
populateStats(pageVo);
|
||||
return pageVo;
|
||||
})
|
||||
.toList();
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), postVos);
|
||||
return new ListResult<>(list.getPage(), list.getSize(), list.getTotal(), pageVos);
|
||||
}
|
||||
|
||||
void populateStats(SinglePageVo pageVo) {
|
||||
String name = pageVo.getMetadata().getName();
|
||||
Counter counter =
|
||||
counterService.getByName(MeterUtils.nameOf(SinglePage.class, name));
|
||||
StatsVo statsVo = StatsVo.builder()
|
||||
.visit(counter.getVisit())
|
||||
.upvote(counter.getUpvote())
|
||||
.comment(counter.getApprovedComment())
|
||||
.build();
|
||||
pageVo.setStats(statsVo);
|
||||
}
|
||||
|
||||
static Comparator<SinglePage> defaultComparator() {
|
||||
|
|
|
@ -35,6 +35,8 @@ public class PostVo {
|
|||
|
||||
private List<Contributor> contributors;
|
||||
|
||||
private StatsVo stats;
|
||||
|
||||
/**
|
||||
* Convert {@link Post} to {@link PostVo}.
|
||||
*
|
||||
|
|
|
@ -29,6 +29,8 @@ public class SinglePageVo {
|
|||
|
||||
private ContentVo content;
|
||||
|
||||
private StatsVo stats;
|
||||
|
||||
private List<Contributor> contributors;
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
|
||||
/**
|
||||
* Stats value object.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
public class StatsVo {
|
||||
|
||||
Integer visit;
|
||||
|
||||
Integer upvote;
|
||||
|
||||
Integer comment;
|
||||
}
|
|
@ -21,6 +21,7 @@ import reactor.core.publisher.Mono;
|
|||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.content.permalinks.ExtensionLocator;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.PostFinder;
|
||||
|
@ -49,7 +50,7 @@ public class PostRouteStrategy implements TemplateRouterStrategy {
|
|||
public RouterFunction<ServerResponse> getRouteFunction(String template, String pattern) {
|
||||
PostRequestParamPredicate postParamPredicate =
|
||||
new PostRequestParamPredicate(pattern);
|
||||
|
||||
GVK gvk = Post.class.getAnnotation(GVK.class);
|
||||
if (postParamPredicate.isQueryParamPattern()) {
|
||||
String paramName = postParamPredicate.getParamName();
|
||||
String placeholderName = postParamPredicate.getPlaceholderName();
|
||||
|
@ -72,10 +73,14 @@ public class PostRouteStrategy implements TemplateRouterStrategy {
|
|||
if (name == null) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
GroupVersionKind groupVersionKind =
|
||||
new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind());
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.POST.getValue(),
|
||||
Map.of(PostRequestParamPredicate.NAME_PARAM, name,
|
||||
"post", postByName(name))
|
||||
"post", postByName(name),
|
||||
"groupVersionKind", groupVersionKind,
|
||||
"plural", gvk.plural())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -95,6 +100,8 @@ public class PostRouteStrategy implements TemplateRouterStrategy {
|
|||
model.putAll(pathMatchInfo.getUriVariables());
|
||||
}
|
||||
model.put("post", postByName(locator.name()));
|
||||
model.put("groupVersionKind", locator.gvk());
|
||||
model.put("plural", gvk.plural());
|
||||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.POST.getValue(), model);
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.core.extension.SinglePage;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
|
@ -60,10 +61,17 @@ public class SinglePageRouteStrategy implements TemplateRouterStrategy {
|
|||
return ServerResponse.ok()
|
||||
.render(DefaultTemplateEnum.SINGLE_PAGE.getValue(),
|
||||
Map.of("name", name,
|
||||
"groupVersionKind", gvk,
|
||||
"plural", getPlural(),
|
||||
"singlePage", singlePageByName(name)));
|
||||
});
|
||||
}
|
||||
|
||||
private String getPlural() {
|
||||
GVK annotation = SinglePage.class.getAnnotation(GVK.class);
|
||||
return annotation.plural();
|
||||
}
|
||||
|
||||
private Mono<SinglePageVo> singlePageByName(String name) {
|
||||
return Mono.defer(() -> Mono.just(singlePageFinder.getByName(name)))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
|
|
|
@ -9,3 +9,8 @@ rules:
|
|||
- apiGroups: [ "api.halo.run" ]
|
||||
resources: [ "comments", "comments/reply" ]
|
||||
verbs: [ "create", "get", "list" ]
|
||||
- apiGroups: [ "api.halo.run" ]
|
||||
resources: [ "*" ]
|
||||
verbs: [ "*" ]
|
||||
- nonResourceURLs: [ "/apis/api.halo.run/v1alpha1/trackers/counter" ]
|
||||
verbs: [ "post" ]
|
||||
|
|
|
@ -49,3 +49,15 @@ rules:
|
|||
resources: [ "users/password" ]
|
||||
resourceNames: [ "-" ]
|
||||
verbs: [ "update" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
metadata:
|
||||
name: role-template-stats
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
halo.run/hidden: "true"
|
||||
rules:
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "stats" ]
|
||||
verbs: [ "get", "list" ]
|
|
@ -0,0 +1 @@
|
|||
!function(){"use strict";!function(t){var e=t.screen,r=e.width,n=e.height,a=t.navigator.language,o=t.location,i=t.localStorage,c=t.document,u=t.history,l=o.hostname,s=o.pathname,p=o.search,f=c.currentScript;if(f){var h=function(t,e,r){var n=t[e];return function(){for(var e=[],a=arguments.length;a--;)e[a]=arguments[a];return r.apply(null,e),n.apply(t,e)}},d=function(){return i&&i.getItem("haloTracker.disabled")||T&&function(){var e=t.doNotTrack,r=t.navigator,n=t.external,a="msTrackingProtectionEnabled",o=e||r.doNotTrack||r.msDoNotTrack||n&&a in n&&n[a]();return"1"==o||"yes"===o}()||j&&!w.includes(l)},g="data-",v=f.getAttribute.bind(f),m=v(g+"group")||"",k=v(g+"plural"),y=v(g+"name"),S=v(g+"host-url"),b="false"!==v(g+"auto-track"),T=v(g+"do-not-track"),j=v(g+"domains")||"",w=j.split(",").map((function(t){return t.trim()})),E=(S?S.replace(/\/$/,""):f.src.split("/").slice(0,-1).join("/"))+"/apis/api.halo.run/v1alpha1/trackers/counter",N=r+"x"+n,O=""+s+p,x=c.referrer,P=function(t,e){return void 0===t&&(t=O),void 0===e&&(e=x),function(t){if(!d())return fetch(E,{method:"POST",body:JSON.stringify(Object.assign({},t)),headers:{"Content-Type":"application/json"}}).then((function(t){return t.text()})).then((function(t){console.debug("Visit count:",t)}))}((r={group:m,plural:k,name:y,hostname:l,screen:N,language:a,url:O},n={url:t,referrer:e},Object.keys(n).forEach((function(t){void 0!==n[t]&&(r[t]=n[t])})),r));var r,n},V=function(t,e,r){if(r){x=O;var n=r.toString();(O="http"===n.substring(0,4)?"/"+n.split("/").splice(3).join("/"):n)!==x&&P()}};if(!t.haloTracker){var A=function(t){return trackEvent(t)};A.trackView=P,t.haloTracker=A}if(b&&!d()){u.pushState=h(u,"pushState",V),u.replaceState=h(u,"replaceState",V);var C=function(){"complete"===c.readyState&&P()};c.addEventListener("readystatechange",C,!0),C()}}}(window)}();
|
|
@ -5,9 +5,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
|
@ -32,13 +32,9 @@ class PostServiceImplTest {
|
|||
@Mock
|
||||
private ContentService contentService;
|
||||
|
||||
@InjectMocks
|
||||
private PostServiceImpl postService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
postService = new PostServiceImpl(contentService, client);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listPredicate() {
|
||||
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
|
||||
|
|
|
@ -38,6 +38,7 @@ import run.halo.app.extension.ReactiveExtensionClient;
|
|||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.metrics.CounterMeterHandler;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureWebTestClient
|
||||
|
@ -56,6 +57,9 @@ class UserEndpointTest {
|
|||
@MockBean
|
||||
UserService userService;
|
||||
|
||||
@MockBean
|
||||
CounterMeterHandler counterMeterHandler;
|
||||
|
||||
@MockBean
|
||||
SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
|
|
|
@ -6,24 +6,37 @@ import static org.mockito.ArgumentMatchers.eq;
|
|||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import run.halo.app.core.extension.Comment;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.core.extension.Reply;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.Scheme;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link CommentReconciler}.
|
||||
|
@ -37,11 +50,19 @@ class CommentReconcilerTest {
|
|||
@Mock
|
||||
private ExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
private final MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
@Mock
|
||||
SchemeManager schemeManager;
|
||||
|
||||
private CommentReconciler commentReconciler;
|
||||
|
||||
private final Instant now = Instant.now();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
commentReconciler = new CommentReconciler(client, meterRegistry, schemeManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcile() {
|
||||
Comment comment = new Comment();
|
||||
|
@ -112,6 +133,94 @@ class CommentReconcilerTest {
|
|||
.contains(CommentReconciler.FINALIZER_NAME)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reconcileCommentCount() {
|
||||
when(client.list(eq(Comment.class), any(), any()))
|
||||
.thenReturn(commentList());
|
||||
when(client.fetch(eq(Comment.class), eq("test")))
|
||||
.thenReturn(Optional.of(getComment("test")));
|
||||
lenient().when(client.list(eq(Reply.class), any(), any()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
Ref ref = getRef();
|
||||
GroupVersionKind groupVersionKind =
|
||||
new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind());
|
||||
Scheme scheme = new Scheme(Post.class, groupVersionKind, "posts", "post",
|
||||
Mockito.mock(ObjectNode.class));
|
||||
when(schemeManager.fetch(any())).thenReturn(Optional.of(scheme));
|
||||
|
||||
String fakePostCounterName =
|
||||
MeterUtils.nameOf(ref.getGroup(), scheme.plural(), "fake-post");
|
||||
String testPostCounterName =
|
||||
MeterUtils.nameOf(ref.getGroup(), scheme.plural(), "test-post");
|
||||
|
||||
Counter approvedCommentCounter =
|
||||
MeterUtils.approvedCommentCounter(meterRegistry, fakePostCounterName);
|
||||
approvedCommentCounter.increment(5);
|
||||
|
||||
assertThat(approvedCommentCounter.count()).isEqualTo(5.0);
|
||||
|
||||
Counter testPostCounter =
|
||||
MeterUtils.approvedCommentCounter(meterRegistry, testPostCounterName);
|
||||
testPostCounter.increment(0);
|
||||
assertThat(testPostCounter.count()).isEqualTo(0.0);
|
||||
|
||||
Counter totalCommentCounter =
|
||||
MeterUtils.totalCommentCounter(meterRegistry, fakePostCounterName);
|
||||
totalCommentCounter.increment(8);
|
||||
|
||||
commentReconciler.reconcile(new Reconciler.Request("test"));
|
||||
assertThat(approvedCommentCounter.count()).isEqualTo(3.0);
|
||||
assertThat(testPostCounter.count()).isEqualTo(1.0);
|
||||
assertThat(totalCommentCounter.count()).isEqualTo(5.0);
|
||||
}
|
||||
|
||||
List<Comment> commentList() {
|
||||
final List<Comment> comments = new ArrayList<>();
|
||||
final Comment commentA = getComment("A");
|
||||
final Comment commentB = getComment("B");
|
||||
final Comment commentC = getComment("C");
|
||||
|
||||
Comment commentD = getComment("D");
|
||||
commentD.getSpec().getSubjectRef().setName("test-post");
|
||||
|
||||
final Comment commentE = getComment("E");
|
||||
commentE.getSpec().setApproved(false);
|
||||
|
||||
final Comment commentF = getComment("F");
|
||||
commentF.getSpec().setApproved(false);
|
||||
|
||||
comments.add(commentA);
|
||||
comments.add(commentB);
|
||||
comments.add(commentC);
|
||||
comments.add(commentD);
|
||||
comments.add(commentE);
|
||||
comments.add(commentF);
|
||||
return comments;
|
||||
}
|
||||
|
||||
private Comment getComment(String name) {
|
||||
final Ref ref = getRef();
|
||||
Comment comment = new Comment();
|
||||
comment.setMetadata(new Metadata());
|
||||
comment.getMetadata().setName(name);
|
||||
comment.setSpec(new Comment.CommentSpec());
|
||||
comment.getSpec().setSubjectRef(ref);
|
||||
comment.getSpec().setApproved(true);
|
||||
comment.getSpec().setLastReadTime(now.plusSeconds(5));
|
||||
comment.setStatus(new Comment.CommentStatus());
|
||||
return comment;
|
||||
}
|
||||
|
||||
private static Ref getRef() {
|
||||
Ref ref = new Ref();
|
||||
ref.setGroup("content.halo.run");
|
||||
ref.setVersion("v1alpha1");
|
||||
ref.setKind("Post");
|
||||
ref.setName("fake-post");
|
||||
return ref;
|
||||
}
|
||||
|
||||
List<Reply> replyList() {
|
||||
Reply replyA = new Reply();
|
||||
replyA.setMetadata(new Metadata());
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.Post;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
/**
|
||||
* Tests for {@link CounterMeterHandler}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CounterMeterHandlerTest {
|
||||
|
||||
@Mock
|
||||
private ReactiveExtensionClient client;
|
||||
|
||||
private final String counterName = MeterUtils.nameOf(Post.class, "fake-post");
|
||||
|
||||
@InjectMocks
|
||||
private CounterMeterHandler counterMeterHandler;
|
||||
|
||||
private MeterRegistry meterRegistry;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
meterRegistry = new SimpleMeterRegistry();
|
||||
counterMeterHandler = new CounterMeterHandler(client, meterRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onApplicationReady() {
|
||||
Counter counter = new Counter();
|
||||
counter.setMetadata(new Metadata());
|
||||
counter.getMetadata().setName(counterName);
|
||||
counter.setVisit(1);
|
||||
counter.setUpvote(3);
|
||||
counter.setTotalComment(5);
|
||||
counter.setApprovedComment(2);
|
||||
|
||||
Mockito.when(client.list(eq(Counter.class), any(), any()))
|
||||
.thenReturn(Flux.just(counter));
|
||||
|
||||
counterMeterHandler.onApplicationReady(Mockito.mock(ApplicationReadyEvent.class))
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
|
||||
assertThat((int) meterRegistry.find(counterName)
|
||||
.tag(MeterUtils.SCENE, value -> value.equals(MeterUtils.VISIT_SCENE))
|
||||
.counter().count()).isEqualTo(counter.getVisit());
|
||||
|
||||
assertThat((int) meterRegistry.find(counterName)
|
||||
.tag(MeterUtils.SCENE, value -> value.equals(MeterUtils.UPVOTE_SCENE))
|
||||
.counter().count()).isEqualTo(counter.getUpvote());
|
||||
assertThat((int) meterRegistry.find(counterName)
|
||||
.tag(MeterUtils.SCENE, value -> value.equals(MeterUtils.TOTAL_COMMENT_SCENE))
|
||||
.counter().count()).isEqualTo(counter.getTotalComment());
|
||||
assertThat((int) meterRegistry.find(counterName)
|
||||
.tag(MeterUtils.SCENE, value -> value.equals(MeterUtils.APPROVED_COMMENT_SCENE))
|
||||
.counter().count()).isEqualTo(counter.getApprovedComment());
|
||||
}
|
||||
|
||||
@Test
|
||||
void save() {
|
||||
MeterUtils.visitCounter(meterRegistry, counterName).increment(2);
|
||||
MeterUtils.upvoteCounter(meterRegistry, counterName).increment(3);
|
||||
|
||||
Mockito.when(client.create(any()))
|
||||
.thenReturn(Mono.just(CounterMeterHandler.emptyCounter(counterName)));
|
||||
Mockito.when(client.fetch(eq(Counter.class), eq(counterName)))
|
||||
.thenReturn(Mono.empty());
|
||||
Mockito.when(client.update(any(Counter.class)))
|
||||
.thenAnswer(a -> {
|
||||
ArgumentCaptor<Counter> captor =
|
||||
ArgumentCaptor.forClass(Counter.class);
|
||||
Mockito.verify(client, Mockito.times(1)).update(captor.capture());
|
||||
Counter value = captor.getValue();
|
||||
assertThat(value.getVisit()).isEqualTo(2);
|
||||
assertThat(value.getUpvote()).isEqualTo(3);
|
||||
assertThat(value.getTotalComment()).isEqualTo(0);
|
||||
assertThat(value.getApprovedComment()).isEqualTo(0);
|
||||
return Mono.just(value);
|
||||
});
|
||||
counterMeterHandler.save()
|
||||
.as(StepVerifier::create)
|
||||
.verifyComplete();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.core.extension.Post;
|
||||
|
||||
/**
|
||||
* Tests for {@link CounterServiceImpl}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class CounterServiceImplTest {
|
||||
|
||||
private CounterServiceImpl counterService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
SimpleMeterRegistry simpleMeterRegistry = new SimpleMeterRegistry();
|
||||
counterService = new CounterServiceImpl(simpleMeterRegistry);
|
||||
String counterName = MeterUtils.nameOf(Post.class, "fake-post");
|
||||
MeterUtils.visitCounter(simpleMeterRegistry,
|
||||
counterName).increment();
|
||||
|
||||
MeterUtils.approvedCommentCounter(simpleMeterRegistry, counterName)
|
||||
.increment(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getByName() {
|
||||
run.halo.app.core.extension.Counter counter =
|
||||
counterService.getByName(MeterUtils.nameOf(Post.class, "fake-post"));
|
||||
assertThat(counter.getVisit()).isEqualTo(1);
|
||||
assertThat(counter.getUpvote()).isEqualTo(0);
|
||||
assertThat(counter.getTotalComment()).isEqualTo(0);
|
||||
assertThat(counter.getApprovedComment()).isEqualTo(2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.search.RequiredSearch;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.core.extension.Post;
|
||||
|
||||
/**
|
||||
* Tests for {@link MeterUtils}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class MeterUtilsTest {
|
||||
|
||||
@Test
|
||||
void nameOf() {
|
||||
String s = MeterUtils.nameOf(Post.class, "fake-post");
|
||||
assertThat(s).isEqualTo("posts.content.halo.run/fake-post");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNameOf() {
|
||||
String s = MeterUtils.nameOf("content.halo.run", "posts", "fake-post");
|
||||
assertThat(s).isEqualTo("posts.content.halo.run/fake-post");
|
||||
}
|
||||
|
||||
@Test
|
||||
void visitCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
MeterUtils.visitCounter(meterRegistry, "posts.content.halo.run/fake-post")
|
||||
.increment();
|
||||
RequiredSearch requiredSearch = meterRegistry.get("posts.content.halo.run/fake-post");
|
||||
assertThat(requiredSearch.counter().count()).isEqualTo(1);
|
||||
Meter.Id id = requiredSearch.counter().getId();
|
||||
assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.VISIT_SCENE);
|
||||
assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))
|
||||
.isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void upvoteCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
MeterUtils.upvoteCounter(meterRegistry, "posts.content.halo.run/fake-post")
|
||||
.increment(2);
|
||||
RequiredSearch requiredSearch = meterRegistry.get("posts.content.halo.run/fake-post");
|
||||
assertThat(requiredSearch.counter().count()).isEqualTo(2);
|
||||
Meter.Id id = requiredSearch.counter().getId();
|
||||
assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.UPVOTE_SCENE);
|
||||
assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))
|
||||
.isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void totalCommentCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
MeterUtils.totalCommentCounter(meterRegistry, "content.halo.run.posts.fake-post")
|
||||
.increment(3);
|
||||
RequiredSearch requiredSearch = meterRegistry.get("content.halo.run.posts.fake-post");
|
||||
assertThat(requiredSearch.counter().count()).isEqualTo(3);
|
||||
Meter.Id id = requiredSearch.counter().getId();
|
||||
assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.TOTAL_COMMENT_SCENE);
|
||||
assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))
|
||||
.isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedCommentCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
MeterUtils.approvedCommentCounter(meterRegistry, "posts.content.halo.run/fake-post")
|
||||
.increment(2);
|
||||
RequiredSearch requiredSearch = meterRegistry.get("posts.content.halo.run/fake-post");
|
||||
assertThat(requiredSearch.counter().count()).isEqualTo(2);
|
||||
Meter.Id id = requiredSearch.counter().getId();
|
||||
assertThat(id.getTag(MeterUtils.SCENE)).isEqualTo(MeterUtils.APPROVED_COMMENT_SCENE);
|
||||
assertThat(id.getTag(MeterUtils.METRICS_COMMON_TAG.getKey()))
|
||||
.isEqualTo(MeterUtils.METRICS_COMMON_TAG.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isVisitCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
Counter visitCounter =
|
||||
MeterUtils.visitCounter(meterRegistry, "posts.content.halo.run/fake-post");
|
||||
assertThat(MeterUtils.isVisitCounter(visitCounter)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isUpvoteCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
Counter upvoteCounter =
|
||||
MeterUtils.upvoteCounter(meterRegistry, "posts.content.halo.run/fake-post");
|
||||
assertThat(MeterUtils.isUpvoteCounter(upvoteCounter)).isTrue();
|
||||
assertThat(MeterUtils.isVisitCounter(upvoteCounter)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isTotalCommentCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
Counter totalCommentCounter =
|
||||
MeterUtils.totalCommentCounter(meterRegistry, "posts.content.halo.run/fake-post");
|
||||
assertThat(MeterUtils.isTotalCommentCounter(totalCommentCounter)).isTrue();
|
||||
assertThat(MeterUtils.isVisitCounter(totalCommentCounter)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isApprovedCommentCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
Counter approvedCommentCounter =
|
||||
MeterUtils.approvedCommentCounter(meterRegistry, "posts.content.halo.run/fake-post");
|
||||
assertThat(MeterUtils.isApprovedCommentCounter(approvedCommentCounter)).isTrue();
|
||||
assertThat(MeterUtils.isVisitCounter(approvedCommentCounter)).isFalse();
|
||||
}
|
||||
}
|
|
@ -7,9 +7,9 @@ import static org.mockito.Mockito.when;
|
|||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
@ -47,14 +47,9 @@ class PostFinderImplTest {
|
|||
@Mock
|
||||
private ContributorFinder contributorFinder;
|
||||
|
||||
@InjectMocks
|
||||
private PostFinderImpl postFinder;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
postFinder = new PostFinderImpl(client, contentService, tagFinder, categoryFinder,
|
||||
contributorFinder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void content() {
|
||||
Post post = post(1);
|
||||
|
|
Loading…
Reference in New Issue