/* * Copyright 2016-present Open Networking Laboratory * * 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.onosproject.cpman.impl; import org.apache.commons.lang3.ArrayUtils; import org.onosproject.cpman.MetricsDatabase; import org.rrd4j.ConsolFun; import org.rrd4j.DsType; import org.rrd4j.core.ArcDef; import org.rrd4j.core.DsDef; import org.rrd4j.core.FetchRequest; import org.rrd4j.core.RrdBackendFactory; import org.rrd4j.core.RrdDb; import org.rrd4j.core.RrdDef; import org.rrd4j.core.Sample; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** * An implementation of control plane metrics back-end database. */ public final class DefaultMetricsDatabase implements MetricsDatabase { private static final Logger log = LoggerFactory.getLogger(DefaultMetricsDatabase.class); private String metricName; private String resourceName; private RrdDb rrdDb; private Sample sample; private static final long SECONDS_OF_DAY = 60L * 60L * 24L; private static final long SECONDS_OF_MINUTE = 60L; private static final ConsolFun CONSOL_FUNCTION = ConsolFun.LAST; private static final String NON_EXIST_METRIC = "Non-existing metric type."; private static final String INSUFFICIENT_DURATION = "Given duration less than one minute."; private static final String EXCEEDED_DURATION = "Given duration exceeds a day time."; /** * Constructs a metrics database using the given metric name and * round robin database. * * @param metricName metric name * @param rrdDb round robin database */ private DefaultMetricsDatabase(String metricName, String resourceName, RrdDb rrdDb) { this.metricName = metricName; this.resourceName = resourceName; this.rrdDb = rrdDb; } @Override public String metricName() { return this.metricName; } @Override public String resourceName() { return this.resourceName; } @Override public void updateMetric(String metricType, double value) { updateMetric(metricType, value, System.currentTimeMillis() / 1000L); } @Override public void updateMetric(String metricType, double value, long time) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); sample = rrdDb.createSample(time); sample.setValue(metricType, value); sample.update(); } catch (IOException e) { log.error("Failed to update metric value due to {}", e); } } @Override public void updateMetrics(Map<String, Double> metrics) { updateMetrics(metrics, System.currentTimeMillis() / 1000L); } @Override public void updateMetrics(Map<String, Double> metrics, long time) { try { sample = rrdDb.createSample(time); metrics.forEach((k, v) -> { try { checkArgument(rrdDb.containsDs(k), NON_EXIST_METRIC); sample.setValue(k, v); } catch (IOException e) { log.error("Failed to update metric value due to {}", e); } }); sample.update(); } catch (IOException e) { log.error("Failed to update metric values due to {}", e); } } @Override public double recentMetric(String metricType) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); return rrdDb.getDatasource(metricType).getLastValue(); } catch (IOException e) { log.error("Failed to obtain metric value due to {}", e); return 0D; } } @Override public double[] recentMetrics(String metricType, int duration, TimeUnit unit) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); long endTime = rrdDb.getLastUpdateTime(); long startTime = endTime - TimeUnit.SECONDS.convert(duration, unit); if (checkTimeRange(startTime, endTime)) { FetchRequest fr = rrdDb.createFetchRequest(CONSOL_FUNCTION, startTime, endTime); return arrangeDataPoints(fr.fetchData().getValues(metricType)); } else { log.warn("Data projection is out-of-range"); return new double[0]; } } catch (IOException e) { log.error("Failed to obtain metric values due to {}", e); return new double[0]; } } @Override public double minMetric(String metricType) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); long endTime = rrdDb.getLastUpdateTime() - 1; long startTime = endTime - SECONDS_OF_DAY + 1; return minMetric(metricType, startTime, endTime); } catch (IOException e) { log.error("Failed to obtain metric value due to {}", e); return 0D; } } @Override public double maxMetric(String metricType) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); long endTime = rrdDb.getLastUpdateTime(); long startTime = endTime - SECONDS_OF_DAY; return maxMetric(metricType, startTime, endTime); } catch (IOException e) { log.error("Failed to obtain metric value due to {}", e); return 0D; } } @Override public double[] metrics(String metricType) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); long endTime = rrdDb.getLastUpdateTime(); long startTime = endTime - SECONDS_OF_DAY; return metrics(metricType, startTime, endTime); } catch (IOException e) { log.error("Failed to obtain metric values due to {}", e); return new double[0]; } } @Override public double[] metrics(String metricType, long startTime, long endTime) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); if (checkTimeRange(startTime, endTime)) { FetchRequest fr = rrdDb.createFetchRequest(CONSOL_FUNCTION, startTime, endTime); return arrangeDataPoints(fr.fetchData().getValues(metricType)); } else { log.warn("Data projection is out-of-range"); return new double[0]; } } catch (IOException e) { log.error("Failed to obtain metric values due to {}", e); return new double[0]; } } @Override public long lastUpdate(String metricType) { try { checkArgument(rrdDb.containsDs(metricType), NON_EXIST_METRIC); return rrdDb.getLastUpdateTime(); } catch (IOException e) { log.error("Failed to obtain last update time due to {}", e); return 0L; } } // try to check whether projected time range is within a day private boolean checkTimeRange(long startTime, long endTime) { // check whether the given startTime and endTime larger than 1 minute checkArgument(endTime - startTime >= SECONDS_OF_MINUTE, INSUFFICIENT_DURATION); // check whether the given start time and endTime smaller than 1 day checkArgument(endTime - startTime <= SECONDS_OF_DAY, EXCEEDED_DURATION); return true; } // try to remove first and last data points private double[] arrangeDataPoints(double[] data) { return Arrays.copyOfRange(data, 1, data.length - 1); } // obtains maximum metric value among projected range private double maxMetric(String metricType, long startTime, long endTime) { double[] all = metrics(metricType, startTime, endTime); List list = Arrays.asList(ArrayUtils.toObject(all)); return (double) Collections.max(list); } // obtains minimum metric value among projected range private double minMetric(String metricType, long startTime, long endTime) { double[] all = metrics(metricType, startTime, endTime); List list = Arrays.asList(ArrayUtils.toObject(all)); return (double) Collections.min(list); } public static final class Builder implements MetricsDatabase.Builder { private static final int RESOLUTION_IN_SECOND = 60; private static final String STORING_METHOD = "MEMORY"; private static final DsType SOURCE_TYPE = DsType.GAUGE; private static final String DB_PATH = "CPMAN"; private static final ConsolFun CONSOL_FUNCTION = ConsolFun.LAST; private static final double MIN_VALUE = 0; private static final double MAX_VALUE = Double.NaN; private static final double XFF_VALUE = 0.2; private static final int STEP_VALUE = 1; private static final int ROW_VALUE = 60 * 24; private static final String METRIC_NAME_MSG = "Must specify a metric name."; private static final String RESOURCE_NAME_MSG = "Must specify a resource name."; private static final String METRIC_TYPE_MSG = "Must supply at least a metric type."; private static final String SPLITTER = "_"; private RrdDb rrdDb; private RrdDef rrdDef; private List<DsDef> dsDefs; private String metricName; private String resourceName; public Builder() { // initialize data source definition list dsDefs = new ArrayList<>(); } @Override public Builder withMetricName(String metric) { this.metricName = metric; return this; } @Override public MetricsDatabase.Builder withResourceName(String resource) { this.resourceName = resource; return this; } @Override public Builder addMetricType(String metricType) { dsDefs.add(defineSchema(metricType)); return this; } @Override public MetricsDatabase build() { checkNotNull(metricName, METRIC_NAME_MSG); checkNotNull(resourceName, RESOURCE_NAME_MSG); checkArgument(!dsDefs.isEmpty(), METRIC_TYPE_MSG); // define the resolution of monitored metrics rrdDef = new RrdDef(DB_PATH + SPLITTER + metricName + SPLITTER + resourceName, RESOLUTION_IN_SECOND); try { DsDef[] dsDefArray = new DsDef[dsDefs.size()]; IntStream.range(0, dsDefs.size()).forEach(i -> dsDefArray[i] = dsDefs.get(i)); rrdDef.addDatasource(dsDefArray); rrdDef.setStep(RESOLUTION_IN_SECOND); // raw archive, no aggregation is required ArcDef rawArchive = new ArcDef(CONSOL_FUNCTION, XFF_VALUE, STEP_VALUE, ROW_VALUE); rrdDef.addArchive(rawArchive); // always store the metric data in memory... rrdDb = new RrdDb(rrdDef, RrdBackendFactory.getFactory(STORING_METHOD)); } catch (IOException e) { log.warn("Failed to create a new round-robin database due to {}", e); } return new DefaultMetricsDatabase(metricName, resourceName, rrdDb); } private DsDef defineSchema(String metricType) { return new DsDef(metricType, SOURCE_TYPE, RESOLUTION_IN_SECOND, MIN_VALUE, MAX_VALUE); } } }