@quenty/influxdbclient 1.1.0-canary.347.e0dad1c.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE.md +21 -0
- package/README.md +23 -0
- package/default.project.json +6 -0
- package/package.json +46 -0
- package/src/Server/Config/InfluxDBClientConfigUtils.lua +22 -0
- package/src/Server/Config/InfluxDBPointSettings.lua +47 -0
- package/src/Server/Config/InfluxDBWriteOptionUtils.lua +48 -0
- package/src/Server/InfluxDBClient.lua +96 -0
- package/src/Server/InfluxDBClient.story.lua +54 -0
- package/src/Server/Utils/InfluxDBErrorUtils.lua +30 -0
- package/src/Server/Write/InfluxDBWriteAPI.lua +191 -0
- package/src/Server/Write/InfluxDBWriteBuffer.lua +94 -0
- package/src/Shared/Utils/InfluxDBEscapeUtils.lua +79 -0
- package/src/Shared/Utils/InfluxDBEscapeUtils.spec.lua +51 -0
- package/src/Shared/Write/InfluxDBPoint.lua +257 -0
- package/src/node_modules.project.json +7 -0
- package/test/default.project.json +14 -0
- package/test/scripts/Server/ServerMain.server.lua +10 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
# 1.1.0-canary.347.e0dad1c.0 (2023-03-31)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Add InfluxDBClient package with just writing ([e0dad1c](https://github.com/Quenty/NevermoreEngine/commit/e0dad1c6044f59b3a389ed388cebbfacf2b4a7ca))
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014-2023 James Onnen (Quenty)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## InfluxDBClient
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="http://quenty.github.io/NevermoreEngine/">
|
|
5
|
+
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/docs.yml/badge.svg" alt="Documentation status" />
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://discord.gg/mhtGUS8">
|
|
8
|
+
<img src="https://img.shields.io/discord/385151591524597761?color=5865F2&label=discord&logo=discord&logoColor=white" alt="Discord" />
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/Quenty/NevermoreEngine/actions">
|
|
11
|
+
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/build.yml/badge.svg" alt="Build and release status" />
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
Provides a Roblox Lua InfluxDB client
|
|
16
|
+
|
|
17
|
+
<div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/InfluxDBClientUtils">View docs →</a></div>
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npm install @quenty/influxdbclient --save
|
|
23
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quenty/influxdbclient",
|
|
3
|
+
"version": "1.1.0-canary.347.e0dad1c.0",
|
|
4
|
+
"description": "Provides a Roblox Lua InfluxDB client",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Roblox",
|
|
7
|
+
"Nevermore",
|
|
8
|
+
"Lua",
|
|
9
|
+
"influxdbclient"
|
|
10
|
+
],
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Quenty/NevermoreEngine/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/Quenty/NevermoreEngine.git",
|
|
17
|
+
"directory": "src/influxdbclient/"
|
|
18
|
+
},
|
|
19
|
+
"funding": {
|
|
20
|
+
"type": "patreon",
|
|
21
|
+
"url": "https://www.patreon.com/quenty"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"contributors": [
|
|
25
|
+
"Quenty"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@quenty/baseobject": "6.2.0",
|
|
29
|
+
"@quenty/httppromise": "6.5.0-canary.347.e0dad1c.0",
|
|
30
|
+
"@quenty/jsonutils": "6.4.0-canary.347.e0dad1c.0",
|
|
31
|
+
"@quenty/loader": "6.2.0",
|
|
32
|
+
"@quenty/maid": "2.5.0",
|
|
33
|
+
"@quenty/math": "2.3.0-canary.347.e0dad1c.0",
|
|
34
|
+
"@quenty/promise": "6.4.0-canary.347.e0dad1c.0",
|
|
35
|
+
"@quenty/rx": "7.8.0-canary.347.e0dad1c.0",
|
|
36
|
+
"@quenty/servicebag": "6.6.0",
|
|
37
|
+
"@quenty/signal": "2.3.0",
|
|
38
|
+
"@quenty/string": "3.1.0",
|
|
39
|
+
"@quenty/table": "3.2.0",
|
|
40
|
+
"@quenty/valueobject": "7.9.0-canary.347.e0dad1c.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"gitHead": "e0dad1c6044f59b3a389ed388cebbfacf2b4a7ca"
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBClientConfigUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local InfluxDBClientConfigUtils = {}
|
|
6
|
+
|
|
7
|
+
function InfluxDBClientConfigUtils.isClientConfig(config)
|
|
8
|
+
return type(config) == "table"
|
|
9
|
+
and type(config.url) == "string"
|
|
10
|
+
and type(config.token) == "string"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
function InfluxDBClientConfigUtils.createClientConfig(config)
|
|
14
|
+
assert(InfluxDBClientConfigUtils.isClientConfig(config), "Bad config")
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
url = config.url;
|
|
18
|
+
token = config.token;
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
return InfluxDBClientConfigUtils
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBPointSettings
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local InfluxDBPointSettings = {}
|
|
8
|
+
InfluxDBPointSettings.ClassName = "InfluxDBPointSettings"
|
|
9
|
+
InfluxDBPointSettings.__index = InfluxDBPointSettings
|
|
10
|
+
|
|
11
|
+
function InfluxDBPointSettings.new()
|
|
12
|
+
local self = setmetatable({}, InfluxDBPointSettings)
|
|
13
|
+
|
|
14
|
+
self._defaultTags = {}
|
|
15
|
+
self._convertTime = nil
|
|
16
|
+
|
|
17
|
+
return self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
function InfluxDBPointSettings:SetDefaultTags(tags)
|
|
21
|
+
assert(type(tags) == "table", "Bad tags")
|
|
22
|
+
|
|
23
|
+
for key, value in pairs(tags) do
|
|
24
|
+
assert(type(value) == "string", "Bad value")
|
|
25
|
+
assert(type(key) == "string", "Bad key")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self._defaultTags = tags
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function InfluxDBPointSettings:GetDefaultTags()
|
|
33
|
+
return self._defaultTags
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
function InfluxDBPointSettings:SetConvertTime(convertTime)
|
|
37
|
+
assert(type(convertTime) == "function", "Bad convertTime")
|
|
38
|
+
|
|
39
|
+
self._convertTime = convertTime
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
function InfluxDBPointSettings:GetConvertTime()
|
|
43
|
+
return self._convertTime
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
return InfluxDBPointSettings
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBWriteOptionUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local Table = require("Table")
|
|
8
|
+
|
|
9
|
+
local InfluxDBWriteOptionUtils = {}
|
|
10
|
+
|
|
11
|
+
function InfluxDBWriteOptionUtils.getDefaultOptions()
|
|
12
|
+
return InfluxDBWriteOptionUtils.createWriteOptions({
|
|
13
|
+
batchSize = 1000;
|
|
14
|
+
maxBatchBytes = 50_000_000; -- default max batch size in the cloud
|
|
15
|
+
flushIntervalSeconds = 60;
|
|
16
|
+
-- maxRetries = 5;
|
|
17
|
+
-- maxRetryTimeSeconds = 180;
|
|
18
|
+
-- maxBufferLines = 32_000;
|
|
19
|
+
-- retryJitterSeconds = 0.2;
|
|
20
|
+
-- minRetryDelaySeconds = 5;
|
|
21
|
+
-- maxRetryDelaySeconds = 125;
|
|
22
|
+
-- exponentialBase = 2;
|
|
23
|
+
-- randomRetry = true;
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
function InfluxDBWriteOptionUtils.createWriteOptions(options)
|
|
28
|
+
assert(InfluxDBWriteOptionUtils.isWriteOptions(options), "Bad options")
|
|
29
|
+
|
|
30
|
+
return Table.readonly(options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
function InfluxDBWriteOptionUtils.isWriteOptions(options)
|
|
34
|
+
return type(options) == "table"
|
|
35
|
+
and type(options.batchSize) == "number"
|
|
36
|
+
and type(options.maxBatchBytes) == "number"
|
|
37
|
+
and type(options.flushIntervalSeconds) == "number"
|
|
38
|
+
-- and type(options.maxRetries) == "number"
|
|
39
|
+
-- and type(options.maxRetryTimeSeconds) == "number"
|
|
40
|
+
-- and type(options.maxBufferLines) == "number"
|
|
41
|
+
-- and type(options.retryJitterSeconds) == "number"
|
|
42
|
+
-- and type(options.minRetryDelaySeconds) == "number"
|
|
43
|
+
-- and type(options.maxRetryDelaySeconds) == "number"
|
|
44
|
+
-- and type(options.exponentialBase) == "number"
|
|
45
|
+
-- and type(options.randomRetry) == "boolean"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return InfluxDBWriteOptionUtils
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBClient
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local BaseObject = require("BaseObject")
|
|
8
|
+
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
9
|
+
local InfluxDBWriteAPI = require("InfluxDBWriteAPI")
|
|
10
|
+
local ValueObject = require("ValueObject")
|
|
11
|
+
local Maid = require("Maid")
|
|
12
|
+
local PromiseUtils = require("PromiseUtils")
|
|
13
|
+
|
|
14
|
+
local InfluxDBClient = setmetatable({}, BaseObject)
|
|
15
|
+
InfluxDBClient.ClassName = "InfluxDBClient"
|
|
16
|
+
InfluxDBClient.__index = InfluxDBClient
|
|
17
|
+
|
|
18
|
+
function InfluxDBClient.new(clientConfig)
|
|
19
|
+
local self = setmetatable(BaseObject.new(), InfluxDBClient)
|
|
20
|
+
|
|
21
|
+
self._clientConfig = ValueObject.new(nil)
|
|
22
|
+
self._maid:GiveTask(self._clientConfig)
|
|
23
|
+
|
|
24
|
+
if clientConfig then
|
|
25
|
+
self:SetClientConfig(clientConfig)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self._writeApis = {}
|
|
29
|
+
|
|
30
|
+
return self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
function InfluxDBClient:SetClientConfig(clientConfig)
|
|
34
|
+
assert(InfluxDBClientConfigUtils.isClientConfig(clientConfig), "Bad clientConfig")
|
|
35
|
+
|
|
36
|
+
self._clientConfig.Value = InfluxDBClientConfigUtils.createClientConfig(clientConfig)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
function InfluxDBClient:GetWriteAPI(org, bucket, precision)
|
|
40
|
+
assert(self._clientConfig, "No self._clientConfig")
|
|
41
|
+
assert(type(org) == "string", "Bad org")
|
|
42
|
+
assert(type(bucket) == "string", "Bad bucket")
|
|
43
|
+
assert(type(precision) == "string" or precision == nil, "Bad precision")
|
|
44
|
+
|
|
45
|
+
self._writeApis[org] = self._writeApis[org] or {}
|
|
46
|
+
if self._writeApis[org][bucket] then
|
|
47
|
+
return self._writeApis[org][bucket]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
local maid = Maid.new()
|
|
51
|
+
|
|
52
|
+
local writeAPI = InfluxDBWriteAPI.new(org, bucket, precision)
|
|
53
|
+
maid:GiveTask(writeAPI)
|
|
54
|
+
|
|
55
|
+
maid:GiveTask(self._clientConfig:Observe():Subscribe(function(clientConfig)
|
|
56
|
+
writeAPI:SetClientConfig(clientConfig)
|
|
57
|
+
end))
|
|
58
|
+
|
|
59
|
+
maid:GiveTask(writeAPI.Destroying:Connect(function()
|
|
60
|
+
self._maid[maid] = nil
|
|
61
|
+
end))
|
|
62
|
+
|
|
63
|
+
self._maid[maid] = maid
|
|
64
|
+
|
|
65
|
+
-- TODO: On destroy flush
|
|
66
|
+
maid:GiveTask(function()
|
|
67
|
+
if self._writeApis[org] then
|
|
68
|
+
if self._writeApis[org][bucket] == writeAPI then
|
|
69
|
+
self._writeApis[org][bucket] = nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end)
|
|
73
|
+
|
|
74
|
+
self._writeApis[org][bucket] = writeAPI
|
|
75
|
+
|
|
76
|
+
-- TODO: Proxy
|
|
77
|
+
return writeAPI
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
function InfluxDBClient:PromiseFlushAll()
|
|
81
|
+
if self._flushAllPromises and self._flushAllPromises:IsPending() then
|
|
82
|
+
return self._flushAllPromises
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
local promises = {}
|
|
86
|
+
for _, bucketList in pairs(self._writeApis) do
|
|
87
|
+
for _, writeAPI in pairs(bucketList) do
|
|
88
|
+
table.insert(promises, writeAPI:PromiseFlush())
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
self._flushAllPromises = PromiseUtils.all(promises)
|
|
93
|
+
return self._flushAllPromises
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return InfluxDBClient
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class InfluxDBClient.story
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local Maid = require("Maid")
|
|
8
|
+
local ServiceBag = require("ServiceBag")
|
|
9
|
+
local InfluxDBClient = require("InfluxDBClient")
|
|
10
|
+
local InfluxDBPoint = require("InfluxDBPoint")
|
|
11
|
+
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
12
|
+
|
|
13
|
+
return function(_target)
|
|
14
|
+
local maid = Maid.new()
|
|
15
|
+
local serviceBag = ServiceBag.new()
|
|
16
|
+
maid:GiveTask(serviceBag)
|
|
17
|
+
|
|
18
|
+
local config = InfluxDBClientConfigUtils.createClientConfig({
|
|
19
|
+
url = "https://ingest.robloxanalytics.com/";
|
|
20
|
+
token = "test-api-key";
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
local influxDBClient = InfluxDBClient.new(config)
|
|
24
|
+
maid:GiveTask(function()
|
|
25
|
+
influxDBClient:PromiseFlushAll():Finally(function()
|
|
26
|
+
influxDBClient:Destroy()
|
|
27
|
+
end)
|
|
28
|
+
end)
|
|
29
|
+
|
|
30
|
+
local writeAPI = influxDBClient:GetWriteAPI("studio-koi-koi", "initial-bucket")
|
|
31
|
+
writeAPI:SetPrintDebugWriteEnabled(true)
|
|
32
|
+
|
|
33
|
+
local point = InfluxDBPoint.new("test")
|
|
34
|
+
point:AddTag("game_name", "boxing")
|
|
35
|
+
point:AddTag("game_id", tostring(game.GameId))
|
|
36
|
+
point:AddTag("place_id", tostring(game.PlaceId))
|
|
37
|
+
point:AddStringField("username", "Quenty")
|
|
38
|
+
point:AddIntField("userid", 4397833)
|
|
39
|
+
point:AddFloatField("fps", 30 + math.random()*30)
|
|
40
|
+
point:AddBooleanField("is_alive", true)
|
|
41
|
+
point:AddBooleanField("is_silent", false)
|
|
42
|
+
|
|
43
|
+
writeAPI:QueuePoint(point)
|
|
44
|
+
|
|
45
|
+
maid:GiveTask(writeAPI.RequestFinished:Connect(function(response)
|
|
46
|
+
print("Got response", response)
|
|
47
|
+
end))
|
|
48
|
+
|
|
49
|
+
writeAPI:PromiseFlush()
|
|
50
|
+
|
|
51
|
+
return function()
|
|
52
|
+
maid:DoCleaning()
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBErrorUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local JSONUtils = require("JSONUtils")
|
|
8
|
+
|
|
9
|
+
local InfluxDBErrorUtils = {}
|
|
10
|
+
|
|
11
|
+
function InfluxDBErrorUtils.tryParseErrorBody(body)
|
|
12
|
+
local ok, decoded, _err = JSONUtils.jsonDecode(body)
|
|
13
|
+
if not ok then
|
|
14
|
+
return nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if InfluxDBErrorUtils.isInfluxDBError(decoded) then
|
|
18
|
+
return decoded
|
|
19
|
+
else
|
|
20
|
+
return nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
function InfluxDBErrorUtils.isInfluxDBError(data)
|
|
25
|
+
return type(data) == "table"
|
|
26
|
+
and type(data.code) == "string"
|
|
27
|
+
and type(data.message) == "string"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
return InfluxDBErrorUtils
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBWriteAPI
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local BaseObject = require("BaseObject")
|
|
8
|
+
local HttpPromise = require("HttpPromise")
|
|
9
|
+
local InfluxDBClientConfigUtils = require("InfluxDBClientConfigUtils")
|
|
10
|
+
local InfluxDBPoint = require("InfluxDBPoint")
|
|
11
|
+
local InfluxDBPointSettings = require("InfluxDBPointSettings")
|
|
12
|
+
local InfluxDBWriteBuffer = require("InfluxDBWriteBuffer")
|
|
13
|
+
local InfluxDBWriteOptionUtils = require("InfluxDBWriteOptionUtils")
|
|
14
|
+
local Promise = require("Promise")
|
|
15
|
+
local Signal = require("Signal")
|
|
16
|
+
local ValueObject = require("ValueObject")
|
|
17
|
+
local InfluxDBErrorUtils = require("InfluxDBErrorUtils")
|
|
18
|
+
|
|
19
|
+
local InfluxDBWriteAPI = setmetatable({}, BaseObject)
|
|
20
|
+
InfluxDBWriteAPI.ClassName = "InfluxDBWriteAPI"
|
|
21
|
+
InfluxDBWriteAPI.__index = InfluxDBWriteAPI
|
|
22
|
+
|
|
23
|
+
function InfluxDBWriteAPI.new(org, bucket, precision)
|
|
24
|
+
local self = setmetatable(BaseObject.new(), InfluxDBWriteAPI)
|
|
25
|
+
|
|
26
|
+
assert(type(org) == "string", "Bad org")
|
|
27
|
+
assert(type(bucket) == "string", "Bad bucket")
|
|
28
|
+
assert(type(precision) == "string" or precision == nil, "Bad precision")
|
|
29
|
+
|
|
30
|
+
self._clientConfig = ValueObject.new(nil)
|
|
31
|
+
self._maid:GiveTask(self._clientConfig)
|
|
32
|
+
|
|
33
|
+
self._printDebugWriteEnabled = false
|
|
34
|
+
self._org = org
|
|
35
|
+
self._bucket = bucket
|
|
36
|
+
self._precision = precision or "ms" -- we can default to ns in the future
|
|
37
|
+
|
|
38
|
+
self._pointSettings = InfluxDBPointSettings.new()
|
|
39
|
+
self._writeOptions = InfluxDBWriteOptionUtils.getDefaultOptions()
|
|
40
|
+
|
|
41
|
+
self.RequestFinished = Signal.new()
|
|
42
|
+
self._maid:GiveTask(self.RequestFinished)
|
|
43
|
+
|
|
44
|
+
self.Destroying = Signal.new()
|
|
45
|
+
self._maid:GiveTask(function()
|
|
46
|
+
self.Destroying:Fire()
|
|
47
|
+
self.Destroying:Destroy()
|
|
48
|
+
end)
|
|
49
|
+
|
|
50
|
+
self._writeBuffer = InfluxDBWriteBuffer.new(self._writeOptions, function(toSend)
|
|
51
|
+
return self:_promiseSendBatch(toSend)
|
|
52
|
+
end)
|
|
53
|
+
self._maid:GiveTask(self._writeBuffer)
|
|
54
|
+
|
|
55
|
+
return self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
function InfluxDBWriteAPI:SetPrintDebugWriteEnabled(printDebugEnabled)
|
|
59
|
+
assert(type(printDebugEnabled) == "boolean", "Bad printDebugEnabled")
|
|
60
|
+
|
|
61
|
+
self._printDebugWriteEnabled = printDebugEnabled
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
function InfluxDBWriteAPI:SetClientConfig(clientConfig)
|
|
65
|
+
assert(InfluxDBClientConfigUtils.isClientConfig(clientConfig), "Bad clientConfig")
|
|
66
|
+
|
|
67
|
+
self._clientConfig.Value = InfluxDBClientConfigUtils.createClientConfig(clientConfig)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
function InfluxDBWriteAPI:SetDefaultTags(tags)
|
|
71
|
+
self._pointSettings:SetDefaultTags(tags)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
function InfluxDBWriteAPI:SetConvertTime(convertTime)
|
|
75
|
+
self._pointSettings:SetConvertTime(convertTime)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
function InfluxDBWriteAPI:QueuePoint(point)
|
|
79
|
+
assert(InfluxDBPoint.isInfluxDBPoint(point), "Bad point")
|
|
80
|
+
|
|
81
|
+
local line = point:ToLineProtocol(self._pointSettings)
|
|
82
|
+
if line then
|
|
83
|
+
self._writeBuffer:Add(line)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if self._printDebugWriteEnabled then
|
|
87
|
+
print(string.format("[InfluxDBWriteAPI.QueuePoint] - Queueing '%s'", line))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
function InfluxDBWriteAPI:QueuePoints(points)
|
|
92
|
+
assert(type(points) == "table", "Bad points")
|
|
93
|
+
|
|
94
|
+
for _, point in pairs(points) do
|
|
95
|
+
assert(InfluxDBPoint.isInfluxDBPoint(point), "Bad point")
|
|
96
|
+
|
|
97
|
+
local line = point:ToLineProtocol(self._pointSettings)
|
|
98
|
+
if line then
|
|
99
|
+
self._writeBuffer:Add(line)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if self._printDebugWriteEnabled then
|
|
103
|
+
print(string.format("[InfluxDBWriteAPI.QueuePoints] - Queueing '%s'", line))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
function InfluxDBWriteAPI:_promiseSendBatch(toSend)
|
|
109
|
+
assert(type(toSend) == "table", "Bad toSend")
|
|
110
|
+
|
|
111
|
+
local clientConfig = self._clientConfig.Value
|
|
112
|
+
if not clientConfig then
|
|
113
|
+
return Promise.rejected("No client configuration")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
assert(type(clientConfig.token) == "string" and #clientConfig.token > 0, "Bad clientConfig.token")
|
|
117
|
+
|
|
118
|
+
local body = table.concat(toSend, "\n")
|
|
119
|
+
local request = {
|
|
120
|
+
Method = "POST";
|
|
121
|
+
Headers = {
|
|
122
|
+
["Content-Type"] = "application/json";
|
|
123
|
+
["Accept"] = "application/json";
|
|
124
|
+
["Authorization"] = "Token " .. clientConfig.token;
|
|
125
|
+
};
|
|
126
|
+
Url = self:_getWriteUrl();
|
|
127
|
+
Body = body;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if self._printDebugWriteEnabled then
|
|
131
|
+
print(string.format("[InfluxDBWriteAPI._promiseSendBatch] - Sending data %s", body))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return self._maid:GivePromise(HttpPromise.request(request))
|
|
135
|
+
:Then(function(result)
|
|
136
|
+
if result.Success then
|
|
137
|
+
self.RequestFinished:Fire(result)
|
|
138
|
+
return true
|
|
139
|
+
else
|
|
140
|
+
return Promise.rejected(result)
|
|
141
|
+
end
|
|
142
|
+
end)
|
|
143
|
+
:Catch(function(err)
|
|
144
|
+
self.RequestFinished:Fire(err)
|
|
145
|
+
|
|
146
|
+
if HttpPromise.isHttpResponse(err) then
|
|
147
|
+
local errorBody = InfluxDBErrorUtils.tryParseErrorBody(err.Body)
|
|
148
|
+
|
|
149
|
+
if errorBody then
|
|
150
|
+
local message = string.format("[InfluxDBWriteAPI:QueuePoint] - %d: %s - %s",
|
|
151
|
+
err.StatusCode,
|
|
152
|
+
errorBody.code,
|
|
153
|
+
errorBody.message)
|
|
154
|
+
warn(message)
|
|
155
|
+
|
|
156
|
+
return Promise.rejected(errorBody)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
warn(string.format("[InfluxDBWriteAPI:QueuePoint] - %d: %s - %s",
|
|
160
|
+
err.StatusCode,
|
|
161
|
+
err.StatusMessage,
|
|
162
|
+
tostring(err.Body)))
|
|
163
|
+
|
|
164
|
+
return Promise.rejected(string.format("[InfluxDBWriteAPI:QueuePoint] - %d: %s", err.StatusCode, err.StatusMessage))
|
|
165
|
+
else
|
|
166
|
+
return Promise.rejected(err or "Request got cancelled")
|
|
167
|
+
end
|
|
168
|
+
end)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
function InfluxDBWriteAPI:PromiseFlush()
|
|
172
|
+
return self._writeBuffer:PromiseFlush()
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
function InfluxDBWriteAPI:_getWriteUrl()
|
|
176
|
+
local config = self._clientConfig.Value
|
|
177
|
+
local url = config.url
|
|
178
|
+
|
|
179
|
+
assert(type(url) == "string", "Bad url")
|
|
180
|
+
|
|
181
|
+
-- escape trailing slashes
|
|
182
|
+
url = string.match(url, "(.-)[\\/]*$")
|
|
183
|
+
|
|
184
|
+
return string.format("%s/api/v2/write?org=%s&bucket=%s&precision=%s",
|
|
185
|
+
url,
|
|
186
|
+
self._org,
|
|
187
|
+
self._bucket,
|
|
188
|
+
self._precision)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
return InfluxDBWriteAPI
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBWriteBuffer
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local BaseObject = require("BaseObject")
|
|
8
|
+
local Promise = require("Promise")
|
|
9
|
+
local Signal = require("Signal")
|
|
10
|
+
|
|
11
|
+
local InfluxDBWriteBuffer = setmetatable({}, BaseObject)
|
|
12
|
+
InfluxDBWriteBuffer.ClassName = "InfluxDBWriteBuffer"
|
|
13
|
+
InfluxDBWriteBuffer.__index = InfluxDBWriteBuffer
|
|
14
|
+
|
|
15
|
+
function InfluxDBWriteBuffer.new(writeOptions, promiseHandleFlush)
|
|
16
|
+
local self = setmetatable(BaseObject.new(), InfluxDBWriteBuffer)
|
|
17
|
+
|
|
18
|
+
self._writeOptions = assert(writeOptions, "Bad writeOptions")
|
|
19
|
+
self._promiseHandleFlush = assert(promiseHandleFlush, "No promiseHandleFlush")
|
|
20
|
+
|
|
21
|
+
self._entries = {}
|
|
22
|
+
self._bytes = 0
|
|
23
|
+
self._length = 0
|
|
24
|
+
|
|
25
|
+
self._requestQueueNext = Signal.new()
|
|
26
|
+
self._maid:GiveTask(self._requestQueueNext)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
return self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
function InfluxDBWriteBuffer:Add(entry)
|
|
33
|
+
assert(type(entry) == "string", "Bad entry")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
-- Already overflowing
|
|
37
|
+
if self._bytes + #entry + 1 >= self._writeOptions.maxBatchBytes then
|
|
38
|
+
self:_promiseFlushAll()
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
table.insert(self._entries, entry)
|
|
42
|
+
|
|
43
|
+
self._bytes = self._bytes + #entry + 1
|
|
44
|
+
self._length = self._length + 1
|
|
45
|
+
|
|
46
|
+
if self._length >= self._writeOptions.batchSize
|
|
47
|
+
or self._bytes >= self._writeOptions.maxBatchBytes then
|
|
48
|
+
|
|
49
|
+
self:_promiseFlushAll()
|
|
50
|
+
else
|
|
51
|
+
self:_queueNextSend()
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
function InfluxDBWriteBuffer:_queueNextSend()
|
|
56
|
+
if self._maid._queuedSendTask then
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
self._maid._queuedSendTask = task.delay(self._writeOptions.flushIntervalSeconds, function()
|
|
61
|
+
task.defer(function()
|
|
62
|
+
if self.Destroy then
|
|
63
|
+
self:_promiseFlushAll()
|
|
64
|
+
end
|
|
65
|
+
end)
|
|
66
|
+
end)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
function InfluxDBWriteBuffer:_reset()
|
|
70
|
+
local entries = self._entries
|
|
71
|
+
|
|
72
|
+
self._bytes = 0
|
|
73
|
+
self._length = 0
|
|
74
|
+
self._entries = {}
|
|
75
|
+
|
|
76
|
+
return entries
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
function InfluxDBWriteBuffer:_promiseFlushAll()
|
|
80
|
+
self._maid._queuedSendTask = nil
|
|
81
|
+
|
|
82
|
+
local entries = self:_reset()
|
|
83
|
+
if #entries > 0 then
|
|
84
|
+
return self._promiseHandleFlush(entries)
|
|
85
|
+
else
|
|
86
|
+
return Promise.resolved()
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
function InfluxDBWriteBuffer:PromiseFlush()
|
|
91
|
+
return self:_promiseFlushAll()
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return InfluxDBWriteBuffer
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBEscapeUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local InfluxDBEscapeUtils = {}
|
|
8
|
+
|
|
9
|
+
local function gsubEscpae(str)
|
|
10
|
+
return str:gsub('%%', '%%%%')
|
|
11
|
+
:gsub('^%^', '%%^')
|
|
12
|
+
:gsub('%$$', '%%$')
|
|
13
|
+
:gsub('%(', '%%(')
|
|
14
|
+
:gsub('%)', '%%)')
|
|
15
|
+
:gsub('%.', '%%.')
|
|
16
|
+
:gsub('%[', '%%[')
|
|
17
|
+
:gsub('%]', '%%]')
|
|
18
|
+
:gsub('%*', '%%*')
|
|
19
|
+
:gsub('%+', '%%+')
|
|
20
|
+
:gsub('%-', '%%-')
|
|
21
|
+
:gsub('%?', '%%?')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
function InfluxDBEscapeUtils.createEscaper(subTable)
|
|
25
|
+
assert(type(subTable) == "table", "Bad subTable")
|
|
26
|
+
|
|
27
|
+
local function replace(char)
|
|
28
|
+
return subTable[char]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
local gsubStr = "(["
|
|
32
|
+
for char, _ in pairs(subTable) do
|
|
33
|
+
assert(#char == 1, "Bad char")
|
|
34
|
+
|
|
35
|
+
gsubStr = gsubStr .. gsubEscpae(char)
|
|
36
|
+
end
|
|
37
|
+
gsubStr = gsubStr .. "])"
|
|
38
|
+
|
|
39
|
+
return function(str)
|
|
40
|
+
return string.gsub(str, gsubStr, replace)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
function InfluxDBEscapeUtils.createQuotedEscaper(subTable)
|
|
45
|
+
assert(type(subTable) == "table", "Bad subTable")
|
|
46
|
+
|
|
47
|
+
local escaper = InfluxDBEscapeUtils.createEscaper(subTable)
|
|
48
|
+
|
|
49
|
+
return function(str)
|
|
50
|
+
return string.format("\"%s\"", escaper(str))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
InfluxDBEscapeUtils.measurement = InfluxDBEscapeUtils.createEscaper({
|
|
56
|
+
[","] = "\\,";
|
|
57
|
+
[" "] = "\\ ";
|
|
58
|
+
["\n"] = "\\n";
|
|
59
|
+
["\r"] = "\\r";
|
|
60
|
+
["\t"] = "\\t";
|
|
61
|
+
["\\"] = "\\\\"; -- not sure about this, is this part of spec?
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
InfluxDBEscapeUtils.quoted = InfluxDBEscapeUtils.createQuotedEscaper({
|
|
65
|
+
["\""] = "\\\"";
|
|
66
|
+
["\\"] = "\\\\";
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
InfluxDBEscapeUtils.tag = InfluxDBEscapeUtils.createEscaper({
|
|
70
|
+
[","] = "\\,";
|
|
71
|
+
[" "] = "\\ ";
|
|
72
|
+
["="] = "\\=";
|
|
73
|
+
["\n"] = "\\n";
|
|
74
|
+
["\r"] = "\\r";
|
|
75
|
+
["\t"] = "\\t";
|
|
76
|
+
["\\"] = "\\\\"; -- not sure about this, is this part of spec?
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return InfluxDBEscapeUtils
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class InfluxDBEscapeUtils.spec.lua
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local InfluxDBEscapeUtils = require("InfluxDBEscapeUtils")
|
|
8
|
+
|
|
9
|
+
return function()
|
|
10
|
+
describe("InfluxDBEscapeUtils.measurement", function()
|
|
11
|
+
it("should pass through fine", function()
|
|
12
|
+
expect(InfluxDBEscapeUtils.measurement("hi")).to.equal("hi")
|
|
13
|
+
end)
|
|
14
|
+
|
|
15
|
+
it("should escape tabs", function()
|
|
16
|
+
expect(InfluxDBEscapeUtils.measurement("\thi")).to.equal("\\thi")
|
|
17
|
+
end)
|
|
18
|
+
end)
|
|
19
|
+
|
|
20
|
+
describe("InfluxDBEscapeUtils.quoted", function()
|
|
21
|
+
it("should pass through fine", function()
|
|
22
|
+
expect(InfluxDBEscapeUtils.quoted("hi")).to.equal("\"hi\"")
|
|
23
|
+
end)
|
|
24
|
+
|
|
25
|
+
it("should escape quotes", function()
|
|
26
|
+
expect(InfluxDBEscapeUtils.quoted("\"hi")).to.equal("\"\\\"hi\"")
|
|
27
|
+
end)
|
|
28
|
+
end)
|
|
29
|
+
|
|
30
|
+
describe("InfluxDBEscapeUtils.tag", function()
|
|
31
|
+
it("should pass through fine", function()
|
|
32
|
+
expect(InfluxDBEscapeUtils.tag("hi")).to.equal("hi")
|
|
33
|
+
end)
|
|
34
|
+
|
|
35
|
+
it("should escape tabs", function()
|
|
36
|
+
expect(InfluxDBEscapeUtils.tag("\thi")).to.equal("\\thi")
|
|
37
|
+
end)
|
|
38
|
+
|
|
39
|
+
it("should escape =", function()
|
|
40
|
+
expect(InfluxDBEscapeUtils.tag("=hi")).to.equal("\\=hi")
|
|
41
|
+
end)
|
|
42
|
+
|
|
43
|
+
it("should escape = and \\", function()
|
|
44
|
+
expect(InfluxDBEscapeUtils.tag("\\=hi")).to.equal("\\\\\\=hi")
|
|
45
|
+
end)
|
|
46
|
+
|
|
47
|
+
it("should escape \\n", function()
|
|
48
|
+
expect(InfluxDBEscapeUtils.tag("\nhi")).to.equal("\\nhi")
|
|
49
|
+
end)
|
|
50
|
+
end)
|
|
51
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class InfluxDBPoint
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local Math = require("Math")
|
|
8
|
+
local InfluxDBEscapeUtils = require("InfluxDBEscapeUtils")
|
|
9
|
+
local Table = require("Table")
|
|
10
|
+
local Set = require("Set")
|
|
11
|
+
|
|
12
|
+
local InfluxDBPoint = {}
|
|
13
|
+
InfluxDBPoint.ClassName = "InfluxDBPoint"
|
|
14
|
+
InfluxDBPoint.__index = InfluxDBPoint
|
|
15
|
+
|
|
16
|
+
function InfluxDBPoint.new(measurementName)
|
|
17
|
+
local self = setmetatable({}, InfluxDBPoint)
|
|
18
|
+
|
|
19
|
+
assert(type(measurementName) == "string" or measurementName == nil, "Bad measurementName")
|
|
20
|
+
|
|
21
|
+
self._measurementName = measurementName
|
|
22
|
+
self._timestamp = nil
|
|
23
|
+
self._tags = {}
|
|
24
|
+
self._fields = {}
|
|
25
|
+
|
|
26
|
+
return self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
function InfluxDBPoint.fromTableData(data)
|
|
30
|
+
assert(type(data) == "table", "Bad data")
|
|
31
|
+
|
|
32
|
+
local copy = InfluxDBPoint.new(data.measurementName)
|
|
33
|
+
copy:SetTimestamp(data.timestamp)
|
|
34
|
+
|
|
35
|
+
if data.tags then
|
|
36
|
+
copy._tags = data.tags
|
|
37
|
+
end
|
|
38
|
+
if data.fields then
|
|
39
|
+
copy._fields = data.fields
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return copy
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
function InfluxDBPoint.isInfluxDBPoint(point)
|
|
46
|
+
return type(point) == "table"
|
|
47
|
+
and getmetatable(point) == InfluxDBPoint
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
function InfluxDBPoint:SetMeasurement(name)
|
|
51
|
+
assert(type(name) == "string" or name == nil, "Bad name")
|
|
52
|
+
|
|
53
|
+
self._measurementName = name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
function InfluxDBPoint:ToTableData()
|
|
57
|
+
return {
|
|
58
|
+
measurementName = self._measurementName;
|
|
59
|
+
timestamp = self._timestamp;
|
|
60
|
+
tags = table.clone(self._tags);
|
|
61
|
+
fields = table.clone(self._fields);
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
--[=[
|
|
66
|
+
If it's nil, the timestamp defaults to send time
|
|
67
|
+
|
|
68
|
+
@param timestamp DateTime | nil
|
|
69
|
+
]=]
|
|
70
|
+
function InfluxDBPoint:SetTimestamp(timestamp)
|
|
71
|
+
assert(typeof(timestamp) == "DateTime" or timestamp == nil, "Bad timestamp")
|
|
72
|
+
|
|
73
|
+
self._timestamp = timestamp
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
--[=[
|
|
77
|
+
Tags are indexed, whereas fields are not.
|
|
78
|
+
|
|
79
|
+
@param tagKey string
|
|
80
|
+
@param tagValue string
|
|
81
|
+
]=]
|
|
82
|
+
function InfluxDBPoint:AddTag(tagKey, tagValue)
|
|
83
|
+
assert(type(tagKey) == "string", "Bad tagKey")
|
|
84
|
+
assert(type(tagValue) == "string", "Bad tagValue")
|
|
85
|
+
|
|
86
|
+
self._tags[tagKey] = tagValue
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
--[=[
|
|
90
|
+
Adds an int field
|
|
91
|
+
|
|
92
|
+
@param fieldName string
|
|
93
|
+
@param value number
|
|
94
|
+
]=]
|
|
95
|
+
function InfluxDBPoint:AddIntField(fieldName, value)
|
|
96
|
+
assert(type(fieldName) == "string", "Bad fieldName")
|
|
97
|
+
assert(type(value) == "number", "Bad value")
|
|
98
|
+
|
|
99
|
+
if Math.isNaN(value)
|
|
100
|
+
or value <= -9223372036854776e3
|
|
101
|
+
or value >= 9223372036854776e3 then
|
|
102
|
+
error(string.format("invalid integer value for field '%s': %s", fieldName, value))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if not Math.isFinite(value) then
|
|
106
|
+
error(string.format("invalid integer value for field '%s': %s", fieldName, value))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
self._fields[fieldName] = string.format("%di", value)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
--[=[
|
|
113
|
+
Adds a uint field
|
|
114
|
+
|
|
115
|
+
@param fieldName string
|
|
116
|
+
@param value number
|
|
117
|
+
]=]
|
|
118
|
+
function InfluxDBPoint:AddUintField(fieldName, value)
|
|
119
|
+
assert(type(fieldName) == "string", "Bad fieldName")
|
|
120
|
+
assert(type(value) == "number", "Bad value")
|
|
121
|
+
|
|
122
|
+
if Math.isNaN(value)
|
|
123
|
+
or value < 0
|
|
124
|
+
or value >= 9007199254740991 then
|
|
125
|
+
error(string.format("invalid uint value for field '%s': %s", fieldName, value))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if not Math.isFinite(value) then
|
|
129
|
+
error(string.format("invalid uint value for field '%s': %s", fieldName, value))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
-- TODO: Support larger uint sizes
|
|
133
|
+
self._fields[fieldName] = string.format("%du", value)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
--[=[
|
|
137
|
+
Adds a float field
|
|
138
|
+
|
|
139
|
+
@param fieldName string
|
|
140
|
+
@param value number
|
|
141
|
+
]=]
|
|
142
|
+
function InfluxDBPoint:AddFloatField(fieldName, value)
|
|
143
|
+
assert(type(fieldName) == "string", "Bad fieldName")
|
|
144
|
+
assert(type(value) == "number", "Bad value")
|
|
145
|
+
|
|
146
|
+
if not Math.isFinite(value) then
|
|
147
|
+
error(string.format("invalid float value for field '%s': %s", fieldName, value))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
self._fields[fieldName] = tostring(value)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
--[=[
|
|
154
|
+
Adds a boolean field
|
|
155
|
+
|
|
156
|
+
@param fieldName string
|
|
157
|
+
@param value boolean
|
|
158
|
+
]=]
|
|
159
|
+
function InfluxDBPoint:AddBooleanField(fieldName, value)
|
|
160
|
+
assert(type(fieldName) == "string", "Bad fieldName")
|
|
161
|
+
assert(type(value) == "boolean", "Bad value")
|
|
162
|
+
|
|
163
|
+
self._fields[fieldName] = value and "T" or "F"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
--[=[
|
|
167
|
+
Adds a string field
|
|
168
|
+
|
|
169
|
+
@param fieldName string
|
|
170
|
+
@param value string
|
|
171
|
+
]=]
|
|
172
|
+
function InfluxDBPoint:AddStringField(fieldName, value)
|
|
173
|
+
assert(type(fieldName) == "string", "Bad fieldName")
|
|
174
|
+
assert(type(value) == "string", "Bad value")
|
|
175
|
+
|
|
176
|
+
self._fields[fieldName] = InfluxDBEscapeUtils.quoted(value)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
function InfluxDBPoint:ToLineProtocol(pointSettings)
|
|
180
|
+
if not self._measurementName then
|
|
181
|
+
return nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
local fieldKeys = Table.keys(self._fields)
|
|
185
|
+
table.sort(fieldKeys)
|
|
186
|
+
|
|
187
|
+
local fields = {}
|
|
188
|
+
for _, key in pairs(fieldKeys) do
|
|
189
|
+
local value = self._fields[key]
|
|
190
|
+
table.insert(fields, InfluxDBEscapeUtils.tag(key) .. "=" .. value)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
-- No fields
|
|
194
|
+
if #fields == 0 then
|
|
195
|
+
warn("[InfluxDBPoint] - Cannot transform point without fields")
|
|
196
|
+
return nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
local tags = nil
|
|
200
|
+
|
|
201
|
+
local defaultTags = pointSettings:GetDefaultTags()
|
|
202
|
+
if next(defaultTags) or next(self._tags) then
|
|
203
|
+
local tagKeysSet = {}
|
|
204
|
+
|
|
205
|
+
for key, value in pairs(self._tags) do
|
|
206
|
+
tagKeysSet[key] = value
|
|
207
|
+
end
|
|
208
|
+
for key, value in pairs(defaultTags) do
|
|
209
|
+
tagKeysSet[key] = value
|
|
210
|
+
end
|
|
211
|
+
local tagKeys = Set.toList(tagKeysSet)
|
|
212
|
+
table.sort(tagKeys)
|
|
213
|
+
|
|
214
|
+
tags = {}
|
|
215
|
+
for _, key in pairs(tagKeys) do
|
|
216
|
+
local value = self._tags[key] or defaultTags[key]
|
|
217
|
+
table.insert(tags, InfluxDBEscapeUtils.tag(key) .. "=" .. InfluxDBEscapeUtils.tag(value))
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
local timestamp = self._timestamp
|
|
222
|
+
local convertTime = pointSettings:GetConvertTime()
|
|
223
|
+
if convertTime then
|
|
224
|
+
timestamp = convertTime(timestamp)
|
|
225
|
+
else
|
|
226
|
+
timestamp = self:_convertTimeToMillis(timestamp)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
local tagsContent
|
|
230
|
+
if tags then
|
|
231
|
+
tagsContent = "," .. table.concat(tags, ",")
|
|
232
|
+
else
|
|
233
|
+
tagsContent = ""
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
return InfluxDBEscapeUtils.measurement(self._measurementName) .. tagsContent .. " " .. table.concat(fields, ",") .. " " .. timestamp
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
function InfluxDBPoint:_convertTimeToMillis(value)
|
|
240
|
+
if value == nil then
|
|
241
|
+
return tostring(DateTime.now().UnixTimestampMillis)
|
|
242
|
+
elseif type(value) == "string" then
|
|
243
|
+
if #value > 0 then
|
|
244
|
+
return value
|
|
245
|
+
else
|
|
246
|
+
return nil
|
|
247
|
+
end
|
|
248
|
+
elseif typeof(value) == "DateTime" then
|
|
249
|
+
return tostring(value.UnixTimestampMillis)
|
|
250
|
+
elseif typeof(value) == "number" then
|
|
251
|
+
return string.format("%0d", value)
|
|
252
|
+
else
|
|
253
|
+
return tostring(value)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
return InfluxDBPoint
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class ServerMain
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
6
|
+
|
|
7
|
+
local loader = ServerScriptService:FindFirstChild("LoaderUtils", true).Parent
|
|
8
|
+
local packages = require(loader).bootstrapGame(ServerScriptService.influxdbclient)
|
|
9
|
+
|
|
10
|
+
require(packages.InfluxDBClient)
|