@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 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
+ ```
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "influxdbclient",
3
+ "tree": {
4
+ "$path": "src"
5
+ }
6
+ }
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,7 @@
1
+ {
2
+ "name": "node_modules",
3
+ "globIgnorePaths": [ "**/.package-lock.json" ],
4
+ "tree": {
5
+ "$path": { "optional": "../node_modules" }
6
+ }
7
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "InfluxDBClientTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "influxdbclient": {
7
+ "$path": ".."
8
+ },
9
+ "Script": {
10
+ "$path": "scripts/Server"
11
+ }
12
+ }
13
+ }
14
+ }
@@ -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)