-
Notifications
You must be signed in to change notification settings - Fork 517
added encoder for OpenTSDB #1751
base: dev
Are you sure you want to change the base?
Changes from 1 commit
482375f
2d4aa4d
0338b2a
43b548a
7e9c7dd
a3c1204
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| -- This Source Code Form is subject to the terms of the Mozilla Public | ||
| -- License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| -- file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
|
|
||
| --[=[ | ||
| Extracts Field data from messages and generates some OpenTSDB-compliant JSON | ||
| (http://opentsdb.net/docs/build/html/api_http/put.html). | ||
|
|
||
| Can optionally buffer and flush events after a configurable number of messages | ||
| (flush_count) or after a configurable number of seconds have passed between the | ||
| last Timestamp and the current one (flush_delta) (encoders currently have no | ||
| support for timer_event()s) | ||
|
|
||
| Config: | ||
|
|
||
| - flush_count (number, default 1) | ||
| Flush the buffer every 'flush_count' messages. | ||
|
|
||
| - flush_delta (number, default 0) | ||
| Flush the buffer if 'flush_delta' seconds have elapsed since the last | ||
| Timestamp and the current one. | ||
|
|
||
| - tag_prefix (string, optional, default "") | ||
| Convert Fields to OpenTSDB tags if they match the tag_prefix. | ||
| (tag_prefix is stripped from the tag name) | ||
|
|
||
| - tag_list (string, optional, default "") | ||
| A list of fields (separated by spaces) which will be converted into tags. | ||
| You can use either "tag_list", "tag_prefix" or both. | ||
|
|
||
| - skip_fields (string, optional, default "") | ||
| A list of fields (separated by spaces) which will be dropped from processing. | ||
|
|
||
| - type_as_prefix (boolean, optional, default true) | ||
| Prepend metrics when encoding them to OpenTSDB format by Type field. | ||
|
|
||
| - ts_from_message (boolean, optional, default false) | ||
| Use the Timestamp field (otherwise "now") | ||
|
|
||
| - add_hostname_if_missing (boolean, optional, default false) | ||
| If no 'host' tag has been seen, append one with the value of the | ||
| Hostname field. Deprecated in favour of using the 'fieldfix' filter. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was left from @hynd's implementation. I'll remove the reference. |
||
|
|
||
|
|
||
| *Example Heka Configuration* | ||
| .. code-block:: ini | ||
|
|
||
| [OpentsdbEncoder] | ||
| type = "SandboxEncoder" | ||
| filename = "lua_encoders/opentsdb_http.lua" | ||
|
|
||
| [OpentsdbEncoder.config] | ||
| ts_from_message = true | ||
| add_hostname_if_missing = true | ||
| type_as_prefix = true | ||
| tag_prefix = "tag_" | ||
| tag_list = "env dc" | ||
| skip_fields = "Pid" | ||
|
|
||
| [opentsdb] | ||
| type = "HttpOutput" | ||
| message_matcher = "Fields[MetricOpentsdb] != NIL" | ||
| address = "http://opentsdb.example.com:4242/api/put" | ||
| encoder = "OpentsdbEncoder" | ||
|
|
||
| *Example Output* | ||
| .. code-block:: json | ||
| [{"value":0.05,"timestamp":1443535912,"metric":"stats.loadavg.15MinAvg","tags":{"env":"test","host":"example.com"}},{"value":2,"timestamp":1443535912,"metric":"stats.loadavg.NumProcesses","tags":{"env":"test","host":"example.com"}},{"value":0.01,"timestamp":1443535912,"metric":"stats.loadavg.5MinAvg","tags":{"env":"test","host":"example.com"}},{"value":0,"timestamp":1443535912,"metric":"stats.loadavg.1MinAvg","tags":{"env":"test","host":"example.com"}}] | ||
|
|
||
| --]=] | ||
|
|
||
| require "cjson" | ||
| require "table" | ||
| require "string" | ||
| require "os" | ||
|
|
||
| local l = require 'lpeg' | ||
| l.locale(l) | ||
|
|
||
| local flush_count = read_config("flush_count") or 1 | ||
| local flush_delta = read_config("flush_delta") or 0 | ||
| local tag_prefix = read_config("tag_prefix") | ||
| local tag_list_str = read_config("tag_list") or "" | ||
| local skip_fields_str = read_config("skip_fields") or "" | ||
| local type_as_prefix = read_config("type_as_prefix") | ||
| local ts_from_message = read_config("ts_from_message") | ||
| local add_hostname = read_config("add_hostname_if_missing") | ||
| _PRESERVATION_VERSION = read_config("preservation_version") or 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our convention is to put the _PRESERVATION_VERSION stuff as the first lines of the module, immediately after the documentation comment. Also, you'll want to add in a value that you can increment in the source code, for times when changing the source code of the encoder invalidate the saved data. You can see an example of how this should be done in the HTTP Status filter. |
||
|
|
||
| if not tag_prefix then | ||
| return -1 | ||
| end | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't return here, you're not inside a function. At module scope, you can use the local tag_prefix = read_config("tag_prefix") or error("`tag_prefix` setting required") |
||
|
|
||
| if type_as_prefix == nil then | ||
| type_as_prefix = true | ||
| end | ||
|
|
||
| if add_hostname == nil then | ||
| add_hostname = true | ||
| end | ||
|
|
||
| if ts_from_message == nil then | ||
| ts_from_message = true | ||
| end | ||
|
|
||
| tag_list = {} | ||
| for field in tag_list_str:gmatch("[%S]+") do | ||
| tag_list[field] = true | ||
| end | ||
|
|
||
| skip_fields = {} | ||
| for field in skip_fields_str:gmatch("[%S]+") do | ||
| skip_fields[field] = true | ||
| end | ||
|
|
||
| prefix = l.P(tag_prefix) | ||
| chars = l.alnum + l.S("_-./") | ||
| grammar = prefix * l.C(l.alpha * chars^0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like these three vars should all be local. |
||
|
|
||
| buffer = {} | ||
| last_flush = 0 | ||
|
|
||
| function flush() | ||
| add_to_payload(cjson.encode(buffer)) | ||
| inject_payload() | ||
| last_flush = os.time() | ||
| buffer = {} | ||
| end | ||
|
|
||
|
|
||
| function process_message() | ||
|
|
||
| local ts | ||
| if ts_from_message then | ||
| ts = read_message("Timestamp") / 1e9 | ||
| else | ||
| ts = os.time() | ||
| end | ||
|
|
||
| if not ts then return -1 end | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| local typ=read_message("Type") | ||
| local tags = {} | ||
| local metrics = {} | ||
|
|
||
| local msg = decode_message(read_message("raw")) | ||
| if not msg.Fields then return -1 end | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When returning an error code it's a good idea to return an error message as the second return var, like so: if not msg.Fields then
return -1, "Malformed message (no fields)"
endHowever, again, while a message might not have any fields, it should always have a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fields are optional and can be missing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, my bad, I thought read_message("raw") would always return at least an empty Fields table. In that case then just make sure to return an error message as specified above.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minimally you can get just a Uuid and a Timestamp (the required fields). Everything else can be nil. |
||
|
|
||
| for _, field in ipairs(msg.Fields) do | ||
| local name = field["name"] -- these are related to way Heka works | ||
| local value = field["value"][1] -- | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lua idiom is to use dot notation when looking up static table keys, like so: local name = field.name
local value = field.value[1] |
||
|
|
||
| local tag = grammar:match(name) or (tag_list[name] ~= nil and name) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't need the |
||
| if tag then | ||
| tags[tag] = value | ||
| else | ||
| if type(value) == "number" and skip_fields[name] == nil then | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nil comparisons are usually written using |
||
| if type_as_prefix then | ||
| name = typ .. "." .. name | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our policy is to only use the |
||
| end | ||
| metrics[name] = value | ||
| end | ||
| end | ||
| end | ||
|
|
||
| if not tags["host"] and add_hostname then | ||
| tags["host"] = msg.Hostname | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| end | ||
|
|
||
| opentsdb_message = {} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem to be used anywhere, should be removed. |
||
|
|
||
| for name, value in pairs(metrics) do | ||
| local message = {} | ||
| message["metric"] = name | ||
| message["value"] = value | ||
| message["timestamp"] = ts | ||
| message["tags"] = tags | ||
| buffer[#buffer+1] = message | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Believe it or not, Lua actually has to count the number of items in an array each time. Since we're adding metrics in a tight loop like this, it'd make sense to store and increment a separate variable for the buffer length, so it doesn't have to be recomputed each time around. |
||
| end | ||
|
|
||
| -- flush the buffer | ||
| if (flush_count > 0 and #buffer >= flush_count) or | ||
| (flush_delta > 0 and os.time() - last_flush > flush_delta) then | ||
| flush() | ||
| else | ||
| return -2 | ||
| end | ||
|
|
||
| return 0 | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems weird to me that this would default to
false. I'd think it more likely that folks will want to use the timestamp from the messages.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was only in docs but really the default is true.
I'll update this and all other points shortly and update the pull request.