fix prod runtime logging and room discovery

This commit is contained in:
hy001 2026-04-29 14:47:46 +08:00
parent 096e37612e
commit e2e59eb1b2
6 changed files with 277 additions and 162 deletions

View File

@ -2,8 +2,3 @@ spring:
data:
mongodb:
uri: ${LIKEI_MONGO_URI}
logging:
level:
org.springframework.data.mongodb.core.MongoTemplate: DEBUG

View File

@ -20,7 +20,6 @@ import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
@ -39,6 +38,7 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -57,6 +57,8 @@ import java.util.zip.GZIPInputStream;
@RequiredArgsConstructor
public class ApiLoggingFilter implements GlobalFilter, Ordered {
private static final int MAX_RESPONSE_BODY_LOG_LENGTH = 500;
private final LoggingUploadService loggingUploadService;
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults()
.messageReaders();
@ -164,40 +166,17 @@ public class ApiLoggingFilter implements GlobalFilter, Ordered {
.map(HttpStatusCode::value)
.map(Objects::toString)
.orElse("?"));
if (body instanceof Flux) {
if (body instanceof Flux && !isSuccessResponse(response)) {
// 获取响应类型如果是 json 就打印
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (isJsonResponse(exchange, response)) {
return super.writeWith(DataBufferUtils.join(Flux.from(body)).map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
DataBufferUtils.release(dataBuffer);
if (StringUtils.isNotBlank(originalResponseContentType) && originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 合并多个流集合解决返回体分段传输
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
// 释放掉内存
DataBufferUtils.release(join);
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
try (GZIPInputStream in = new GZIPInputStream(new java.io.ByteArrayInputStream(content))) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
String responseResult = out.toString(StandardCharsets.UTF_8);
// 截取500个字符
responseResult = responseResult.substring(0, Math.min(responseResult.length(), 500));
String responseResult = readResponseBody(exchange, content);
responseResult = responseResult.substring(0, Math.min(responseResult.length(), MAX_RESPONSE_BODY_LOG_LENGTH));
loggingResponse.setBody(responseResult);
} catch (IOException e) {
log.error("Failed to decompress data", e);
}
return bufferFactory.wrap(content);
}));
}
@ -207,6 +186,52 @@ public class ApiLoggingFilter implements GlobalFilter, Ordered {
};
}
private boolean isSuccessResponse(ServerHttpResponse response) {
HttpStatusCode statusCode = response.getStatusCode();
return statusCode == null || statusCode.is2xxSuccessful();
}
private boolean isJsonResponse(ServerWebExchange exchange, ServerHttpResponse response) {
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (StringUtils.isNotBlank(originalResponseContentType) && originalResponseContentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
return true;
}
MediaType contentType = response.getHeaders().getContentType();
return contentType != null && MediaType.APPLICATION_JSON.isCompatibleWith(contentType);
}
private String readResponseBody(ServerWebExchange exchange, byte[] content) {
if (!isGzipResponse(exchange, content)) {
return new String(content, StandardCharsets.UTF_8);
}
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(content))) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
return out.toString(StandardCharsets.UTF_8);
} catch (IOException e) {
log.warn("Failed to read gzip response body for logging: {}", e.getMessage());
return "";
}
}
private boolean isGzipResponse(ServerWebExchange exchange, byte[] content) {
boolean gzipHeader = exchange.getResponse().getHeaders().getOrEmpty(HttpHeaders.CONTENT_ENCODING)
.stream()
.anyMatch(value -> StringUtils.isNotBlank(value) && value.toLowerCase().contains("gzip"));
return gzipHeader || hasGzipMagic(content);
}
private boolean hasGzipMagic(byte[] content) {
return content != null
&& content.length >= 2
&& (content[0] & 0xff) == 0x1f
&& (content[1] & 0xff) == 0x8b;
}
/**
* 请求装饰器重新计算 headers.
*/

View File

@ -24,7 +24,7 @@ public class LoggingUploadService {
private String getTopic() {
log.info("profileActive: {}", profileActive);
if (Objects.equals(profileActive, "prod")) {
return "4d467d31-9165-42c6-b80a-2ea23ae7777e";
return "4fd8ef93-1c0c-4802-a999-9268831a3ee5";
}
// if (Objects.equals(profileActive, "dev")) {

View File

@ -2,7 +2,6 @@ package com.red.circle.other.app.command.room.query;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.common.business.enums.SVIPLevelEnum;
import com.red.circle.live.inner.endpoint.LiveMicClient;
@ -31,15 +30,13 @@ import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileD
import com.red.circle.other.inner.model.cmd.live.AppActiveVoiceRoomQryCmd;
import com.red.circle.other.inner.model.dto.live.ActiveVoiceRoomCO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@ -72,18 +69,29 @@ public class RoomVoiceDiscoverQryExe {
private static final String EMPTY_ROOM_CACHE_KEY = "empty_room_cache:*";
private static final String MEMBER_QUANTITY_EXT_KEY = "memberQuantity";
private static final int DISCOVERY_LIMIT = 50;
private static final long DISCOVERY_SLOW_STEP_MILLIS = 100L;
private static final long DISCOVERY_SLOW_TOTAL_MILLIS = 300L;
private static final long EMPTY_ROOM_SCAN_COUNT = 200L;
public List<RoomVoiceProfileCO> execute(AppExtCommand cmd) {
boolean allRegionWhiteListUser = roomRegionFilterCommon.canViewAllRegions(cmd.requiredReqUserId());
RegionConfig currentRegion = roomRegionFilterCommon.requireRegionConfig(cmd.requiredReqUserId());
String region = currentRegion.getRegionCode();
UserProfile userProfile = userProfileGateway.getByUserId(cmd.requiredReqUserId());
long totalStart = System.currentTimeMillis();
long stepStart = totalStart;
Long reqUserId = cmd.requiredReqUserId();
String sysOrigin = cmd.requireReqSysOrigin();
String key = discoveryCacheKey(cmd.requireReqSysOrigin(), region, allRegionWhiteListUser);
boolean allRegionWhiteListUser = roomRegionFilterCommon.canViewAllRegions(reqUserId);
RegionConfig currentRegion = roomRegionFilterCommon.requireRegionConfig(reqUserId);
String region = currentRegion.getRegionCode();
UserProfile userProfile = userProfileGateway.getByUserId(reqUserId);
stepStart = logDiscoveryStep("prepare", stepStart, reqUserId, sysOrigin, region);
String key = discoveryCacheKey(sysOrigin, region, allRegionWhiteListUser);
String regionsRoom = regionRoomCacheService.getRegionsRoom(key);
stepStart = logDiscoveryStep("read-region-cache", stepStart, reqUserId, sysOrigin, region);
if (Objects.nonNull(regionsRoom)) {
List<RoomVoiceProfileCO> roomList = JSON.parseArray(regionsRoom, RoomVoiceProfileCO.class);
if (CollectionUtils.isEmpty(roomList)) {
logDiscoveryTotal(totalStart, reqUserId, sysOrigin, region, allRegionWhiteListUser, true, 0);
return CollectionUtils.newArrayList();
}
List<RoomVoiceProfileCO> availableRooms = roomVoiceProfileCommon
@ -91,32 +99,36 @@ public class RoomVoiceDiscoverQryExe {
if (availableRooms.size() != roomList.size()) {
regionRoomCacheService.remove(key);
}
stepStart = logDiscoveryStep("filter-cache", stepStart, reqUserId, sysOrigin, region);
logDiscoveryTotal(totalStart, reqUserId, sysOrigin, region, allRegionWhiteListUser, true, availableRooms.size());
return availableRooms;
}
List<ActiveVoiceRoom> activeVoiceRooms = allRegionWhiteListUser
? activeVoiceRoomService.listDiscover(
cmd.requireReqSysOrigin(),
sysOrigin,
true,
region,
Optional.ofNullable(userProfile).map(UserProfile::getCountryCode).orElse(null),
DISCOVERY_LIMIT
)
: roomRegionFilterCommon.filterActiveVoiceRoomsByRegion(
activeVoiceRoomService.listByRegion(cmd.requireReqSysOrigin(), region, DISCOVERY_LIMIT),
activeVoiceRoomService.listByRegion(sysOrigin, region, DISCOVERY_LIMIT),
region
);
stepStart = logDiscoveryStep("query-active-rooms", stepStart, reqUserId, sysOrigin, region);
// 查询没人的房间缓存加入进去
List<ActiveVoiceRoom> emptyRooms = getEmptyRoomsFromCache();
stepStart = logDiscoveryStep("scan-empty-room-cache", stepStart, reqUserId, sysOrigin, region);
if (CollectionUtils.isNotEmpty(emptyRooms)) {
List<ActiveVoiceRoom> matchedEmptyRooms = allRegionWhiteListUser
? emptyRooms.stream()
.filter(room -> Objects.equals(cmd.requireReqSysOrigin(), room.getSysOrigin()))
.filter(room -> Objects.equals(sysOrigin, room.getSysOrigin()))
.toList()
: roomRegionFilterCommon.filterActiveVoiceRoomsByRegion(
emptyRooms.stream()
.filter(room -> Objects.equals(cmd.requireReqSysOrigin(), room.getSysOrigin()))
.filter(room -> Objects.equals(sysOrigin, room.getSysOrigin()))
.toList(),
region
);
@ -131,10 +143,13 @@ public class RoomVoiceDiscoverQryExe {
activeVoiceRooms = allRooms;
}
}
stepStart = logDiscoveryStep("merge-empty-rooms", stepStart, reqUserId, sysOrigin, region);
List<RoomVoiceProfileCO> roomList = roomVoiceProfileCommon.toListRoomVoiceProfileCO(activeVoiceRooms);
stepStart = logDiscoveryStep("build-profile-list", stepStart, reqUserId, sysOrigin, region);
if (roomList.size() < DISCOVERY_LIMIT) {
List<RoomProfileManager> fallbackRoomManagersSource = roomProfileManagerService.listSelectiveLimitByCountryCodes(
cmd.requireReqSysOrigin(),
sysOrigin,
allRegionWhiteListUser ? List.of() : roomRegionFilterCommon.listCountryCodes(currentRegion),
roomList.stream().map(RoomVoiceProfileCO::getId).toList(),
DISCOVERY_LIMIT
@ -148,9 +163,12 @@ public class RoomVoiceDiscoverQryExe {
roomList = mergeRoomList(roomList, roomVoiceProfileCommon.listRoomVoiceProfilesV2(fallbackRoomManagers),
DISCOVERY_LIMIT);
}
stepStart = logDiscoveryStep("fallback-room-profiles", stepStart, reqUserId, sysOrigin, region);
roomList = sortByMemberQuantityDesc(roomList);
stepStart = logDiscoveryStep("sort-rooms", stepStart, reqUserId, sysOrigin, region);
if (CollectionUtils.isEmpty(roomList)) {
logDiscoveryTotal(totalStart, reqUserId, sysOrigin, region, allRegionWhiteListUser, false, 0);
return CollectionUtils.newArrayList();
}
Map<Long, SVIPLevelEnum> svipLevelEnumMap = userSVipGateway.checkSVipIdentity(
@ -161,11 +179,17 @@ public class RoomVoiceDiscoverQryExe {
room.setUserSVipLevel(svipLevelEnumMap.get(room.getUserId()));
})
.toList();
stepStart = logDiscoveryStep("fill-svip", stepStart, reqUserId, sysOrigin, region);
// 填充火箭状态
fillRocketStatus(roomList);
stepStart = logDiscoveryStep("fill-rocket-status", stepStart, reqUserId, sysOrigin, region);
// 填充游戏图标
fillGameIcon(roomList);
stepStart = logDiscoveryStep("fill-game-icon", stepStart, reqUserId, sysOrigin, region);
saveDiscoveryCache(key, roomList, reqUserId, sysOrigin, region);
logDiscoveryStep("write-region-cache", stepStart, reqUserId, sysOrigin, region);
logDiscoveryTotal(totalStart, reqUserId, sysOrigin, region, allRegionWhiteListUser, false, roomList.size());
return roomList;
}
@ -201,6 +225,38 @@ public class RoomVoiceDiscoverQryExe {
return allRegionWhiteListUser ? sysOrigin + region + ":ALL_REGION" : sysOrigin + region;
}
private void saveDiscoveryCache(String key, List<RoomVoiceProfileCO> roomList, Long userId,
String sysOrigin, String region) {
if (CollectionUtils.isEmpty(roomList)) {
return;
}
try {
regionRoomCacheService.saveRegionsRoom(JSON.toJSONString(roomList), key);
} catch (Exception e) {
log.warn("写入房间发现缓存失败, key={}, userId={}, sysOrigin={}, region={}",
key, userId, sysOrigin, region, e);
}
}
private long logDiscoveryStep(String step, long stepStart, Long userId, String sysOrigin, String region) {
long now = System.currentTimeMillis();
long cost = now - stepStart;
if (cost >= DISCOVERY_SLOW_STEP_MILLIS) {
log.warn("房间发现慢步骤, step={}, cost={}ms, userId={}, sysOrigin={}, region={}",
step, cost, userId, sysOrigin, region);
}
return now;
}
private void logDiscoveryTotal(long totalStart, Long userId, String sysOrigin, String region,
boolean allRegionWhiteListUser, boolean cacheHit, int roomCount) {
long cost = System.currentTimeMillis() - totalStart;
if (cost >= DISCOVERY_SLOW_TOTAL_MILLIS) {
log.warn("房间发现总耗时慢, cost={}ms, userId={}, sysOrigin={}, region={}, allRegion={}, cacheHit={}, roomCount={}",
cost, userId, sysOrigin, region, allRegionWhiteListUser, cacheHit, roomCount);
}
}
private List<RoomVoiceProfileCO> mergeRoomList(List<RoomVoiceProfileCO> currentRooms,
List<RoomVoiceProfileCO> fallbackRooms, int limit) {
if (CollectionUtils.isEmpty(fallbackRooms)) {
@ -350,7 +406,7 @@ public class RoomVoiceDiscoverQryExe {
*/
private List<ActiveVoiceRoom> getEmptyRoomsFromCache() {
try {
Set<String> keys = redisTemplate.keys(EMPTY_ROOM_CACHE_KEY);
Set<String> keys = scanEmptyRoomKeys();
if (CollectionUtils.isEmpty(keys)) {
return new ArrayList<>();
}
@ -380,6 +436,23 @@ public class RoomVoiceDiscoverQryExe {
}
}
private Set<String> scanEmptyRoomKeys() {
Set<String> keys = new HashSet<>();
ScanOptions options = ScanOptions.scanOptions()
.match(EMPTY_ROOM_CACHE_KEY)
.count(EMPTY_ROOM_SCAN_COUNT)
.build();
try (var cursor = redisTemplate.scan(options)) {
if (Objects.isNull(cursor)) {
return keys;
}
while (cursor.hasNext()) {
keys.add(Objects.toString(cursor.next(), ""));
}
}
return keys;
}
/**
* 将ActiveVoiceRoomCO转换为ActiveVoiceRoom
*

View File

@ -47,6 +47,8 @@ public class RocketEnergyAggregator {
private final StringRedisTemplate redisTemplate;
@Value("${spring.profiles.active}")
private String activeProfile;
@Value("${rocket.energy.enabled:false}")
private boolean rocketEnergyEnabled;
/**
* Redis 队列 Key 前缀
@ -96,6 +98,9 @@ public class RocketEnergyAggregator {
* 接收能量增加请求写入Redis队列
*/
public void addEnergy(RocketEnergyAddCmd cmd) {
if (!rocketEnergyEnabled) {
return;
}
try {
Long roomId = cmd.getRoomId();
String queueKey = getKey(String.valueOf(roomId)) ;
@ -124,6 +129,9 @@ public class RocketEnergyAggregator {
*/
@Scheduled(fixedDelay = FLUSH_INTERVAL)
public void flushToDatabase() {
if (!rocketEnergyEnabled) {
return;
}
try {
// 获取所有活跃房间
Set<String> activeRooms = redisTemplate.opsForSet().members(getActiveRoomsKey());
@ -355,6 +363,9 @@ public class RocketEnergyAggregator {
*/
@Scheduled(fixedRate = 3600000)
public void cleanupExpiredData() {
if (!rocketEnergyEnabled) {
return;
}
try {
// 清理本地推送时间缓存
Set<String> activeRooms = redisTemplate.opsForSet().members(getActiveRoomsKey());
@ -382,6 +393,9 @@ public class RocketEnergyAggregator {
*/
@javax.annotation.PreDestroy
public void shutdown() {
if (!rocketEnergyEnabled) {
return;
}
log.info("应用关闭,强制刷新所有待处理能量");
try {
flushToDatabase();
@ -394,6 +408,10 @@ public class RocketEnergyAggregator {
* 手动触发刷新供运维使用
*/
public void manualFlush() {
if (!rocketEnergyEnabled) {
log.info("火箭能量聚合未启用,跳过手动刷新");
return;
}
log.info("手动触发刷新");
flushToDatabase();
}
@ -403,6 +421,10 @@ public class RocketEnergyAggregator {
*/
public Map<String, Object> getQueueStatus() {
Map<String, Object> status = new HashMap<>();
status.put("enabled", rocketEnergyEnabled);
if (!rocketEnergyEnabled) {
return status;
}
Set<String> activeRooms = redisTemplate.opsForSet().members(getActiveRoomsKey());
status.put("activeRoomCount", activeRooms != null ? activeRooms.size() : 0);

View File

@ -8,7 +8,7 @@ public enum LogTopic {
/**
* 钱包金币资产.
*/
WALLET_GOLD_ASSET_PROD("45b22e07-3cc4-41e5-944c-ebda65330e39"),
WALLET_GOLD_ASSET_PROD("656b398e-9713-4de9-8975-b6fac77a3f61"),
WALLET_GOLD_ASSET_DEVELOP("931e4d86-fde3-455e-b225-59f83ce30235");
private final String topic;