6 changed files with 321 additions and 0 deletions
@ -0,0 +1,27 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.dao.util; |
||||
|
|
||||
|
import java.lang.annotation.ElementType; |
||||
|
import java.lang.annotation.Retention; |
||||
|
import java.lang.annotation.RetentionPolicy; |
||||
|
import java.lang.annotation.Target; |
||||
|
|
||||
|
@Target(ElementType.METHOD) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
public @interface TenantDbCall { |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.dao.aspect; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.springframework.data.util.Pair; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.ConcurrentMap; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
import java.util.concurrent.atomic.AtomicLong; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Data |
||||
|
public class DbCallStats { |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final ConcurrentMap<String, Pair<AtomicInteger, AtomicLong>> methodStats = new ConcurrentHashMap<>(); |
||||
|
private final AtomicInteger successCalls = new AtomicInteger(); |
||||
|
private final AtomicInteger failureCalls = new AtomicInteger(); |
||||
|
|
||||
|
public void onMethodCall(String methodName, boolean success, long executionTime) { |
||||
|
var pair = methodStats.computeIfAbsent(methodName, |
||||
|
m -> Pair.of(new AtomicInteger(0), new AtomicLong(0L))); |
||||
|
pair.getFirst().incrementAndGet(); |
||||
|
pair.getSecond().addAndGet(executionTime); |
||||
|
if (success) { |
||||
|
successCalls.incrementAndGet(); |
||||
|
} else { |
||||
|
failureCalls.incrementAndGet(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public DbCallStatsSnapshot snapshot() { |
||||
|
return DbCallStatsSnapshot.builder() |
||||
|
.tenantId(tenantId) |
||||
|
.totalSuccess(successCalls.get()) |
||||
|
.totalFailure(failureCalls.get()) |
||||
|
.methodExecutions(methodStats.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getFirst().get()))) |
||||
|
.methodTimings(methodStats.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getSecond().get()))) |
||||
|
.totalTiming(methodStats.values().stream().map(Pair::getSecond).map(AtomicLong::get).reduce(0L, Long::sum)) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.dao.aspect; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Data |
||||
|
@Builder |
||||
|
public class DbCallStatsSnapshot { |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final int totalSuccess; |
||||
|
private final int totalFailure; |
||||
|
private final long totalTiming; |
||||
|
private final Map<String, Integer> methodExecutions; |
||||
|
private final Map<String, Long> methodTimings; |
||||
|
|
||||
|
public int getTotalCalls() { |
||||
|
return totalSuccess + totalFailure; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,190 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.dao.aspect; |
||||
|
|
||||
|
import com.google.common.util.concurrent.FutureCallback; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.google.common.util.concurrent.MoreExecutors; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.aspectj.lang.ProceedingJoinPoint; |
||||
|
import org.aspectj.lang.annotation.Around; |
||||
|
import org.aspectj.lang.annotation.Aspect; |
||||
|
import org.checkerframework.checker.nullness.qual.Nullable; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.scheduling.annotation.Scheduled; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.Comparator; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.ConcurrentMap; |
||||
|
import java.util.function.Consumer; |
||||
|
import java.util.function.Function; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Aspect |
||||
|
@ConditionalOnProperty(prefix = "sql", value = "log_tenant_stats", havingValue = "true") |
||||
|
@Component |
||||
|
@Slf4j |
||||
|
public class TenantDbCallAspect { |
||||
|
|
||||
|
private final Set<String> invalidTenantDbCallMethods = ConcurrentHashMap.newKeySet(); |
||||
|
private final ConcurrentMap<TenantId, DbCallStats> statsMap = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@Scheduled(fixedDelayString = "${sql.log_tenant_stats.log_tenant_stats_interval:60000}") |
||||
|
public void printStats() { |
||||
|
try { |
||||
|
if (log.isTraceEnabled()) { |
||||
|
List<DbCallStatsSnapshot> snapshots = snapshot(); |
||||
|
logTopNTenants(snapshots, Comparator.comparing(DbCallStatsSnapshot::getTotalTiming).reversed(), 0, snapshot -> { |
||||
|
log.trace("[{}]: calls: {}, exec time: {} ", snapshot.getTenantId(), snapshot.getTotalCalls(), snapshot.getTotalTiming()); |
||||
|
snapshot.getMethodExecutions().forEach((method, count) -> { |
||||
|
log.trace("[{}]: method: {}, count: {}, exec time: {}", |
||||
|
snapshot.getTenantId(), method, count, snapshot.getMethodTimings().getOrDefault(method, 0L)); |
||||
|
}); |
||||
|
}); |
||||
|
// todo: log top 10 tenants for each method sorted by number of execution.
|
||||
|
} else if (log.isDebugEnabled()) { |
||||
|
List<DbCallStatsSnapshot> snapshots = snapshot(); |
||||
|
log.debug("Total calls statistics below:"); |
||||
|
logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalCalls).reversed(), |
||||
|
10, s -> logSnapshotWithDebugLevel(s, 10)); |
||||
|
log.debug("Total timing statistics below:"); |
||||
|
logTopNTenants(snapshots, Comparator.comparingLong(DbCallStatsSnapshot::getTotalTiming).reversed(), |
||||
|
10, s -> logSnapshotWithDebugLevel(s, 10)); |
||||
|
log.debug("Total errors statistics below:"); |
||||
|
logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), |
||||
|
10, s -> logSnapshotWithDebugLevel(s, 10)); |
||||
|
} else if (log.isInfoEnabled()) { |
||||
|
log.debug("Total calls statistics below:"); |
||||
|
List<DbCallStatsSnapshot> snapshots = snapshot(); |
||||
|
logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), |
||||
|
3, s -> logSnapshotWithDebugLevel(s, 3)); |
||||
|
} |
||||
|
} finally { |
||||
|
statsMap.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void logSnapshotWithDebugLevel(DbCallStatsSnapshot snapshot, int limit) { |
||||
|
log.debug("[{}]: calls: {}, failures: {}, exec time: {} ", |
||||
|
snapshot.getTenantId(), snapshot.getTotalCalls(), snapshot.getTotalFailure(), snapshot.getTotalTiming()); |
||||
|
var stream = snapshot.getMethodTimings().entrySet().stream() |
||||
|
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())); |
||||
|
if (limit > 0) { |
||||
|
stream = stream.limit(limit); |
||||
|
} |
||||
|
stream.forEach(e -> { |
||||
|
long timing = snapshot.getMethodTimings().getOrDefault(e.getKey(), 0L); |
||||
|
log.debug("[{}]: method: {}, count: {}, exec time: {}", snapshot.getTenantId(), e.getKey(), e.getValue(), timing); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private List<DbCallStatsSnapshot> snapshot() { |
||||
|
return statsMap.values().stream().map(DbCallStats::snapshot).collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
// private void logTopNMethods(List<DbCallStatsSnapshot> snapshots, Comparator<DbCallStatsSnapshot> comparator,
|
||||
|
// int n, Consumer<DbCallStatsSnapshot> logFunction) {
|
||||
|
//// var stream = snapshots.stream().sorted(comparator).sorted();
|
||||
|
// // find top methods by execution time and then top
|
||||
|
// if (n > 0) {
|
||||
|
// stream = stream.limit(n);
|
||||
|
// }
|
||||
|
// stream.forEach(logFunction);
|
||||
|
// }
|
||||
|
|
||||
|
private void logTopNTenants(List<DbCallStatsSnapshot> snapshots, Comparator<DbCallStatsSnapshot> comparator, |
||||
|
int n, Consumer<DbCallStatsSnapshot> logFunction) { |
||||
|
var stream = snapshots.stream().sorted(comparator).sorted(); |
||||
|
if (n > 0) { |
||||
|
stream = stream.limit(n); |
||||
|
} |
||||
|
stream.forEach(logFunction); |
||||
|
} |
||||
|
|
||||
|
@SuppressWarnings({"rawtypes", "unchecked"}) |
||||
|
@Around("@annotation(org.thingsboard.server.dao.util.TenantDbCall)") |
||||
|
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { |
||||
|
var signature = joinPoint.getSignature(); |
||||
|
var method = signature.toShortString(); |
||||
|
if (invalidTenantDbCallMethods.contains(method)) { |
||||
|
//Simply call the method if tenant is not found
|
||||
|
return joinPoint.proceed(); |
||||
|
} |
||||
|
var tenantId = getTenantId(method, joinPoint.getArgs()); |
||||
|
if (tenantId == null) { |
||||
|
//Simply call the method if tenant is null
|
||||
|
return joinPoint.proceed(); |
||||
|
} |
||||
|
var startTime = System.currentTimeMillis(); |
||||
|
try { |
||||
|
var result = joinPoint.proceed(); |
||||
|
if (result instanceof ListenableFuture) { |
||||
|
Futures.addCallback((ListenableFuture) result, |
||||
|
new FutureCallback<>() { |
||||
|
@Override |
||||
|
public void onSuccess(@Nullable Object result) { |
||||
|
logTenantMethodExecution(tenantId, method, true, startTime); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onFailure(Throwable t) { |
||||
|
logTenantMethodExecution(tenantId, method, false, startTime); |
||||
|
} |
||||
|
}, |
||||
|
MoreExecutors.directExecutor()); |
||||
|
} else { |
||||
|
logTenantMethodExecution(tenantId, method, true, startTime); |
||||
|
} |
||||
|
return result; |
||||
|
} catch (Throwable t) { |
||||
|
logTenantMethodExecution(tenantId, method, false, startTime); |
||||
|
throw t; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void logTenantMethodExecution(TenantId tenantId, String method, boolean success, long startTime) { |
||||
|
statsMap.computeIfAbsent(tenantId, DbCallStats::new) |
||||
|
.onMethodCall(method, success, System.currentTimeMillis() - startTime); |
||||
|
} |
||||
|
|
||||
|
TenantId getTenantId(String methodName, Object[] args) { |
||||
|
if (args == null || args.length == 0) { |
||||
|
addAndLogInvalidMethods(methodName); |
||||
|
return null; |
||||
|
} |
||||
|
for (Object arg : args) { |
||||
|
if (arg instanceof TenantId) { |
||||
|
log.debug("Method: {} is annotated with @TenantDbCall but the TenantId is null. Args: {}", methodName, Arrays.toString(args)); |
||||
|
return (TenantId) arg; |
||||
|
} |
||||
|
} |
||||
|
addAndLogInvalidMethods(methodName); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private void addAndLogInvalidMethods(String methodName) { |
||||
|
log.warn("Method: {} is annotated with @TenantDbCall but no TenantId in args", methodName); |
||||
|
invalidTenantDbCallMethods.add(methodName); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue