├── .gitignore ├── extensions ├── __init__.py └── zabbix.py ├── LICENSE.txt ├── CHANGELOG.txt ├── README.rst ├── .github └── workflows │ └── create-release.yml ├── zabbix-redis.py ├── template-app-redis-sentinel.j2 └── template-app-redis-server.j2 /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /extensions/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ''' 4 | :url: https://github.com/allenta/zabbix-template-for-redis 5 | :copyright: (c) 2015-2022 by Allenta Consulting S.L. . 6 | :license: BSD, see LICENSE.txt for more details. 7 | ''' 8 | -------------------------------------------------------------------------------- /extensions/zabbix.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ''' 4 | :url: https://github.com/allenta/zabbix-template-for-redis 5 | :copyright: (c) 2015-2022 by Allenta Consulting S.L. . 6 | :license: BSD, see LICENSE.txt for more details. 7 | ''' 8 | 9 | import binascii 10 | import hashlib 11 | from jinja2.ext import Extension 12 | 13 | 14 | class ZabbixExtension(Extension): 15 | uuids = set() 16 | 17 | def __init__(self, environment): 18 | super(ZabbixExtension, self).__init__(environment) 19 | environment.filters['zuuid'] = self._zuuid 20 | 21 | def _zuuid(self, seed): 22 | data = bytearray(hashlib.md5(seed.encode('utf-8')).digest()) 23 | data[6] = data[6] & 0x0f | 0x40 24 | data[8] = data[8] & 0x3f | 0x80 25 | uuid = binascii.hexlify(data).decode('utf-8') 26 | 27 | if uuid in self.uuids: 28 | raise Exception("Duplicated seed/UUID: '{}' ➙ {}".format(seed, uuid)) 29 | self.uuids.add(uuid) 30 | 31 | return uuid 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2022 Allenta Consulting S.L. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 19 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 21 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 22 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 23 | SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | - 16.0 (2025-06-27): 2 | - Added support for Zabbix 7.4. 3 | 4 | - 15.0 (2024-12-12): 5 | - Added support for Zabbix 7.2. 6 | 7 | - 14.0 (2024-05-31): 8 | - Updated script shebang to python3. 9 | - Added support for Zabbix 7.0. New 'Disable lost resources' property for 10 | discovery rules will adopt the default value 'Immediately' instead of 11 | trying to preserve the 6.x behaviour ('Never'). 12 | 13 | - 13.0 (2023-05-05): 14 | - Resolve warnings on Github Actions workflow and include script on releases. 15 | 16 | - 12.0 (2023-04-11): 17 | - Removed support for Zabbix 5.0, 5.2 & 5.4. 18 | 19 | - 11.0 (2023-03-17): 20 | - Added support for Zabbix 6.4. 21 | 22 | - 10.0 (2022-07-05): 23 | - Added support for Zabbix 6.2. 24 | 25 | - 9.0 (2022-01-26): 26 | - Added support for Zabbix 6.0. 27 | 28 | - 8.0 (2021-11-29): 29 | - Stopped assuming all preprocessing steps require a single parameter. 30 | 31 | - 7.0 (2021-07-08): 32 | - Avoided JavaScript precision issues when filtering is not needed. 33 | 34 | - 6.0 (2021-07-07): 35 | - Added {$REDIS_SENTINEL.LLD_EXCLUDED_SUBJECTS} and {$REDIS_SERVER.LLD_EXCLUDED_SUBJECTS} for filtering LLD items based on {#SUBJECT}. 36 | - Added {$REDIS_SENTINEL.EXCLUDED_STATS} and {$REDIS_SERVER.EXCLUDED_STATS} for filtering entries in the master item. 37 | 38 | - 5.0 (2021-07-05): 39 | - Removed graphs. 40 | 41 | - 4.0 (2021-05-25): 42 | - Dropped support for Zabbix 4.x. BEWARE UUIDs of triggers (not trigger prototypes) will change. 43 | - Restored unused triggers. 44 | 45 | - 3.0 (2021-05-24): 46 | - Added support for Zabbix 5.4. BEWARE custom UUIDs are used (i.e. history will be lost when manually upgrading the template). 47 | - 'App' tag renamed to 'Application'. 48 | 49 | - 2.0 (2020-11-19): 50 | - Added support for Zabbix 5.2. 51 | 52 | - 1.0 (2020-05-14): 53 | - Initial release. 54 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **This is a Zabbix template + script useful to monitor Redis Server & Redis Sentinel instances:** 2 | 3 | 1. Copy ``zabbix-redis.py`` to ``/usr/local/bin/``. 4 | 5 | 2. Add the ``redis_server.discovery`` & ``redis_server.stats`` and / or ``redis_sentinel.discovery`` & ``redis_sentinel.stats`` user parameters to Zabbix:: 6 | 7 | UserParameter=redis_server.discovery[*],/usr/local/bin/zabbix-redis.py -i '$1' -t server discover $2 2> /dev/null 8 | UserParameter=redis_server.stats[*],/usr/local/bin/zabbix-redis.py -i '$1' -t server stats 2> /dev/null 9 | UserParameter=redis_sentinel.discovery[*],/usr/local/bin/zabbix-redis.py -i '$1' -t sentinel discover $2 2> /dev/null 10 | UserParameter=redis_sentinel.stats[*],/usr/local/bin/zabbix-redis.py -i '$1' -t sentinel stats 2> /dev/null 11 | 12 | 3. Import the templates. You may download the appropriate versions from `the releases page `_ or generate them using the Jinja2 skeletons:: 13 | 14 | $ pip install jinja2-cli 15 | $ PYTHONPATH=. jinja2 \ 16 | -D version={6.0,6.2,6.4,7.0,7.2,7.4} \ 17 | [-D name='Redis Server'] \ 18 | [-D description=''] \ 19 | [-D release='trunk'] \ 20 | --extension=extensions.zabbix.ZabbixExtension --strict -o template.xml template-app-redis-server.j2 21 | $ PYTHONPATH=. jinja2 \ 22 | -D version={6.0,6.2,6.4,7.0,7.2,7.4} \ 23 | [-D name='Redis Sentinel'] \ 24 | [-D description=''] \ 25 | [-D release='trunk'] \ 26 | --extension=extensions.zabbix.ZabbixExtension --strict -o template.xml template-app-redis-sentinel.j2 27 | 28 | 4. Link hosts to the templates. Beware depending on the used template you must set a value for the ``{$REDIS_SERVER.LOCATIONS}`` or ``{$REDIS_SENTINEL.LOCATIONS}`` macro (comma-delimited list of Redis instances; ``port``, ``host:port`` and ``unix:///path/to/socket`` formats are allowed). Additional macros and contexts are available for further customizations. 29 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | create_release: 10 | name: Create release 11 | 12 | runs-on: ubuntu-24.04 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Create draft 18 | run: gh release create $GITHUB_REF_NAME --draft=true --title $GITHUB_REF_NAME zabbix-redis.py 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | upload_assets: 23 | name: Upload assets 24 | 25 | runs-on: ubuntu-24.04 26 | 27 | needs: create_release 28 | 29 | strategy: 30 | matrix: 31 | version: 32 | - '6.0' 33 | - '6.2' 34 | - '6.4' 35 | - '7.0' 36 | - '7.2' 37 | - '7.4' 38 | flavour: 39 | - name: Redis Server 40 | slug: redis-server 41 | - name: Redis Sentinel 42 | slug: redis-sentinel 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Set up Python 48 | uses: actions/setup-python@v5 49 | with: 50 | python-version: 3.12 51 | 52 | - name: Install dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install jinja2-cli 56 | 57 | - name: Build template file name 58 | run: | 59 | echo "TEMPLATE_FILE_NAME=template-app-${{ matrix.flavour.slug }}-${{ matrix.version }}-$GITHUB_REF_NAME.xml" >> $GITHUB_ENV 60 | 61 | - name: Generate template 62 | run: | 63 | PYTHONPATH=. jinja2 \ 64 | -D version=${{ matrix.version }} \ 65 | -D name='${{ matrix.flavour.name }}' \ 66 | -D description="Template App ${{ matrix.flavour.name }} $GITHUB_REF_NAME" \ 67 | -D release="${GITHUB_REF_NAME:1}" \ 68 | --extension=extensions.zabbix.ZabbixExtension --strict -o $TEMPLATE_FILE_NAME template-app-${{ matrix.flavour.slug }}.j2 69 | 70 | - name: Upload template 71 | run: gh release upload $GITHUB_REF_NAME $TEMPLATE_FILE_NAME 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | 75 | publish_release: 76 | name: Publish release 77 | 78 | runs-on: ubuntu-24.04 79 | 80 | needs: 81 | - create_release 82 | - upload_assets 83 | 84 | steps: 85 | - uses: actions/checkout@v4 86 | 87 | - name: Publish release 88 | run: gh release edit $GITHUB_REF_NAME --draft=false 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | -------------------------------------------------------------------------------- /zabbix-redis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | :url: https://github.com/allenta/zabbix-template-for-redis 6 | :copyright: (c) Allenta Consulting S.L. . 7 | :license: BSD, see LICENSE.txt for more details. 8 | ''' 9 | 10 | from __future__ import absolute_import, division, print_function, unicode_literals 11 | import json 12 | import re 13 | import subprocess 14 | import sys 15 | from argparse import ArgumentParser 16 | 17 | TYPE_COUNTER = 1 18 | TYPE_GAUGE = 2 19 | TYPE_OTHER = 3 20 | TYPES = (TYPE_COUNTER, TYPE_GAUGE, TYPE_OTHER) 21 | 22 | ITEMS = { 23 | 'server': ( 24 | (r'server:uptime_in_seconds', TYPE_GAUGE), 25 | (r'clients:connected_clients', TYPE_GAUGE), 26 | (r'clients:blocked_clients', TYPE_GAUGE), 27 | (r'memory:used_memory', TYPE_GAUGE), 28 | (r'memory:used_memory_rss', TYPE_GAUGE), 29 | (r'memory:used_memory_lua', TYPE_GAUGE), 30 | (r'memory:mem_fragmentation_ratio', TYPE_GAUGE), 31 | (r'persistence:rdb_changes_since_last_save', TYPE_GAUGE), 32 | (r'persistence:rdb_last_save_time', TYPE_GAUGE), 33 | (r'persistence:rdb_last_bgsave_status', TYPE_OTHER), 34 | (r'persistence:rdb_last_bgsave_time_sec', TYPE_GAUGE), 35 | (r'persistence:aof_last_rewrite_time_sec', TYPE_GAUGE), 36 | (r'persistence:aof_last_bgrewrite_status', TYPE_OTHER), 37 | (r'persistence:aof_last_write_status', TYPE_OTHER), 38 | (r'stats:total_connections_received', TYPE_COUNTER), 39 | (r'stats:total_commands_processed', TYPE_COUNTER), 40 | (r'stats:total_net_input_bytes', TYPE_COUNTER), 41 | (r'stats:total_net_output_bytes', TYPE_COUNTER), 42 | (r'stats:rejected_connections', TYPE_COUNTER), 43 | (r'stats:sync_full', TYPE_COUNTER), 44 | (r'stats:sync_partial_ok', TYPE_COUNTER), 45 | (r'stats:sync_partial_err', TYPE_COUNTER), 46 | (r'stats:expired_keys', TYPE_COUNTER), 47 | (r'stats:evicted_keys', TYPE_COUNTER), 48 | (r'stats:keyspace_hits', TYPE_COUNTER), 49 | (r'stats:keyspace_misses', TYPE_COUNTER), 50 | (r'stats:pubsub_channels', TYPE_GAUGE), 51 | (r'stats:pubsub_patterns', TYPE_GAUGE), 52 | (r'replication:role', TYPE_GAUGE), 53 | (r'replication:connected_slaves', TYPE_GAUGE), 54 | (r'cpu:used_cpu_sys', TYPE_COUNTER), 55 | (r'cpu:used_cpu_user', TYPE_COUNTER), 56 | (r'cpu:used_cpu_sys_children', TYPE_COUNTER), 57 | (r'cpu:used_cpu_user_children', TYPE_COUNTER), 58 | (r'commandstats:[^:]+:calls', TYPE_COUNTER), 59 | (r'commandstats:[^:]+:usec_per_call', TYPE_GAUGE), 60 | (r'keyspace:[^:]+:(?:keys|expires|avg_ttl)', TYPE_GAUGE), 61 | (r'cluster:cluster_state', TYPE_GAUGE), 62 | (r'cluster:cluster_slots_assigned', TYPE_GAUGE), 63 | (r'cluster:cluster_slots_ok', TYPE_GAUGE), 64 | (r'cluster:cluster_slots_pfail', TYPE_GAUGE), 65 | (r'cluster:cluster_slots_fail', TYPE_GAUGE), 66 | (r'cluster:cluster_known_nodes', TYPE_GAUGE), 67 | (r'cluster:cluster_size', TYPE_GAUGE), 68 | ), 69 | 'sentinel': ( 70 | (r'server:uptime_in_seconds', TYPE_GAUGE), 71 | (r'sentinel:sentinel_masters', TYPE_GAUGE), 72 | (r'sentinel:sentinel_running_scripts', TYPE_GAUGE), 73 | (r'sentinel:sentinel_scripts_queue_length', TYPE_GAUGE), 74 | (r'masters:[^:]+:(?:status|slaves|sentinels|ckquorum|usable_sentinels)', TYPE_GAUGE), 75 | ), 76 | } 77 | 78 | SUBJECTS = { 79 | 'server': { 80 | 'items': None, 81 | 'commandstats': re.compile(r'^commandstats:([^:]+):.+$'), 82 | 'keyspace': re.compile(r'^keyspace:([^:]+):.+$'), 83 | }, 84 | 'sentinel': { 85 | 'items': None, 86 | 'masters': re.compile(r'^masters:([^:]+):.+$'), 87 | }, 88 | } 89 | 90 | 91 | ############################################################################### 92 | ## 'stats' COMMAND 93 | ############################################################################### 94 | 95 | def stats(options): 96 | # Initializations. 97 | result = {} 98 | 99 | # Build master item contents. 100 | for instance in options.redis_instances.split(','): 101 | instance = instance.strip() 102 | stats = _stats( 103 | instance, 104 | options.redis_type, 105 | options.redis_user, 106 | options.redis_password) 107 | for item in stats.items: 108 | result['%(instance)s.%(name)s' % { 109 | 'instance': _safe_zabbix_string(instance), 110 | 'name': _safe_zabbix_string(item.name), 111 | }] = item.value 112 | 113 | # Render output. 114 | sys.stdout.write(json.dumps(result, separators=(',', ':'))) 115 | 116 | 117 | ############################################################################### 118 | ## 'discover' COMMAND 119 | ############################################################################### 120 | 121 | def discover(options): 122 | # Initializations. 123 | discovery = { 124 | 'data': [], 125 | } 126 | 127 | # Build Zabbix discovery input. 128 | for instance in options.redis_instances.split(','): 129 | instance = instance.strip() 130 | if options.subject == 'items': 131 | discovery['data'].append({ 132 | '{#LOCATION}': instance, 133 | '{#LOCATION_ID}': _safe_zabbix_string(instance), 134 | }) 135 | else: 136 | stats = _stats( 137 | instance, 138 | options.redis_type, 139 | options.redis_user, 140 | options.redis_password) 141 | for subject in stats.subjects(options.subject): 142 | discovery['data'].append({ 143 | '{#LOCATION}': instance, 144 | '{#LOCATION_ID}': _safe_zabbix_string(instance), 145 | '{#SUBJECT}': subject, 146 | '{#SUBJECT_ID}': _safe_zabbix_string(subject), 147 | }) 148 | 149 | # Render output. 150 | sys.stdout.write(json.dumps(discovery, sort_keys=True, indent=2)) 151 | 152 | 153 | ############################################################################### 154 | ## HELPERS 155 | ############################################################################### 156 | 157 | class Item(object): 158 | ''' 159 | A class to hold all relevant information about an item in the stats: name, 160 | value, type and subject (type & value). 161 | ''' 162 | 163 | def __init__( 164 | self, name, value, type, subject_type=None, subject_value=None): 165 | # Set name and value. 166 | self._name = name 167 | self._value = value 168 | self._type = type 169 | self._subject_type = subject_type or 'items' 170 | self._subject_value = subject_value 171 | 172 | @property 173 | def name(self): 174 | return self._name 175 | 176 | @property 177 | def value(self): 178 | return self._value 179 | 180 | @property 181 | def type(self): 182 | return self._type 183 | 184 | @property 185 | def subject_type(self): 186 | return self._subject_type 187 | 188 | @property 189 | def subject_value(self): 190 | return self._subject_value 191 | 192 | def aggregate(self, value): 193 | # Aggregate another value. Only counter and gauges can be aggregated. 194 | # In any other case, mark this item's value as discarded. 195 | if self.type in (TYPE_COUNTER, TYPE_GAUGE): 196 | self._value += value 197 | else: 198 | self._value = None 199 | 200 | 201 | class Stats(object): 202 | ''' 203 | A class to hold results for a call to _stats: keeps all processed items and 204 | all subjects seen per subject type and provides helper methods to build and 205 | process those items. 206 | ''' 207 | 208 | def __init__(self, items_definitions, subjects_patterns, log_handler=None): 209 | # Build items regular expression that will be used to match item names 210 | # and discover item types. 211 | items_re = dict((type, []) for type in TYPES) 212 | for item_re, item_type in items_definitions: 213 | items_re[item_type].append(item_re) 214 | self._items_patterns = dict( 215 | (type, re.compile(r'^(?:' + '|'.join(res) + r')$')) 216 | for type, res in items_re.items()) 217 | 218 | # Set subject patterns that will be used to assign subject type and 219 | # subject values to items. 220 | self._subjects_patterns = subjects_patterns 221 | 222 | # Other initializations. 223 | self._log_handler = log_handler or sys.stderr.write 224 | self._items = {} 225 | self._subjects = {} 226 | 227 | @property 228 | def items(self): 229 | # Return all items that haven't had their value discarded because an 230 | # invalid aggregation. 231 | return (item for item in self._items.values() if item.value is not None) 232 | 233 | def add(self, name, value, type=None, subject_type=None, 234 | subject_value=None): 235 | # Add a new item to the internal state or simply aggregate it's value 236 | # if an item with the same name has already been added. 237 | if name in self._items: 238 | self._items[name].aggregate(value) 239 | else: 240 | # Build item. 241 | item = self._build_item( 242 | name, value, type, subject_type, subject_value) 243 | 244 | if item is not None: 245 | # Add new item to the internal state. 246 | self._items[item.name] = item 247 | 248 | # Also, register this item's subject in the corresponding set. 249 | if item.subject_type != None and item.subject_value != None: 250 | if item.subject_type not in self._subjects: 251 | self._subjects[item.subject_type] = set() 252 | self._subjects[item.subject_type].add(item.subject_value) 253 | 254 | def get(self, name, default=None): 255 | # Return current value for a particular item or the given default value 256 | # if that item is not available or has had it's value discarded. 257 | if name in self._items and self._items[name].value is not None: 258 | return self._items[name].value 259 | else: 260 | return default 261 | 262 | def subjects(self, subject_type): 263 | # Return the set of registered subjects for a given subject type. 264 | return self._subjects.get(subject_type, set()) 265 | 266 | def log(self, message): 267 | self._log_handler(message) 268 | 269 | def _build_item( 270 | self, name, value, type=None, subject_type=None, 271 | subject_value=None): 272 | # Initialize type if none was provided. 273 | if type is None: 274 | type = next(( 275 | type for type in TYPES 276 | if self._items_patterns[type].match(name) is not None), None) 277 | 278 | # Filter invalid items. 279 | if type not in TYPES: 280 | return None 281 | 282 | # Initialize subject_type and subject_value if none were provided. 283 | if subject_type is None and subject_value is None: 284 | for subject, subject_re in self._subjects_patterns.items(): 285 | if subject_re is not None: 286 | match = subject_re.match(name) 287 | if match is not None: 288 | subject_type = subject 289 | subject_value = match.group(1) 290 | break 291 | 292 | # Return item instance. 293 | return Item( 294 | name=name, 295 | value=value, 296 | type=type, 297 | subject_type=subject_type, 298 | subject_value=subject_value 299 | ) 300 | 301 | 302 | def _stats(location, type, user, password): 303 | # Initializations. 304 | stats = Stats(ITEMS[type], SUBJECTS[type]) 305 | clustered = False 306 | 307 | # Parse location of the Redis instance. 308 | prefix = 'unix://' 309 | if location.startswith(prefix): 310 | opts = '-s "%s"' % location[len(prefix):] 311 | else: 312 | if ':' in location: 313 | opts = '-h "%s" -p "%s"' % tuple(location.split(':', 1)) 314 | else: 315 | opts = '-p "%s"' % location 316 | 317 | # Authenticate as an user other than default? 318 | if user is not None: 319 | opts += ' --user "%s"' % user 320 | 321 | # Use password? 322 | if password is not None: 323 | opts += ' -a "%s"' % password 324 | 325 | # Fetch general stats through redis-cli. 326 | rc, output = _execute('redis-cli %(opts)s INFO %(section)s' % { 327 | 'opts': opts, 328 | 'section': 'all' if type == 'server' else 'default', 329 | }) 330 | if rc == 0: 331 | section = None 332 | for line in output.splitlines(): 333 | # Start of section. Keep it's name. 334 | if line.startswith('#'): 335 | section = line[1:].strip().lower() 336 | 337 | # Item. Process it. 338 | elif section is not None and ':' in line: 339 | # Extract item name and item value. 340 | key, value = (v.strip() for v in line.split(':', 1)) 341 | if section == 'commandstats' and \ 342 | key.startswith('cmdstat_'): 343 | key = key[8:].upper() 344 | name = '%s:%s' % (section, key) 345 | 346 | # If this item enables cluster mode set the corresponding flag 347 | # to later check for cluster stats. 348 | if name == 'cluster:cluster_enabled' and value == '1': 349 | clustered = True 350 | 351 | # Special keys with subvalues. 352 | if ((type == 'server' and 353 | section in ('keyspace', 'commandstats')) or \ 354 | (type == 'sentinel' and 355 | section == 'sentinel' and 356 | key.startswith('master'))): 357 | # Extract subvalues. 358 | subvalues = {} 359 | for item in value.split(','): 360 | if '=' in item: 361 | subkey, subvalue = item.split('=', 1) 362 | subvalues[subkey.strip()] = subvalue.strip() 363 | 364 | # Process subvalues. 365 | for subkey, subvalue in subvalues.items(): 366 | if type == 'sentinel' and subkey == 'name': 367 | _stats_sentinel( 368 | stats, 369 | opts, 370 | subvalue, 371 | 'masters:%s(%s)' % (key, subvalue)) 372 | else: 373 | subname = None 374 | if type != 'sentinel': 375 | subname = '%s:%s' % (name, subkey) 376 | elif subkey != 'name' and 'name' in subvalues: 377 | subname = 'masters:%s(%s):%s' % ( 378 | key, subvalues['name'], subkey) 379 | if subname is not None: 380 | # Add item to the result. 381 | stats.add(subname, subvalue) 382 | 383 | # Simple keys with no subvalues. 384 | else: 385 | # Add item to the result. 386 | stats.add(name, value) 387 | 388 | # Error recovering information from redis-cli. 389 | else: 390 | stats.log(output) 391 | 392 | # Fetch cluster stats through redis-cli. 393 | if type == 'server' and clustered: 394 | _stats_cluster(stats, opts) 395 | 396 | # Done! 397 | return stats 398 | 399 | 400 | def _stats_sentinel(stats, opts, master_name, prefix): 401 | # Fetch sentinel stats through redis-cli. 402 | rc, output = _execute('redis-cli %(opts)s SENTINEL ckquorum %(name)s' % { 403 | 'opts': opts, 404 | 'name': master_name, 405 | }) 406 | if rc == 0: 407 | # Examples: 408 | # - OK 3 usable Sentinels. Quorum and failover authorization can be 409 | # reached. 410 | # - NOQUORUM 1 usable Sentinels. Not enough available Sentinels to 411 | # reach the majority and authorize a failover. 412 | stats.add( 413 | name='%s:ckquorum' % prefix, 414 | value='1' if output.startswith('OK') else '0') 415 | items = output.split(' ', 2) 416 | if len(items) >= 2 and items[1].isdigit(): 417 | stats.add( 418 | name='%s:usable_sentinels' % prefix, 419 | value=items[1]) 420 | 421 | # Error recovering information from redis-cli. 422 | else: 423 | stats.log(output) 424 | 425 | 426 | def _stats_cluster(stats, opts): 427 | # Fetch cluster stats through redis-cli. 428 | rc, output = _execute('redis-cli %(opts)s CLUSTER INFO' % { 429 | 'opts': opts, 430 | }) 431 | if rc == 0: 432 | for line in output.splitlines(): 433 | if ':' in line: 434 | key, value = line.split(':', 1) 435 | stats.add( 436 | name='cluster:%s' % key.strip(), 437 | value=value.strip()) 438 | 439 | # Error recovering information from redis-cli. 440 | else: 441 | stats.log(output) 442 | 443 | 444 | def _safe_zabbix_string(value): 445 | # Return a modified version of 'value' safe to be used as part of: 446 | # - A quoted key parameter (see https://www.zabbix.com/documentation/5.0/manual/config/items/item/key). 447 | # - A JSON string. 448 | return value.replace('"', '\\"') 449 | 450 | 451 | def _execute(command, stdin=None): 452 | child = subprocess.Popen( 453 | command, 454 | shell=True, 455 | stdout=subprocess.PIPE, 456 | stdin=subprocess.PIPE, 457 | stderr=subprocess.STDOUT) 458 | output = child.communicate( 459 | input=stdin.encode('utf-8') if stdin is not None else None)[0].decode('utf-8') 460 | return child.returncode, output 461 | 462 | 463 | ############################################################################### 464 | ## MAIN 465 | ############################################################################### 466 | 467 | def main(): 468 | # Set up the base command line parser. 469 | parser = ArgumentParser() 470 | parser.add_argument( 471 | '-i', '--redis-instances', dest='redis_instances', 472 | type=str, required=True, 473 | help='comma-delimited list of Redis instances to get stats from ' 474 | '(port, host:port and unix:///path/to/socket formats are alowed') 475 | parser.add_argument( 476 | '-t', '--redis-type', dest='redis_type', 477 | type=str, required=True, choices=SUBJECTS.keys(), 478 | help='the type of the Redis instance to get stats from') 479 | parser.add_argument( 480 | '--redis-user', dest='redis_user', 481 | type=str, default=None, 482 | help='user name to be used in Redis instances authentication (redis >= 6.0)') 483 | parser.add_argument( 484 | '--redis-password', dest='redis_password', 485 | type=str, default=None, 486 | help='password required to access to Redis instances') 487 | subparsers = parser.add_subparsers(dest='command') 488 | 489 | # Set up 'stats' command. 490 | subparser = subparsers.add_parser( 491 | 'stats', 492 | help='collect Redis stats') 493 | 494 | # Set up 'discover' command. 495 | subparser = subparsers.add_parser( 496 | 'discover', 497 | help='generate Zabbix discovery schema') 498 | subparser.add_argument( 499 | 'subject', type=str, 500 | help='dynamic resources to be discovered') 501 | 502 | # Parse command line arguments. 503 | options = parser.parse_args() 504 | 505 | # Check subject to be discovered. 506 | if options.command == 'discover': 507 | subjects = SUBJECTS[options.redis_type].keys() 508 | if options.subject not in subjects: 509 | sys.stderr.write('Invalid subject (choose from %(subjects)s)\n' % { 510 | 'subjects': ', '.join("'{0}'".format(s) for s in subjects), 511 | }) 512 | sys.exit(1) 513 | 514 | # Execute command. 515 | if options.command: 516 | globals()[options.command](options) 517 | else: 518 | parser.print_help() 519 | sys.exit(1) 520 | sys.exit(0) 521 | 522 | if __name__ == '__main__': 523 | main() 524 | -------------------------------------------------------------------------------- /template-app-redis-sentinel.j2: -------------------------------------------------------------------------------- 1 | {%- set name = name|default('Redis Sentinel') -%} 2 | 3 | {%- set description = description|default('') -%} 4 | 5 | {%- set release = release|default('trunk') -%} 6 | 7 | {%- set seed = ['Allenta', 'Redis Sentinel']|join('/') -%} 8 | 9 | {%- set master = 'redis_sentinel.stats["{$REDIS_SENTINEL.LOCATIONS}"]' -%} 10 | 11 | {#-#########################################################################-#} 12 | {#- MACROS -#} 13 | {#-#########################################################################-#} 14 | 15 | {%- macro trigger(definition) -%} 16 | {{ [seed, definition.id]|join('/')|zuuid }} 17 | {{ definition.expression|e }} 18 | {{ definition.name|e }} 19 | {{ definition.priority }} 20 | {%- if 'recovery' in definition %} 21 | RECOVERY_EXPRESSION 22 | {{ definition.recovery|e }} 23 | {%- endif %} 24 | {%- endmacro -%} 25 | 26 | {%- macro discovery_rule(rule, items, triggers) -%} 27 | 28 | {{ [seed, rule.id]|join('/')|zuuid }} 29 | {{ rule.name|e }} 30 | ZABBIX_ACTIVE 31 | {{ rule.key|e }} 32 | {$REDIS_SENTINEL.LLD_UPDATE_INTERVAL:"{{ rule.context }}"} 33 | {$REDIS_SENTINEL.LLD_KEEP_LOST_RESOURCES_PERIOD:"{{ rule.context }}"} 34 | {%- if rule.context != 'items' %} 35 | 36 | 37 | 38 | {%- if version in ('6.0', '6.2', '6.4', '7.0') %} 39 | A 40 | {%- endif %} 41 | {{ '{#' }}SUBJECT} 42 | NOT_MATCHES_REGEX 43 | {$REDIS_SENTINEL.LLD_EXCLUDED_SUBJECTS:"{{ rule.context }}"} 44 | 45 | 46 | 47 | {%- endif %} 48 | 49 | {%- for item in items %} 50 | 51 | {{ [seed, item.id]|join('/')|zuuid }} 52 | Redis Sentinel[{{ '{#' }}LOCATION}] - {{ item.name|e }} 53 | {{ item.type }} 54 | {{ item.key|e }} 55 | {%- if 'delay' in item and item.delay %} 56 | {$REDIS_SENTINEL.ITEM_UPDATE_INTERVAL} 57 | {%- elif version in ('6.0', '6.2', '6.4', '7.0') %} 58 | 0 59 | {%- endif %} 60 | {%- if 'history' in item and not item.history %} 61 | 0 62 | {%- else %} 63 | {$REDIS_SENTINEL.ITEM_HISTORY_STORAGE_PERIOD} 64 | {%- endif %} 65 | {%- if 'trends' in item and not item.trends %} 66 | {%- if version in ('6.0', '6.2', '6.4', '7.0') %} 67 | 0 68 | {%- endif %} 69 | {%- else %} 70 | {$REDIS_SENTINEL.ITEM_TREND_STORAGE_PERIOD} 71 | {%- endif %} 72 | {%- if item.value_type != 'UNSIGNED' or version in ('6.0', '6.2', '6.4', '7.0', '7.2') %} 73 | {{ item.value_type }} 74 | {%- endif %} 75 | {{ item.units|default('')|e }} 76 | {{ item.params|default('')|e }} 77 | {%- if 'preprocessing' in item %} 78 | 79 | {%- for step in item.preprocessing %} 80 | 81 | {{ step.type }} 82 | 83 | {%- for param in step.params|default([]) %} 84 | {{ param|e }} 85 | {%- endfor %} 86 | 87 | {%- if 'error_handler' in step or version in ('6.0', '6.2', '6.4', '7.0', '7.2') %} 88 | {{ step.error_handler|default('ORIGINAL_ERROR') }} 89 | {%- endif %} 90 | 91 | {%- endfor %} 92 | 93 | {%- endif %} 94 | {%- if 'master_item_key' in item %} 95 | 96 | {{ item.master_item_key|e }} 97 | 98 | {%- endif %} 99 | 100 | 101 | Application 102 | Redis Sentinel 103 | 104 | 105 | {%- if 'triggers' in item %} 106 | 107 | {%- for definition in item.triggers %} 108 | 109 | {{ trigger(definition) }} 110 | 111 | {%- endfor %} 112 | 113 | {%- endif %} 114 | 115 | {%- endfor %} 116 | 117 | 118 | {%- for definition in triggers %} 119 | 120 | {{ trigger(definition) }} 121 | 122 | {%- endfor %} 123 | 124 | 125 | {%- endmacro -%} 126 | 127 | {#-#########################################################################-#} 128 | {#- MAIN -#} 129 | {#-#########################################################################-#} 130 | 131 | 132 | 133 | {{ version }} 134 | {%- if version in ('6.0', '6.2') %} 135 | 2018-10-30T08:22:30Z 136 | {%- endif %} 137 | {%- if version != '6.0' %} 138 | 139 | 140 | {%- else %} 141 | 142 | 143 | {%- endif %} 144 | 7df96b18c230490a9a0a9e2307226338 145 | Templates 146 | {%- if version != '6.0' %} 147 | 148 | 149 | {%- else %} 150 | 151 | 152 | {%- endif %} 153 | 154 | 528 | 529 | 530 | -------------------------------------------------------------------------------- /template-app-redis-server.j2: -------------------------------------------------------------------------------- 1 | {%- set name = name|default('Redis Server') -%} 2 | 3 | {%- set description = description|default('') -%} 4 | 5 | {%- set release = release|default('trunk') -%} 6 | 7 | {%- set seed = ['Allenta', 'Redis Server']|join('/') -%} 8 | 9 | {%- set master = 'redis_server.stats["{$REDIS_SERVER.LOCATIONS}"]' -%} 10 | 11 | {#-#########################################################################-#} 12 | {#- MACROS -#} 13 | {#-#########################################################################-#} 14 | 15 | {%- macro trigger(definition) -%} 16 | {{ [seed, definition.id]|join('/')|zuuid }} 17 | {{ definition.expression|e }} 18 | {{ definition.name|e }} 19 | {{ definition.priority }} 20 | {%- if 'recovery' in definition %} 21 | RECOVERY_EXPRESSION 22 | {{ definition.recovery|e }} 23 | {%- endif %} 24 | {%- endmacro -%} 25 | 26 | {%- macro discovery_rule(rule, items, triggers) -%} 27 | 28 | {{ [seed, rule.id]|join('/')|zuuid }} 29 | {{ rule.name|e }} 30 | ZABBIX_ACTIVE 31 | {{ rule.key|e }} 32 | {$REDIS_SERVER.LLD_UPDATE_INTERVAL:"{{ rule.context }}"} 33 | {$REDIS_SERVER.LLD_KEEP_LOST_RESOURCES_PERIOD:"{{ rule.context }}"} 34 | {%- if rule.context != 'items' %} 35 | 36 | 37 | 38 | {%- if version in ('6.0', '6.2', '6.4', '7.0') %} 39 | A 40 | {%- endif %} 41 | {{ '{#' }}SUBJECT} 42 | NOT_MATCHES_REGEX 43 | {$REDIS_SERVER.LLD_EXCLUDED_SUBJECTS:"{{ rule.context }}"} 44 | 45 | 46 | 47 | {%- endif %} 48 | 49 | {%- for item in items %} 50 | 51 | {{ [seed, item.id]|join('/')|zuuid }} 52 | Redis Server[{{ '{#' }}LOCATION}] - {{ item.name|e }} 53 | {{ item.type }} 54 | {{ item.key|e }} 55 | {%- if 'delay' in item and item.delay %} 56 | {$REDIS_SERVER.ITEM_UPDATE_INTERVAL} 57 | {%- elif version in ('6.0', '6.2', '6.4', '7.0') %} 58 | 0 59 | {%- endif %} 60 | {%- if 'history' in item and not item.history %} 61 | 0 62 | {%- else %} 63 | {$REDIS_SERVER.ITEM_HISTORY_STORAGE_PERIOD} 64 | {%- endif %} 65 | {%- if 'trends' in item and not item.trends %} 66 | {%- if version in ('6.0', '6.2', '6.4', '7.0') %} 67 | 0 68 | {%- endif %} 69 | {%- else %} 70 | {$REDIS_SERVER.ITEM_TREND_STORAGE_PERIOD} 71 | {%- endif %} 72 | {%- if item.value_type != 'UNSIGNED' or version in ('6.0', '6.2', '6.4', '7.0', '7.2') %} 73 | {{ item.value_type }} 74 | {%- endif %} 75 | {{ item.units|default('')|e }} 76 | {{ item.params|default('')|e }} 77 | {%- if 'preprocessing' in item %} 78 | 79 | {%- for step in item.preprocessing %} 80 | 81 | {{ step.type }} 82 | 83 | {%- for param in step.params|default([]) %} 84 | {{ param|e }} 85 | {%- endfor %} 86 | 87 | {%- if 'error_handler' in step or version in ('6.0', '6.2', '6.4', '7.0', '7.2') %} 88 | {{ step.error_handler|default('ORIGINAL_ERROR') }} 89 | {%- endif %} 90 | 91 | {%- endfor %} 92 | 93 | {%- endif %} 94 | {%- if 'master_item_key' in item %} 95 | 96 | {{ item.master_item_key|e }} 97 | 98 | {%- endif %} 99 | 100 | 101 | Application 102 | Redis Server 103 | 104 | 105 | {%- if 'triggers' in item %} 106 | 107 | {%- for definition in item.triggers %} 108 | 109 | {{ trigger(definition) }} 110 | 111 | {%- endfor %} 112 | 113 | {%- endif %} 114 | 115 | {%- endfor %} 116 | 117 | 118 | {%- for definition in triggers %} 119 | 120 | {{ trigger(definition) }} 121 | 122 | {%- endfor %} 123 | 124 | 125 | {%- endmacro -%} 126 | 127 | {#-#########################################################################-#} 128 | {#- MAIN -#} 129 | {#-#########################################################################-#} 130 | 131 | 132 | 133 | {{ version }} 134 | {%- if version in ('6.0', '6.2') %} 135 | 2018-10-29T09:52:03Z 136 | {%- endif %} 137 | {%- if version != '6.0' %} 138 | 139 | 140 | {%- else %} 141 | 142 | 143 | {%- endif %} 144 | 7df96b18c230490a9a0a9e2307226338 145 | Templates 146 | {%- if version != '6.0' %} 147 | 148 | 149 | {%- else %} 150 | 151 | 152 | {%- endif %} 153 | 154 | 1184 | 1185 | 1186 | --------------------------------------------------------------------------------