Skip to content

Commit 365d067

Browse files
committed
basicstation: implement forwarding gateway statistics.
Note that these stats are aggregated by the ChirpStack Gateway Bridge as these are not exposed by the BasicStation.
1 parent 5d4c0d5 commit 365d067

File tree

6 files changed

+176
-23
lines changed

6 files changed

+176
-23
lines changed

cmd/chirpstack-gateway-bridge/cmd/configfile.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ type="{{ .Backend.Type }}"
124124
# certificate of the gateway has been signed by this CA certificate.
125125
ca_cert="{{ .Backend.BasicStation.CACert }}"
126126
127+
# Stats interval.
128+
#
129+
# This defines the interval in which the ChirpStack Gateway Bridge forwards
130+
# the uplink / downlink statistics.
131+
stats_interval="{{ .Backend.BasicStation.StatsInterval }}"
132+
127133
# Ping interval.
128134
ping_interval="{{ .Backend.BasicStation.PingInterval }}"
129135

cmd/chirpstack-gateway-bridge/cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func init() {
4545
viper.SetDefault("backend.concentratord.command_url", "ipc:///tmp/concentratord_command")
4646

4747
viper.SetDefault("backend.basic_station.bind", ":3001")
48+
viper.SetDefault("backend.basic_station.stats_interval", time.Second*30)
4849
viper.SetDefault("backend.basic_station.ping_interval", time.Minute)
4950
viper.SetDefault("backend.basic_station.read_timeout", time.Minute+(5*time.Second))
5051
viper.SetDefault("backend.basic_station.write_timeout", time.Second)

docs/content/install/config.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ type="semtech_udp"
142142
fake_rx_time=false
143143

144144

145-
146145
# ChirpStack Concentratord backend.
147146
[backend.concentratord]
148147

@@ -175,6 +174,12 @@ type="semtech_udp"
175174
# certificate of the gateway has been signed by this CA certificate.
176175
ca_cert=""
177176

177+
# Stats interval.
178+
#
179+
# This defines the interval in which the ChirpStack Gateway Bridge forwards
180+
# the uplink / downlink statistics.
181+
stats_interval="30s"
182+
178183
# Ping interval.
179184
ping_interval="1m0s"
180185

internal/backend/basicstation/backend.go

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import (
1616
"time"
1717

1818
"github.com/gofrs/uuid"
19+
"github.com/golang/protobuf/ptypes"
1920
"github.com/gorilla/websocket"
21+
"github.com/patrickmn/go-cache"
2022
"github.com/pkg/errors"
2123
log "github.com/sirupsen/logrus"
2224

@@ -43,9 +45,10 @@ type Backend struct {
4345
scheme string
4446
isClosed bool
4547

46-
pingInterval time.Duration
47-
readTimeout time.Duration
48-
writeTimeout time.Duration
48+
statsInterval time.Duration
49+
pingInterval time.Duration
50+
readTimeout time.Duration
51+
writeTimeout time.Duration
4952

5053
gateways gateways
5154

@@ -62,10 +65,11 @@ type Backend struct {
6265
frequencyMax uint32
6366
routerConfig structs.RouterConfig
6467

65-
// diidMap stores the mapping of diid to UUIDs. This should take ~ 1MB of
66-
// memory. Optionaly this could be optimized by letting keys expire after
67-
// a given time.
68-
diidMap map[uint16][]byte
68+
// Cache to store stats.
69+
statsCache *cache.Cache
70+
71+
// Cache to store diid to UUIDs.
72+
diidCache *cache.Cache
6973
}
7074

7175
// NewBackend creates a new Backend.
@@ -83,15 +87,17 @@ func NewBackend(conf config.Config) (*Backend, error) {
8387
gatewayStatsChan: make(chan gw.GatewayStats),
8488
rawPacketForwarderEventChan: make(chan gw.RawPacketForwarderEvent),
8589

86-
pingInterval: conf.Backend.BasicStation.PingInterval,
87-
readTimeout: conf.Backend.BasicStation.ReadTimeout,
88-
writeTimeout: conf.Backend.BasicStation.WriteTimeout,
90+
statsInterval: conf.Backend.BasicStation.StatsInterval,
91+
pingInterval: conf.Backend.BasicStation.PingInterval,
92+
readTimeout: conf.Backend.BasicStation.ReadTimeout,
93+
writeTimeout: conf.Backend.BasicStation.WriteTimeout,
8994

9095
region: band.Name(conf.Backend.BasicStation.Region),
9196
frequencyMin: conf.Backend.BasicStation.FrequencyMin,
9297
frequencyMax: conf.Backend.BasicStation.FrequencyMax,
9398

94-
diidMap: make(map[uint16][]byte),
99+
diidCache: cache.New(time.Minute, time.Minute),
100+
statsCache: cache.New(conf.Backend.BasicStation.StatsInterval*2, conf.Backend.BasicStation.StatsInterval*2),
95101
}
96102

97103
for _, n := range conf.Filters.NetIDs {
@@ -238,8 +244,10 @@ func (b *Backend) SendDownlinkFrame(df gw.DownlinkFrame) error {
238244
copy(gatewayID[:], df.GetGatewayId())
239245
copy(downID[:], df.GetDownlinkId())
240246

247+
b.incrementTxStats(gatewayID)
248+
241249
// store token to UUID mapping
242-
b.diidMap[uint16(df.Token)] = df.GetDownlinkId()
250+
b.diidCache.SetDefault(fmt.Sprintf("%d", df.Token), df.GetDownlinkId())
243251

244252
websocketSendCounter("dnmsg").Inc()
245253
if err := b.sendToGateway(gatewayID, pl); err != nil {
@@ -382,15 +390,69 @@ func (b *Backend) handleGateway(r *http.Request, c *websocket.Conn) {
382390
"remote_addr": r.RemoteAddr,
383391
}).Info("backend/basicstation: gateway connected")
384392

393+
done := make(chan struct{})
394+
385395
// remove the gateway on return
386396
defer func() {
397+
done <- struct{}{}
387398
b.gateways.remove(gatewayID)
388399
log.WithFields(log.Fields{
389400
"gateway_id": gatewayID,
390401
"remote_addr": r.RemoteAddr,
391402
}).Info("backend/basicstation: gateway disconnected")
392403
}()
393404

405+
statsTicker := time.NewTicker(b.statsInterval)
406+
defer statsTicker.Stop()
407+
408+
// stats publishing loop
409+
go func() {
410+
gwIDStr := gatewayID.String()
411+
412+
for {
413+
select {
414+
case <-statsTicker.C:
415+
id, err := uuid.NewV4()
416+
if err != nil {
417+
log.WithError(err).Error("backend/basicstation: new uuid error")
418+
continue
419+
}
420+
421+
var rx, rxOK, tx, txOK uint32
422+
if v, ok := b.statsCache.Get(gwIDStr + ":rx"); ok {
423+
rx = v.(uint32)
424+
}
425+
if v, ok := b.statsCache.Get(gwIDStr + ":rxOK"); ok {
426+
rxOK = v.(uint32)
427+
}
428+
if v, ok := b.statsCache.Get(gwIDStr + ":tx"); ok {
429+
tx = v.(uint32)
430+
}
431+
if v, ok := b.statsCache.Get(gwIDStr + ":txOK"); ok {
432+
txOK = v.(uint32)
433+
}
434+
435+
b.statsCache.Delete(gwIDStr + ":rx")
436+
b.statsCache.Delete(gwIDStr + ":rxOK")
437+
b.statsCache.Delete(gwIDStr + ":tx")
438+
b.statsCache.Delete(gwIDStr + ":txOK")
439+
440+
b.gatewayStatsChan <- gw.GatewayStats{
441+
GatewayId: gatewayID[:],
442+
Time: ptypes.TimestampNow(),
443+
StatsId: id[:],
444+
RxPacketsReceived: rx,
445+
RxPacketsReceivedOk: rxOK,
446+
TxPacketsReceived: tx,
447+
TxPacketsEmitted: txOK,
448+
}
449+
case <-done:
450+
return
451+
}
452+
}
453+
454+
}()
455+
394456
// receive data
395457
for {
396458
mt, msg, err := c.ReadMessage()
@@ -447,6 +509,7 @@ func (b *Backend) handleGateway(r *http.Request, c *websocket.Conn) {
447509
b.handleVersion(gatewayID, pl)
448510
case structs.UplinkDataFrameMessage:
449511
// handle uplink
512+
b.incrementRxStats(gatewayID)
450513
var pl structs.UplinkDataFrame
451514
if err := json.Unmarshal(msg, &pl); err != nil {
452515
log.WithError(err).WithFields(log.Fields{
@@ -459,6 +522,7 @@ func (b *Backend) handleGateway(r *http.Request, c *websocket.Conn) {
459522
b.handleUplinkDataFrame(gatewayID, pl)
460523
case structs.JoinRequestMessage:
461524
// handle join-request
525+
b.incrementRxStats(gatewayID)
462526
var pl structs.JoinRequest
463527
if err := json.Unmarshal(msg, &pl); err != nil {
464528
log.WithError(err).WithFields(log.Fields{
@@ -471,6 +535,7 @@ func (b *Backend) handleGateway(r *http.Request, c *websocket.Conn) {
471535
b.handleJoinRequest(gatewayID, pl)
472536
case structs.ProprietaryDataFrameMessage:
473537
// handle proprietary uplink
538+
b.incrementRxStats(gatewayID)
474539
var pl structs.UplinkProprietaryFrame
475540
if err := json.Unmarshal(msg, &pl); err != nil {
476541
log.WithError(err).WithFields(log.Fields{
@@ -483,6 +548,7 @@ func (b *Backend) handleGateway(r *http.Request, c *websocket.Conn) {
483548
b.handleProprietaryDataFrame(gatewayID, pl)
484549
case structs.DownlinkTransmittedMessage:
485550
// handle downlink transmitted
551+
b.incrementTxOkStats(gatewayID)
486552
var pl structs.DownlinkTransmitted
487553
if err := json.Unmarshal(msg, &pl); err != nil {
488554
log.WithError(err).WithFields(log.Fields{
@@ -584,7 +650,10 @@ func (b *Backend) handleDownlinkTransmittedMessage(gatewayID lorawan.EUI64, v st
584650
}).Error("backend/basicstation: error converting downlink transmitted to protobuf message")
585651
return
586652
}
587-
txack.DownlinkId = b.diidMap[uint16(v.DIID)]
653+
654+
if v, ok := b.diidCache.Get(fmt.Sprintf("%d", v.DIID)); ok {
655+
txack.DownlinkId = v.([]byte)
656+
}
588657

589658
var downID uuid.UUID
590659
copy(downID[:], txack.GetDownlinkId())
@@ -723,3 +792,31 @@ func (b *Backend) websocketWrap(handler func(*http.Request, *websocket.Conn), w
723792
handler(r, conn)
724793
done <- struct{}{}
725794
}
795+
796+
func (b *Backend) incrementRxStats(id lorawan.EUI64) {
797+
idStr := id.String()
798+
799+
if _, err := b.statsCache.IncrementUint32(idStr+":rx", uint32(1)); err != nil {
800+
b.statsCache.SetDefault(idStr+":rx", uint32(1))
801+
}
802+
803+
if _, err := b.statsCache.IncrementUint32(idStr+":rxOK", uint32(1)); err != nil {
804+
b.statsCache.SetDefault(idStr+":rxOK", uint32(1))
805+
}
806+
}
807+
808+
func (b *Backend) incrementTxOkStats(id lorawan.EUI64) {
809+
idStr := id.String()
810+
811+
if _, err := b.statsCache.IncrementUint32(idStr+"txOK", uint32(1)); err != nil {
812+
b.statsCache.SetDefault(idStr+":txOK", uint32(1))
813+
}
814+
}
815+
816+
func (b *Backend) incrementTxStats(id lorawan.EUI64) {
817+
idStr := id.String()
818+
819+
if _, err := b.statsCache.IncrementUint32(idStr+"tx", uint32(1)); err != nil {
820+
b.statsCache.SetDefault(idStr+":tx", uint32(1))
821+
}
822+
}

internal/backend/basicstation/backend_test.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func (ts *BackendTestSuite) SetupTest() {
4444
conf.Backend.BasicStation.Region = "EU868"
4545
conf.Backend.BasicStation.FrequencyMin = 867000000
4646
conf.Backend.BasicStation.FrequencyMax = 869000000
47+
conf.Backend.BasicStation.StatsInterval = 30 * time.Second
4748
conf.Backend.BasicStation.PingInterval = time.Minute
4849
conf.Backend.BasicStation.ReadTimeout = 2 * time.Minute
4950
conf.Backend.BasicStation.WriteTimeout = time.Second
@@ -168,6 +169,14 @@ func (ts *BackendTestSuite) TestUplinkDataFrame() {
168169
CrcStatus: gw.CRCStatus_CRC_OK,
169170
},
170171
}, uplinkFrame)
172+
173+
rx, ok := ts.backend.statsCache.Get("0102030405060708:rx")
174+
assert.True(ok)
175+
assert.Equal(uint32(1), rx)
176+
177+
rxOK, ok := ts.backend.statsCache.Get("0102030405060708:rxOK")
178+
assert.True(ok)
179+
assert.Equal(uint32(1), rxOK)
171180
}
172181

173182
func (ts *BackendTestSuite) TestJoinRequest() {
@@ -221,6 +230,14 @@ func (ts *BackendTestSuite) TestJoinRequest() {
221230
CrcStatus: gw.CRCStatus_CRC_OK,
222231
},
223232
}, uplinkFrame)
233+
234+
rx, ok := ts.backend.statsCache.Get("0102030405060708:rx")
235+
assert.True(ok)
236+
assert.Equal(uint32(1), rx)
237+
238+
rxOK, ok := ts.backend.statsCache.Get("0102030405060708:rxOK")
239+
assert.True(ok)
240+
assert.Equal(uint32(1), rxOK)
224241
}
225242

226243
func (ts *BackendTestSuite) TestProprietaryDataFrame() {
@@ -269,14 +286,22 @@ func (ts *BackendTestSuite) TestProprietaryDataFrame() {
269286
CrcStatus: gw.CRCStatus_CRC_OK,
270287
},
271288
}, uplinkFrame)
289+
290+
rx, ok := ts.backend.statsCache.Get("0102030405060708:rx")
291+
assert.True(ok)
292+
assert.Equal(uint32(1), rx)
293+
294+
rxOK, ok := ts.backend.statsCache.Get("0102030405060708:rxOK")
295+
assert.True(ok)
296+
assert.Equal(uint32(1), rxOK)
272297
}
273298

274299
func (ts *BackendTestSuite) TestDownlinkTransmitted() {
275300
assert := require.New(ts.T())
276301
id, err := uuid.NewV4()
277302
assert.NoError(err)
278303

279-
ts.backend.diidMap[12345] = id[:]
304+
ts.backend.diidCache.SetDefault("12345", id[:])
280305

281306
dtx := structs.DownlinkTransmitted{
282307
MessageType: structs.DownlinkTransmittedMessage,
@@ -292,6 +317,14 @@ func (ts *BackendTestSuite) TestDownlinkTransmitted() {
292317
Token: 12345,
293318
DownlinkId: id[:],
294319
}, txAck)
320+
321+
// this variable is not yet stored
322+
_, ok := ts.backend.statsCache.Get("0102030405060708:tx")
323+
assert.False(ok)
324+
325+
txOK, ok := ts.backend.statsCache.Get("0102030405060708:txOK")
326+
assert.True(ok)
327+
assert.Equal(uint32(1), txOK)
295328
}
296329

297330
func (ts *BackendTestSuite) TestSendDownlinkFrame() {
@@ -331,7 +364,9 @@ func (ts *BackendTestSuite) TestSendDownlinkFrame() {
331364
})
332365
assert.NoError(err)
333366

334-
assert.Equal(id[:], ts.backend.diidMap[1234])
367+
idResp, ok := ts.backend.diidCache.Get("1234")
368+
assert.True(ok)
369+
assert.Equal(id[:], idResp)
335370

336371
var df structs.DownlinkFrame
337372
assert.NoError(ts.wsClient.ReadJSON(&df))
@@ -355,6 +390,14 @@ func (ts *BackendTestSuite) TestSendDownlinkFrame() {
355390
RX1DR: &dr2,
356391
RX1Freq: &freq,
357392
}, df)
393+
394+
tx, ok := ts.backend.statsCache.Get("0102030405060708:tx")
395+
assert.True(ok)
396+
assert.Equal(uint32(1), tx)
397+
398+
// this variable is not yet stored
399+
_, ok = ts.backend.statsCache.Get("0102030405060708:txOK")
400+
assert.False(ok)
358401
}
359402

360403
func (ts *BackendTestSuite) TestRawPacketForwarderCommand() {

internal/config/config.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ type Config struct {
2626
} `mapstructure:"semtech_udp"`
2727

2828
BasicStation struct {
29-
Bind string `mapstructure:"bind"`
30-
TLSCert string `mapstructure:"tls_cert"`
31-
TLSKey string `mapstructure:"tls_key"`
32-
CACert string `mapstructure:"ca_cert"`
33-
PingInterval time.Duration `mapstructure:"ping_interval"`
34-
ReadTimeout time.Duration `mapstructure:"read_timeout"`
35-
WriteTimeout time.Duration `mapstructure:"write_timeout"`
29+
Bind string `mapstructure:"bind"`
30+
TLSCert string `mapstructure:"tls_cert"`
31+
TLSKey string `mapstructure:"tls_key"`
32+
CACert string `mapstructure:"ca_cert"`
33+
StatsInterval time.Duration `mapstructure:"stats_interval"`
34+
PingInterval time.Duration `mapstructure:"ping_interval"`
35+
ReadTimeout time.Duration `mapstructure:"read_timeout"`
36+
WriteTimeout time.Duration `mapstructure:"write_timeout"`
3637
// TODO: remove Filters in the next major release, use global filters instead
3738
Filters struct {
3839
NetIDs []string `mapstructure:"net_ids"`

0 commit comments

Comments
 (0)