committed by
GitHub
19 changed files with 819 additions and 20 deletions
@ -0,0 +1,19 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.common.msg.gateway.metrics; |
||||
|
|
||||
|
public record GatewayMetadata(String connector, long receivedTs, long publishedTs) { |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.transport.mqtt; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
||||
|
|
||||
|
import java.lang.annotation.Retention; |
||||
|
import java.lang.annotation.RetentionPolicy; |
||||
|
|
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled}'=='true')") |
||||
|
public @interface TbMqttTransportComponent { |
||||
|
} |
||||
@ -0,0 +1,113 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.transport.mqtt.gateway; |
||||
|
|
||||
|
import jakarta.annotation.PostConstruct; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.transport.TransportService; |
||||
|
import org.thingsboard.server.common.transport.TransportServiceCallback; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos; |
||||
|
import org.thingsboard.server.queue.scheduler.SchedulerComponent; |
||||
|
import org.thingsboard.server.transport.mqtt.TbMqttTransportComponent; |
||||
|
import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata; |
||||
|
import org.thingsboard.server.transport.mqtt.gateway.metrics.GatewayMetricsState; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
@TbMqttTransportComponent |
||||
|
public class GatewayMetricsService { |
||||
|
|
||||
|
public static final String GATEWAY_METRICS = "gatewayMetrics"; |
||||
|
|
||||
|
@Value("${transport.mqtt.gateway_metrics_report_interval_sec:60}") |
||||
|
private int metricsReportIntervalSec; |
||||
|
|
||||
|
@Autowired |
||||
|
private SchedulerComponent scheduler; |
||||
|
|
||||
|
@Autowired |
||||
|
private TransportService transportService; |
||||
|
|
||||
|
private Map<DeviceId, GatewayMetricsState> states = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@PostConstruct |
||||
|
private void init() { |
||||
|
scheduler.scheduleAtFixedRate(this::reportMetrics, metricsReportIntervalSec, metricsReportIntervalSec, TimeUnit.SECONDS); |
||||
|
} |
||||
|
|
||||
|
public void process(TransportProtos.SessionInfoProto sessionInfo, DeviceId gatewayId, List<GatewayMetadata> data, long serverReceiveTs) { |
||||
|
states.computeIfAbsent(gatewayId, k -> new GatewayMetricsState(sessionInfo)).update(data, serverReceiveTs); |
||||
|
} |
||||
|
|
||||
|
public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceId gatewayId) { |
||||
|
var state = states.get(gatewayId); |
||||
|
if (state != null) { |
||||
|
state.updateSessionInfo(sessionInfo); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void onDeviceDelete(DeviceId deviceId) { |
||||
|
states.remove(deviceId); |
||||
|
} |
||||
|
|
||||
|
public void reportMetrics() { |
||||
|
if (states.isEmpty()) { |
||||
|
return; |
||||
|
} |
||||
|
Map<DeviceId, GatewayMetricsState> statesToReport = states; |
||||
|
states = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
long ts = System.currentTimeMillis(); |
||||
|
|
||||
|
statesToReport.forEach((gatewayId, state) -> { |
||||
|
reportMetrics(state, ts); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void reportMetrics(GatewayMetricsState state, long ts) { |
||||
|
if (state.isEmpty()) { |
||||
|
return; |
||||
|
} |
||||
|
var result = state.getStateResult(); |
||||
|
var kvProto = TransportProtos.KeyValueProto.newBuilder() |
||||
|
.setKey(GATEWAY_METRICS) |
||||
|
.setType(TransportProtos.KeyValueType.JSON_V) |
||||
|
.setJsonV(JacksonUtil.toString(result)) |
||||
|
.build(); |
||||
|
|
||||
|
TransportProtos.TsKvListProto tsKvList = TransportProtos.TsKvListProto.newBuilder() |
||||
|
.setTs(ts) |
||||
|
.addKv(kvProto) |
||||
|
.build(); |
||||
|
|
||||
|
TransportProtos.PostTelemetryMsg telemetryMsg = TransportProtos.PostTelemetryMsg.newBuilder() |
||||
|
.addTsKvList(tsKvList) |
||||
|
.build(); |
||||
|
|
||||
|
transportService.process(state.getSessionInfo(), telemetryMsg, TransportServiceCallback.EMPTY); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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.transport.mqtt.gateway.metrics; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
import java.util.concurrent.atomic.AtomicLong; |
||||
|
import java.util.concurrent.locks.Lock; |
||||
|
import java.util.concurrent.locks.ReentrantLock; |
||||
|
|
||||
|
public class GatewayMetricsState { |
||||
|
|
||||
|
private final Map<String, ConnectorMetricsState> connectors; |
||||
|
private final Lock updateLock; |
||||
|
|
||||
|
@Getter |
||||
|
private volatile TransportProtos.SessionInfoProto sessionInfo; |
||||
|
|
||||
|
public GatewayMetricsState(TransportProtos.SessionInfoProto sessionInfo) { |
||||
|
this.connectors = new HashMap<>(); |
||||
|
this.updateLock = new ReentrantLock(); |
||||
|
this.sessionInfo = sessionInfo; |
||||
|
} |
||||
|
|
||||
|
public void updateSessionInfo(TransportProtos.SessionInfoProto sessionInfo) { |
||||
|
this.sessionInfo = sessionInfo; |
||||
|
} |
||||
|
|
||||
|
public void update(List<GatewayMetadata> metricsData, long serverReceiveTs) { |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
metricsData.forEach(data -> { |
||||
|
connectors.computeIfAbsent(data.connector(), k -> new ConnectorMetricsState()).update(data, serverReceiveTs); |
||||
|
}); |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Map<String, ConnectorMetricsResult> getStateResult() { |
||||
|
Map<String, ConnectorMetricsResult> result = new HashMap<>(); |
||||
|
updateLock.lock(); |
||||
|
try { |
||||
|
connectors.forEach((name, state) -> result.put(name, state.getResult())); |
||||
|
connectors.clear(); |
||||
|
} finally { |
||||
|
updateLock.unlock(); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public boolean isEmpty() { |
||||
|
return connectors.isEmpty(); |
||||
|
} |
||||
|
|
||||
|
private static class ConnectorMetricsState { |
||||
|
private final AtomicInteger count; |
||||
|
private final AtomicLong gwLatencySum; |
||||
|
private final AtomicLong transportLatencySum; |
||||
|
private volatile long minGwLatency; |
||||
|
private volatile long maxGwLatency; |
||||
|
private volatile long minTransportLatency; |
||||
|
private volatile long maxTransportLatency; |
||||
|
|
||||
|
private ConnectorMetricsState() { |
||||
|
this.count = new AtomicInteger(0); |
||||
|
this.gwLatencySum = new AtomicLong(0); |
||||
|
this.transportLatencySum = new AtomicLong(0); |
||||
|
} |
||||
|
|
||||
|
private void update(GatewayMetadata metricsData, long serverReceiveTs) { |
||||
|
long gwLatency = metricsData.publishedTs() - metricsData.receivedTs(); |
||||
|
long transportLatency = serverReceiveTs - metricsData.publishedTs(); |
||||
|
count.incrementAndGet(); |
||||
|
gwLatencySum.addAndGet(gwLatency); |
||||
|
transportLatencySum.addAndGet(transportLatency); |
||||
|
if (minGwLatency == 0 || minGwLatency > gwLatency) { |
||||
|
minGwLatency = gwLatency; |
||||
|
} |
||||
|
if (maxGwLatency < gwLatency) { |
||||
|
maxGwLatency = gwLatency; |
||||
|
} |
||||
|
if (minTransportLatency == 0 || minTransportLatency > transportLatency) { |
||||
|
minTransportLatency = transportLatency; |
||||
|
} |
||||
|
if (maxTransportLatency < transportLatency) { |
||||
|
maxTransportLatency = transportLatency; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ConnectorMetricsResult getResult() { |
||||
|
long count = this.count.get(); |
||||
|
long avgGwLatency = gwLatencySum.get() / count; |
||||
|
long avgTransportLatency = transportLatencySum.get() / count; |
||||
|
return new ConnectorMetricsResult(avgGwLatency, minGwLatency, maxGwLatency, avgTransportLatency, minTransportLatency, maxTransportLatency); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public record ConnectorMetricsResult(long avgGwLatency, long minGwLatency, long maxGwLatency, |
||||
|
long avgTransportLatency, long minTransportLatency, long maxTransportLatency) { |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue