/* * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.boot.actuate.metrics.repository.redis; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Set; import org.springframework.boot.actuate.metrics.Metric; import org.springframework.boot.actuate.metrics.repository.MetricRepository; import org.springframework.boot.actuate.metrics.writer.Delta; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.BoundZSetOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.util.Assert; /** * A {@link MetricRepository} implementation for a redis backend. Metric values are stored * as zset values plus a regular hash value for the timestamp, both against a key composed * of the metric name prefixed with a constant (default "spring.metrics."). If you have * multiple metrics repositories all point at the same instance of Redis, it may be useful * to change the prefix to be unique (but not if you want them to contribute to the same * metrics). * * @author Dave Syer */ public class RedisMetricRepository implements MetricRepository { private static final String DEFAULT_METRICS_PREFIX = "spring.metrics."; private static final String DEFAULT_KEY = "keys.spring.metrics"; private String prefix = DEFAULT_METRICS_PREFIX; private String key = DEFAULT_KEY; private BoundZSetOperations<String, String> zSetOperations; private final RedisOperations<String, String> redisOperations; /** * Create a RedisMetricRepository with a default prefix to apply to all metric names. * If multiple repositories share a redis instance they will feed into the same global * metrics. * @param redisConnectionFactory the redis connection factory */ public RedisMetricRepository(RedisConnectionFactory redisConnectionFactory) { this(redisConnectionFactory, null); } /** * Create a RedisMetricRepository with a prefix to apply to all metric names (ideally * unique to this repository or to a logical repository contributed to by multiple * instances, where they all see the same values). Recommended constructor for general * purpose use. * @param redisConnectionFactory the redis connection factory * @param prefix the prefix to set for all metrics keys */ public RedisMetricRepository(RedisConnectionFactory redisConnectionFactory, String prefix) { this(redisConnectionFactory, prefix, null); } /** * Allows user to set the prefix and key to use to store the index of other keys. The * redis store will hold a zset under the key just so the metric names can be * enumerated. Read operations, especially {@link #findAll()} and {@link #count()}, * will only be accurate if the key is unique to the prefix of this repository. * @param redisConnectionFactory the redis connection factory * @param prefix the prefix to set for all metrics keys * @param key the key to set */ public RedisMetricRepository(RedisConnectionFactory redisConnectionFactory, String prefix, String key) { if (prefix == null) { prefix = DEFAULT_METRICS_PREFIX; if (key == null) { key = DEFAULT_KEY; } } else if (key == null) { key = "keys." + prefix; } Assert.notNull(redisConnectionFactory, "RedisConnectionFactory must not be null"); this.redisOperations = RedisUtils.stringTemplate(redisConnectionFactory); if (!prefix.endsWith(".")) { prefix = prefix + "."; } this.prefix = prefix; if (key.endsWith(".")) { key = key.substring(0, key.length() - 1); } this.key = key; this.zSetOperations = this.redisOperations.boundZSetOps(this.key); } @Override public Metric<?> findOne(String metricName) { String redisKey = keyFor(metricName); String raw = this.redisOperations.opsForValue().get(redisKey); return deserialize(redisKey, raw, this.zSetOperations.score(redisKey)); } @Override public Iterable<Metric<?>> findAll() { // This set is sorted Set<String> keys = this.zSetOperations.range(0, -1); Iterator<String> keysIt = keys.iterator(); List<Metric<?>> result = new ArrayList<>(keys.size()); List<String> values = this.redisOperations.opsForValue().multiGet(keys); for (String v : values) { String key = keysIt.next(); Metric<?> value = deserialize(key, v, this.zSetOperations.score(key)); if (value != null) { result.add(value); } } return result; } @Override public long count() { return this.zSetOperations.size(); } @Override public void increment(Delta<?> delta) { String name = delta.getName(); String key = keyFor(name); trackMembership(key); double value = this.zSetOperations.incrementScore(key, delta.getValue().doubleValue()); String raw = serialize(new Metric<>(name, value, delta.getTimestamp())); this.redisOperations.opsForValue().set(key, raw); } @Override public void set(Metric<?> value) { String name = value.getName(); String key = keyFor(name); trackMembership(key); this.zSetOperations.add(key, value.getValue().doubleValue()); String raw = serialize(value); this.redisOperations.opsForValue().set(key, raw); } @Override public void reset(String metricName) { String key = keyFor(metricName); if (this.zSetOperations.remove(key) == 1) { this.redisOperations.delete(key); } } private Metric<?> deserialize(String redisKey, String v, Double value) { if (redisKey == null || v == null || !redisKey.startsWith(this.prefix)) { return null; } Date timestamp = new Date(Long.valueOf(v)); return new Metric<>(nameFor(redisKey), value, timestamp); } private String serialize(Metric<?> entity) { return String.valueOf(entity.getTimestamp().getTime()); } private String keyFor(String name) { return this.prefix + name; } private String nameFor(String redisKey) { return redisKey.substring(this.prefix.length()); } private void trackMembership(String redisKey) { this.zSetOperations.incrementScore(redisKey, 0.0D); } }