committed by
GitHub
46 changed files with 475 additions and 0 deletions
@ -0,0 +1,26 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Target(ElementType.TYPE) |
|||
public @interface SqlDao { |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
/** |
|||
* 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, MethodCallStats> 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 methodCallStats = methodStats.computeIfAbsent(methodName, m -> new MethodCallStats()); |
|||
methodCallStats.getExecutions().incrementAndGet(); |
|||
methodCallStats.getTiming().addAndGet(executionTime); |
|||
if (success) { |
|||
successCalls.incrementAndGet(); |
|||
} else { |
|||
failureCalls.incrementAndGet(); |
|||
methodCallStats.getFailures().incrementAndGet(); |
|||
} |
|||
} |
|||
|
|||
public DbCallStatsSnapshot snapshot() { |
|||
return DbCallStatsSnapshot.builder() |
|||
.tenantId(tenantId) |
|||
.totalSuccess(successCalls.get()) |
|||
.totalFailure(failureCalls.get()) |
|||
.methodStats(methodStats.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().snapshot()))) |
|||
.totalTiming(methodStats.values().stream().map(MethodCallStats::getTiming).map(AtomicLong::get).reduce(0L, Long::sum)) |
|||
.build(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* 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, MethodCallStatsSnapshot> methodStats; |
|||
|
|||
public int getTotalCalls() { |
|||
return totalSuccess + totalFailure; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* 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 java.util.concurrent.atomic.AtomicInteger; |
|||
import java.util.concurrent.atomic.AtomicLong; |
|||
|
|||
@Data |
|||
public class MethodCallStats { |
|||
private final AtomicInteger executions = new AtomicInteger(); |
|||
private final AtomicInteger failures = new AtomicInteger(); |
|||
private final AtomicLong timing = new AtomicLong(); |
|||
|
|||
public MethodCallStatsSnapshot snapshot() { |
|||
return new MethodCallStatsSnapshot(executions.get(), failures.get(), timing.get()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* 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; |
|||
|
|||
@Data |
|||
public class MethodCallStatsSnapshot { |
|||
private final int executions; |
|||
private final int failures; |
|||
private final long timing; |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
/** |
|||
* 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.apache.commons.lang3.ArrayUtils; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.apache.commons.lang3.exception.ExceptionUtils; |
|||
import org.aspectj.lang.ProceedingJoinPoint; |
|||
import org.aspectj.lang.annotation.Around; |
|||
import org.aspectj.lang.annotation.Aspect; |
|||
import org.aspectj.lang.reflect.MethodSignature; |
|||
import org.checkerframework.checker.nullness.qual.Nullable; |
|||
import org.hibernate.exception.JDBCConnectionException; |
|||
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.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.function.Consumer; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Aspect |
|||
@ConditionalOnProperty(prefix = "sql", value = "log_tenant_stats", havingValue = "true") |
|||
@Component |
|||
@Slf4j |
|||
public class SqlDaoCallsAspect { |
|||
|
|||
private final Set<String> invalidTenantDbCallMethods = ConcurrentHashMap.newKeySet(); |
|||
private final ConcurrentMap<TenantId, DbCallStats> statsMap = new ConcurrentHashMap<>(); |
|||
|
|||
@Scheduled(initialDelayString = "${sql.log_tenant_stats_interval_ms:60000}", |
|||
fixedDelayString = "${sql.log_tenant_stats_interval_ms:60000}") |
|||
public void printStats() { |
|||
List<DbCallStatsSnapshot> snapshots = snapshot(); |
|||
if (snapshots.isEmpty()) return; |
|||
try { |
|||
if (log.isTraceEnabled()) { |
|||
logTopNTenants(snapshots, Comparator.comparing(DbCallStatsSnapshot::getTotalTiming).reversed(), 0, snapshot -> { |
|||
logSnapshot(snapshot, 0, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), log::trace); |
|||
}); |
|||
|
|||
Map<String, Map<TenantId, MethodCallStatsSnapshot>> byMethodStats = new HashMap<>(); |
|||
for (DbCallStatsSnapshot snapshot : snapshots) { |
|||
snapshot.getMethodStats().forEach((method, stats) -> { |
|||
byMethodStats.computeIfAbsent(method, m -> new HashMap<>()) |
|||
.put(snapshot.getTenantId(), stats); |
|||
}); |
|||
} |
|||
byMethodStats.forEach((method, byTenantStats) -> { |
|||
log.trace("Top tenants for method {} by calls:", method); |
|||
byTenantStats.entrySet().stream() |
|||
.sorted(Map.Entry.comparingByValue(Comparator.comparing(MethodCallStatsSnapshot::getExecutions).reversed())) |
|||
.limit(10) |
|||
.forEach(e -> { |
|||
TenantId tenantId = e.getKey(); |
|||
MethodCallStatsSnapshot methodStats = e.getValue(); |
|||
log.trace("[{}] calls: {}, failures: {}, timing: {}", tenantId, |
|||
methodStats.getExecutions(), methodStats.getFailures(), methodStats.getTiming()); |
|||
}); |
|||
}); |
|||
} else if (log.isDebugEnabled()) { |
|||
log.debug("Total calls statistics below:"); |
|||
logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalCalls).reversed(), 10, |
|||
s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getExecutions).reversed(), log::debug)); |
|||
log.debug("Total timing statistics below:"); |
|||
logTopNTenants(snapshots, Comparator.comparingLong(DbCallStatsSnapshot::getTotalTiming).reversed(), |
|||
10, s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getTiming).reversed(), log::debug)); |
|||
log.debug("Total errors statistics below:"); |
|||
logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), |
|||
10, s -> logSnapshot(s, 10, Comparator.comparing(MethodCallStatsSnapshot::getFailures).reversed(), log::debug)); |
|||
} else if (log.isInfoEnabled()) { |
|||
log.info("Total calls statistics below:"); |
|||
logTopNTenants(snapshots, Comparator.comparingInt(DbCallStatsSnapshot::getTotalFailure).reversed(), |
|||
3, s -> logSnapshot(s, 3, Comparator.comparing(MethodCallStatsSnapshot::getFailures).reversed(), log::info)); |
|||
} |
|||
} finally { |
|||
statsMap.clear(); |
|||
} |
|||
} |
|||
|
|||
private void logSnapshot(DbCallStatsSnapshot snapshot, int limit, Comparator<MethodCallStatsSnapshot> methodStatsComparator, Consumer<String> logger) { |
|||
logger.accept(String.format("[%s]: calls: %s, failures: %s, exec time: %s ", |
|||
snapshot.getTenantId(), snapshot.getTotalCalls(), snapshot.getTotalFailure(), snapshot.getTotalTiming())); |
|||
var stream = snapshot.getMethodStats().entrySet().stream() |
|||
.sorted(Map.Entry.comparingByValue(methodStatsComparator)); |
|||
if (limit > 0) { |
|||
stream = stream.limit(limit); |
|||
} |
|||
stream.forEach(e -> { |
|||
MethodCallStatsSnapshot methodStats = e.getValue(); |
|||
logger.accept(String.format("[%s]: method: %s, calls: %s, failures: %s, exec time: %s", snapshot.getTenantId(), e.getKey(), |
|||
methodStats.getExecutions(), methodStats.getFailures(), methodStats.getTiming())); |
|||
}); |
|||
} |
|||
|
|||
private List<DbCallStatsSnapshot> snapshot() { |
|||
return statsMap.values().stream().map(DbCallStats::snapshot).collect(Collectors.toList()); |
|||
} |
|||
|
|||
private void logTopNTenants(List<DbCallStatsSnapshot> snapshots, Comparator<DbCallStatsSnapshot> comparator, |
|||
int n, Consumer<DbCallStatsSnapshot> logFunction) { |
|||
var stream = snapshots.stream().sorted(comparator); |
|||
if (n > 0) { |
|||
stream = stream.limit(n); |
|||
} |
|||
stream.forEach(logFunction); |
|||
} |
|||
|
|||
@SuppressWarnings({"rawtypes", "unchecked"}) |
|||
@Around("@within(org.thingsboard.server.dao.util.SqlDao)") |
|||
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { |
|||
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); |
|||
var methodName = signature.toShortString(); |
|||
if (invalidTenantDbCallMethods.contains(methodName)) { |
|||
//Simply call the method if tenant is not found
|
|||
return joinPoint.proceed(); |
|||
} |
|||
var tenantId = getTenantId(signature, methodName, joinPoint.getArgs()); |
|||
if (tenantId == null || tenantId.isNullUid()) { |
|||
//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, methodName, true, startTime, null); |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(Throwable t) { |
|||
logTenantMethodExecution(tenantId, methodName, false, startTime, t); |
|||
} |
|||
}, |
|||
MoreExecutors.directExecutor()); |
|||
} else { |
|||
logTenantMethodExecution(tenantId, methodName, true, startTime, null); |
|||
} |
|||
return result; |
|||
} catch (Throwable t) { |
|||
logTenantMethodExecution(tenantId, methodName, false, startTime, t); |
|||
throw t; |
|||
} |
|||
} |
|||
|
|||
private void logTenantMethodExecution(TenantId tenantId, String method, boolean success, long startTime, Throwable t) { |
|||
if (!success && ExceptionUtils.indexOfThrowable(t, JDBCConnectionException.class) >= 0) { |
|||
return; |
|||
} |
|||
statsMap.computeIfAbsent(tenantId, DbCallStats::new) |
|||
.onMethodCall(method, success, System.currentTimeMillis() - startTime); |
|||
} |
|||
|
|||
TenantId getTenantId(MethodSignature signature, String methodName, Object[] args) { |
|||
if (args == null || args.length == 0) { |
|||
addAndLogInvalidMethods(methodName); |
|||
return null; |
|||
} |
|||
for (int i = 0; i < args.length; i++) { |
|||
Object arg = args[i]; |
|||
if (arg instanceof TenantId) { |
|||
return (TenantId) arg; |
|||
} else if (arg instanceof UUID) { |
|||
if (signature.getParameterNames() != null && StringUtils.equals(signature.getParameterNames()[i], "tenantId")) { |
|||
log.trace("Method {} uses UUID for tenantId param instead of TenantId class", methodName); |
|||
return TenantId.fromUUID((UUID) arg); |
|||
} |
|||
} |
|||
} |
|||
if (ArrayUtils.contains(signature.getParameterTypes(), TenantId.class) || |
|||
ArrayUtils.contains(signature.getParameterNames(), "tenantId")) { |
|||
log.debug("Null was submitted as tenantId to method {}. Args: {}", methodName, Arrays.toString(args)); |
|||
} else { |
|||
addAndLogInvalidMethods(methodName); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private void addAndLogInvalidMethods(String methodName) { |
|||
invalidTenantDbCallMethods.add(methodName); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue