├── LICENSE ├── README.md ├── ts3.chart.py └── ts3.conf /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Jan Arnold 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Teamspeak 3 netdata Plugin # 3 | 4 | This is a [netdata](https://github.com/firehol/netdata/) plugin that polls active 5 | users and bandwidth from TeamSpeak 3 servers. 6 | 7 | ![TS3 plugin screenshot](http://semper.space/netdata_ts3/screenshot01.png "Netdata TS3 plugin") 8 | 9 | ## Installation ## 10 | 11 | With your default netdata installation copy the ts3.chart.py script to 12 | `/usr/libexec/netdata/python.d/` and the ts3.conf config file to 13 | `/etc/netdata/python.d/`. The location of these directories may vary depending 14 | on your distribution. Read your given release of netdata for more information. 15 | 16 | Edit the config file to set the TeamSpeak Server Query user and password. If not 17 | already set, connect to your TeamSpeak server with the TeamSpeak client and go 18 | to menu 'Extras' -> 'ServerQuery Login' and set a user and password. 19 | 20 | Restart netdata to activate the plugin after you have made these changes. 21 | 22 | To disable the Teamspeak 3 plugin, edit `/etc/netdata/python.d.conf` and add 23 | `ts3: no`. 24 | 25 | ## Debugging 26 | switch to netdata user: 27 | 28 | `sudo su -s /bin/bash netdata` 29 | 30 | Run plugin in debug mode: 31 | 32 | `/usr/libexec/netdata/plugins.d/python.d.plugin 1 debug trace ts3` 33 | 34 | ## Version History ## 35 | 36 | - v0.9 37 | - Avoid login with every query and subsequently filling up teamspeaks query log 38 | - v0.8 39 | - Fixed SocketService import 40 | - compatibility fix for netdata 1.10 by @arnowelzel 41 | - formatting and tweaks by @vennekilde and @catlinman 42 | - v0.7 43 | - Major readability and formatting changes as well as a compatibility fix for Python 3.6.2 44 | - v0.6 45 | - Bugfix: Default netdata socket service raised an error when decoding non ASCII characters 46 | - v0.5 47 | - Added packet loss graph and file transfer bandwidth 48 | - v0.4 49 | - Added bandwidth graph 50 | - v0.3 51 | - Cleanup and config implementation (Special thanks to @paulfantom) 52 | - v0.2 53 | - Rewrote plugin to use Netdata's SocketService 54 | - v0.1 55 | - Initial release 56 | 57 | ## License ## 58 | 59 | This repository is released under the MIT license. For more information please 60 | refer to [LICENSE](https://github.com/catlinman/netdata_ts3_plugin/blob/master/LICENSE) 61 | -------------------------------------------------------------------------------- /ts3.chart.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | NetData plugin for active users on TeamSpeak 3 servers. 5 | Please set user and password for your TeamSpeakQuery login in the 'ts3.conf'. 6 | 7 | The MIT License (MIT) 8 | Copyright (c) 2016-2017 Jan Arnold 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of 11 | this software and associated documentation files (the "Software"), to deal in 12 | the Software without restriction, including without limitation the rights to 13 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 14 | the Software, and to permit persons to whom the Software is furnished to do so, 15 | subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 22 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 23 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 24 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | # @Title : ts3.chart 28 | # @Description : NetData plugin for active users on TeamSpeak 3 servers 29 | # @Author : Jan Arnold 30 | # @Email : jan.arnold (at) coraxx.net 31 | # @Copyright : Copyright (C) 2016-2017 Jan Arnold 32 | # @License : MIT 33 | # @Maintainer : Jan Arnold 34 | # @Date : 2018/10/02 35 | # @Version : 0.9 36 | # @Status : stable 37 | # @Usage : Automatically processed by netdata 38 | # @Notes : With default NetData installation put this file under 39 | # : /usr/libexec/netdata/python.d/ and the config file under 40 | # : /etc/netdata/python.d/ 41 | # @Python_version : >3.6.2 or >2.7.3 42 | """ 43 | 44 | import os 45 | import re 46 | import select 47 | 48 | from bases.FrameworkServices.SocketService import SocketService 49 | 50 | # Basic plugin settings for netdata. 51 | update_every = 1 52 | priority = 60000 53 | retries = 10 54 | 55 | ORDER = ['users', 'bandwidth_total', 'bandwidth_filetransfer', 'packetloss'] 56 | 57 | CHARTS = { 58 | 'users': { 59 | 'options': [None, 'Users online', 'users', 'Users', 'ts3.connected_user', 'line'], 60 | 'lines': [ 61 | ['connected_users', 'online', 'absolute'] 62 | ] 63 | }, 64 | 'bandwidth_total': { 65 | 'options': [None, 'Bandwidth total', 'kb/s', 'Bandwidth', 'ts3.bandwidth_total', 'area'], 66 | 'lines': [ 67 | ['bandwidth_total_received', 'received', 'absolute', 1, 1000], 68 | ['bandwidth_total_sent', 'sent', 'absolute', -1, 1000] 69 | ] 70 | }, 71 | 'bandwidth_filetransfer': { 72 | 'options': [None, 'Bandwidth filetransfer', 'kb/s', 'Bandwidth', 'ts3.bandwidth_filetransfer', 'area'], 73 | 'lines': [ 74 | ['bandwidth_filetransfer_received', 'received', 'absolute', 1, 1000], 75 | ['bandwidth_filetransfer_sent', 'sent', 'absolute', -1, 1000] 76 | ] 77 | }, 78 | 'packetloss': { 79 | 'options': [None, 'Average data packet loss', 'packets loss %', 'Packet Loss', 'ts3.packetloss', 'line'], 80 | 'lines': [ 81 | ['packetloss_speech', 'speech', 'absolute', 1, 1000], 82 | ['packetloss_keepalive', 'keepalive', 'absolute', 1, 1000], 83 | ['packetloss_control', 'control', 'absolute', 1, 1000], 84 | ['packetloss_total', 'total', 'absolute', 1, 1000], 85 | ] 86 | } 87 | } 88 | 89 | 90 | class Service(SocketService): 91 | def __init__(self, configuration=None, name=None): 92 | SocketService.__init__(self, configuration=configuration, name=name) 93 | 94 | # Default TeamSpeak Server connection settings. 95 | self.host = "127.0.0.1" 96 | self.port = "10011" 97 | 98 | # Connection socket settings. 99 | self.unix_socket = None 100 | self._keep_alive = True 101 | self.request = "serverinfo\n" 102 | self.loggedIn = False 103 | 104 | # Chart information handled by netdata. 105 | self.order = ORDER 106 | self.definitions = CHARTS 107 | 108 | def check(self): 109 | """ 110 | Parse configuration and check if local Teamspeak server is running 111 | :return: boolean 112 | """ 113 | self._parse_config() 114 | 115 | try: 116 | self.user = self.configuration['user'] 117 | 118 | if self.user == '': 119 | raise KeyError 120 | 121 | except KeyError: 122 | self.error( 123 | "Please specify a TeamSpeak Server query user inside the ts3.conf!", "Disabling plugin...") 124 | 125 | return False 126 | 127 | try: 128 | self.passwd = self.configuration['pass'] 129 | 130 | if self.passwd == '': 131 | raise KeyError 132 | 133 | except KeyError: 134 | self.error( 135 | "Please specify a TeamSpeak Server query password inside the ts3.conf!", "Disabling plugin...") 136 | 137 | return False 138 | 139 | try: 140 | self.sid = self.configuration['sid'] 141 | 142 | except KeyError: 143 | self.sid = 1 144 | self.debug("No sid specified. Using: '{0}'".format(self.sid)) 145 | 146 | # Check once if TS3 is running when host is localhost. 147 | if self.host in ['localhost', '127.0.0.1']: 148 | TS3_running = False 149 | 150 | pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] 151 | 152 | for pid in pids: 153 | try: 154 | if b'ts3server' in open(os.path.join('/proc', pid, 'cmdline').encode(), 'rb').read(): 155 | TS3_running = True 156 | 157 | break 158 | 159 | except IOError as e: 160 | self.error(e) 161 | 162 | if TS3_running is False: 163 | self.error("No local TeamSpeak server running. Disabling plugin...") 164 | 165 | return False 166 | 167 | else: 168 | self.debug("TeamSpeak server process found. Connecting...") 169 | 170 | return True 171 | 172 | def _send(self, request=None): 173 | """ 174 | Send request. 175 | :return: boolean 176 | """ 177 | # Send request if it is needed 178 | if self.request != "".encode(): 179 | try: 180 | if not self.loggedIn: 181 | self._sock.send("login {0} {1}\n".format(self.user, self.passwd).encode()) 182 | self._receive() 183 | self._sock.send("use sid={0}\n".format(self.sid).encode()) 184 | self._receive() 185 | self.loggedIn = True 186 | self._sock.send(self.request) 187 | 188 | except Exception as e: 189 | self.loggedIn = False 190 | self._disconnect() 191 | self.error( 192 | str(e), 193 | "used configuration: host:", str(self.host), 194 | "port:", str(self.port), 195 | "socket:", str(self.unix_socket) 196 | ) 197 | 198 | return False 199 | 200 | return True 201 | 202 | def _receive(self, raw=False): 203 | """ 204 | Receive data from socket 205 | :return: str 206 | """ 207 | data = "" 208 | 209 | while True: 210 | try: 211 | ready_to_read, _, in_error = select.select([self._sock], [], [], 5) 212 | 213 | except Exception as e: 214 | self.debug("SELECT", str(e)) 215 | self._disconnect() 216 | 217 | break 218 | 219 | if len(ready_to_read) > 0: 220 | buf = self._sock.recv(4096) 221 | 222 | if len(buf) == 0 or buf is None: # handle server disconnect 223 | break 224 | 225 | self.debug(str(buf)) 226 | data += buf.decode("utf-8") 227 | 228 | if self._check_raw_data(data): 229 | break 230 | else: 231 | self.error("Socket timed out.") 232 | self._disconnect() 233 | 234 | break 235 | 236 | return data 237 | 238 | def _get_data(self): 239 | """ 240 | Format data received from socket 241 | :return: dict 242 | """ 243 | data = {} 244 | 245 | try: 246 | raw = self._get_raw_data() 247 | 248 | except (ValueError, AttributeError): 249 | self.error("No data received.") 250 | 251 | return None 252 | 253 | reg = re.compile( 254 | "virtualserver_clientsonline=(\d*)|" + 255 | "virtualserver_queryclientsonline=(\d*)|" + 256 | "connection_bandwidth_sent_last_second_total=(\d*)|" + 257 | "connection_bandwidth_received_last_second_total=(\d*)|" + 258 | "connection_filetransfer_bandwidth_sent=(\d*)|" + 259 | "connection_filetransfer_bandwidth_received=(\d*)|" + 260 | "virtualserver_total_packetloss_speech=(\d+\.\d+)|" + 261 | "virtualserver_total_packetloss_keepalive=(\d+\.\d+)|" + 262 | "virtualserver_total_packetloss_control=(\d+\.\d+)|" + 263 | "virtualserver_total_packetloss_total=(\d+\.\d+)" 264 | ) 265 | 266 | regex = reg.findall(raw) 267 | 268 | self.debug(str(regex)) 269 | 270 | if regex == []: 271 | self.error("Information could not be extracted") 272 | return None 273 | 274 | try: 275 | # Clients and query clients connected. 276 | connected_users = int(regex[0][0]) - int(regex[1][1]) 277 | data["connected_users"] = connected_users 278 | 279 | # Bandwidth info from server in bytes/s. 280 | data["bandwidth_total_sent"] = int(regex[8][2]) 281 | data["bandwidth_total_received"] = int(regex[9][3]) 282 | data["bandwidth_filetransfer_sent"] = int(regex[6][4]) 283 | data["bandwidth_filetransfer_received"] = int(regex[7][5]) 284 | 285 | # The average packet loss. 286 | data["packetloss_speech"] = float(regex[2][6]) * 100000 287 | data["packetloss_keepalive"] = float(regex[3][7]) * 100000 288 | data["packetloss_control"] = float(regex[4][8]) * 100000 289 | data["packetloss_total"] = float(regex[5][9]) * 100000 290 | 291 | except Exception as e: 292 | self.error(str(e)) 293 | 294 | return None 295 | 296 | return data 297 | 298 | def _check_raw_data(self, data): 299 | if data.endswith("msg=ok\n\r"): 300 | return True 301 | 302 | else: 303 | return False 304 | -------------------------------------------------------------------------------- /ts3.conf: -------------------------------------------------------------------------------- 1 | # netdata python.d.plugin configuration for ts3 2 | # 3 | # TS3 Query user and password: If not set already, connect to your TS server 4 | # with the TS client and go to the menu 'Extras'->'ServerQuery Login' 5 | # 6 | # sid is the virtual server id. Only change this if you have a TeamSpeak setup 7 | # with multiple virtual servers (TS servers with a license). 8 | 9 | local: 10 | # update_every: 1 11 | # priority: 60000 12 | # retries: 10 13 | # host: '127.0.0.1' 14 | # port: 10011 15 | # sid: 1 16 | user: '' 17 | pass: '' 18 | 19 | # external: 20 | # update_every: 1 21 | # priority: 60000 22 | # retries: 10 23 | # host: '123.456.789.0' 24 | # port: 10011 25 | # sid: 1 26 | # user: '' 27 | # pass: '' 28 | --------------------------------------------------------------------------------