View Javadoc

1   /*
2    * Copyright The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements. See the NOTICE file distributed with this
6    * work for additional information regarding copyright ownership. The ASF
7    * licenses this file to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   * http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16   * License for the specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.hadoop.hbase.regionserver.metrics;
21  
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.TreeMap;
30  import java.util.TreeSet;
31  import java.util.concurrent.ConcurrentHashMap;
32  import java.util.concurrent.atomic.AtomicLongArray;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  
36  import org.apache.commons.lang.mutable.MutableDouble;
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.apache.hadoop.conf.Configuration;
40  import org.apache.hadoop.hbase.HConstants;
41  import org.apache.hadoop.hbase.io.hfile.BlockType.BlockCategory;
42  import org.apache.hadoop.hbase.util.Bytes;
43  import org.apache.hadoop.hbase.util.Pair;
44  
45  /**
46   * A collection of metric names in a given column family or a (table, column
47   * family) combination. The following "dimensions" are supported:
48   * <ul>
49   * <li>Table name (optional; enabled based on configuration)</li>
50   * <li>Per-column family vs. aggregated. The aggregated mode is only supported
51   * when table name is not included.</li>
52   * <li>Block category (data, index, bloom filter, etc.)</li>
53   * <li>Whether the request is part of a compaction</li>
54   * <li>Metric type (read time, block read count, cache hits/misses, etc.)</li>
55   * </ul>
56   * <p>
57   * An instance of this class does not store any metric values. It just allows
58   * to determine the correct metric name for each combination of the above
59   * dimensions.
60   * <p>
61   * <table>
62   * <tr>
63   * <th rowspan="2">Metric key</th>
64   * <th colspan="2">Per-table metrics conf setting</th>
65   * <th rowspan="2">Description</th>
66   * </tr>
67   * <tr>
68   * <th>On</th>
69   * <th>Off</th>
70   * </th>
71   * <tr>
72   *   <td> tbl.T.cf.CF.M </td> <td> Include </td> <td> Skip    </td>
73   *   <td> A specific column family of a specific table        </td>
74   * </tr>
75   * <tr>
76   *   <td> tbl.T.M       </td> <td> Skip    </td> <td> Skip    </td>
77   *   <td> All column families in the given table              </td>
78   * </tr>
79   * <tr>
80   *   <td> cf.CF.M       </td> <td> Skip    </td> <td> Include </td>
81   *   <td> A specific column family in all tables              </td>
82   * </tr>
83   * <tr>
84   *   <td> M             </td> <td> Include </td> <td> Include </td>
85   *   <td> All column families in all tables                   </td>
86   * </tr>
87   * </table>
88   */
89  public class SchemaMetrics {
90  
91    public interface SchemaAware {
92      public String getTableName();
93      public String getColumnFamilyName();
94      public SchemaMetrics getSchemaMetrics();
95    }
96  
97    private static final Log LOG = LogFactory.getLog(SchemaMetrics.class);
98  
99    public static enum BlockMetricType {
100     // Metric configuration: compactionAware, timeVarying
101     READ_TIME("Read",                   true, true),
102     READ_COUNT("BlockReadCnt",          true, false),
103     CACHE_HIT("BlockReadCacheHitCnt",   true, false),
104     CACHE_MISS("BlockReadCacheMissCnt", true, false),
105 
106     CACHE_SIZE("blockCacheSize",        false, false),
107     CACHED("blockCacheNumCached",       false, false),
108     EVICTED("blockCacheNumEvicted",     false, false);
109 
110     private final String metricStr;
111     private final boolean compactionAware;
112     private final boolean timeVarying;
113 
114     BlockMetricType(String metricStr, boolean compactionAware,
115           boolean timeVarying) {
116       this.metricStr = metricStr;
117       this.compactionAware = compactionAware;
118       this.timeVarying = timeVarying;
119     }
120 
121     @Override
122     public String toString() {
123       return metricStr;
124     }
125 
126     private static final String BLOCK_METRIC_TYPE_RE;
127     static {
128       StringBuilder sb = new StringBuilder();
129       for (BlockMetricType bmt : values()) {
130         if (sb.length() > 0)
131           sb.append("|");
132         sb.append(bmt);
133       }
134       BLOCK_METRIC_TYPE_RE = sb.toString();
135     }
136   };
137 
138   public static enum StoreMetricType {
139     STORE_FILE_COUNT("storeFileCount"),
140     STORE_FILE_INDEX_SIZE("storeFileIndexSizeMB"),
141     STORE_FILE_SIZE_MB("storeFileSizeMB"),
142     STATIC_BLOOM_SIZE_KB("staticBloomSizeKB"),
143     MEMSTORE_SIZE_MB("memstoreSizeMB"),
144     STATIC_INDEX_SIZE_KB("staticIndexSizeKB"),
145     FLUSH_SIZE("flushSize");
146 
147     private final String metricStr;
148 
149     StoreMetricType(String metricStr) {
150       this.metricStr = metricStr;
151     }
152 
153     @Override
154     public String toString() {
155       return metricStr;
156     }
157   };
158 
159   // Constants
160   /**
161    * A string used when column family or table name is unknown, and in some
162    * unit tests. This should not normally show up in metric names but if it
163    * does it is better than creating a silent discrepancy in total vs.
164    * per-CF/table metrics.
165    */
166   public static final String UNKNOWN = "__unknown";
167 
168   public static final String TABLE_PREFIX = "tbl.";
169   public static final String CF_PREFIX = "cf.";
170   public static final String BLOCK_TYPE_PREFIX = "bt.";
171   public static final String REGION_PREFIX = "region.";
172 
173   public static final String CF_UNKNOWN_PREFIX = CF_PREFIX + UNKNOWN + ".";
174   public static final String CF_BAD_FAMILY_PREFIX = CF_PREFIX + "__badfamily.";
175 
176   /** Use for readability when obtaining non-compaction counters */
177   public static final boolean NO_COMPACTION = false;
178 
179   public static final String METRIC_GETSIZE = "getsize";
180   public static final String METRIC_NEXTSIZE = "nextsize";
181 
182   /**
183    * A special schema metric value that means "all tables aggregated" or
184    * "all column families aggregated" when used as a table name or a column
185    * family name.
186    */
187   public static final String TOTAL_KEY = "";
188 
189   /**
190    * Special handling for meta-block-specific metrics for
191    * backwards-compatibility.
192    */
193   private static final String META_BLOCK_CATEGORY_STR = "Meta";
194 
195   private static final int NUM_BLOCK_CATEGORIES =
196       BlockCategory.values().length;
197 
198   private static final int NUM_METRIC_TYPES =
199       BlockMetricType.values().length;
200 
201   static final boolean[] BOOL_VALUES = new boolean[] { false, true };
202 
203   private static final int NUM_BLOCK_METRICS =
204       NUM_BLOCK_CATEGORIES *  // blockCategory
205       BOOL_VALUES.length *    // isCompaction
206       NUM_METRIC_TYPES;       // metricType
207 
208   private static final int NUM_STORE_METRIC_TYPES =
209       StoreMetricType.values().length;
210 
211   /** Conf key controlling whether we include table name in metric names */
212   private static final String SHOW_TABLE_NAME_CONF_KEY =
213       "hbase.metrics.showTableName";
214 
215   /** We use this when too many column families are involved in a request. */
216   private static final String MORE_CFS_OMITTED_STR = "__more";
217 
218   /**
219    * Maximum length of a metric name prefix. Used when constructing metric
220    * names from a set of column families participating in a request.
221    */
222   private static final int MAX_METRIC_PREFIX_LENGTH =
223       256 - MORE_CFS_OMITTED_STR.length();
224 
225   // Global variables
226   /**
227    * Maps a string key consisting of table name and column family name, with
228    * table name optionally replaced with {@link #TOTAL_KEY} if per-table
229    * metrics are disabled, to an instance of this class.
230    */
231   private static final ConcurrentHashMap<String, SchemaMetrics>
232       tableAndFamilyToMetrics = new ConcurrentHashMap<String, SchemaMetrics>();
233 
234   /** Metrics for all tables and column families. */
235   // This has to be initialized after cfToMetrics.
236   public static final SchemaMetrics ALL_SCHEMA_METRICS =
237     getInstance(TOTAL_KEY, TOTAL_KEY);
238 
239   /** Threshold for flush the metrics, currently used only for "on cache hit" */
240   private static final long THRESHOLD_METRICS_FLUSH = 100l;
241 
242   /**
243    * Whether to include table name in metric names. If this is null, it has not
244    * been initialized. This is a global instance, but we also have a copy of it
245    * per a {@link SchemaMetrics} object to avoid synchronization overhead.
246    */
247   private static volatile Boolean useTableNameGlobally;
248 
249   /** Whether we logged a message about configuration inconsistency */
250   private static volatile boolean loggedConfInconsistency;
251 
252   // Instance variables
253   private final String[] blockMetricNames = new String[NUM_BLOCK_METRICS];
254   private final boolean[] blockMetricTimeVarying =
255       new boolean[NUM_BLOCK_METRICS];
256 
257   private final String[] bloomMetricNames = new String[2];
258   private final String[] storeMetricNames = new String[NUM_STORE_METRIC_TYPES];
259   private final String[] storeMetricNamesMax = new String[NUM_STORE_METRIC_TYPES];
260   private final AtomicLongArray onHitCacheMetrics= 
261       new AtomicLongArray(NUM_BLOCK_CATEGORIES * BOOL_VALUES.length);
262 
263   private SchemaMetrics(final String tableName, final String cfName) {
264     String metricPrefix = SchemaMetrics.generateSchemaMetricsPrefix(
265         tableName, cfName);
266 
267     for (BlockCategory blockCategory : BlockCategory.values()) {
268       for (boolean isCompaction : BOOL_VALUES) {
269         // initialize the cache metrics
270         onHitCacheMetrics.set(getCacheHitMetricIndex(blockCategory, isCompaction), 0);
271         
272         for (BlockMetricType metricType : BlockMetricType.values()) {
273           if (!metricType.compactionAware && isCompaction) {
274             continue;
275           }
276 
277           StringBuilder sb = new StringBuilder(metricPrefix);
278           if (blockCategory != BlockCategory.ALL_CATEGORIES
279               && blockCategory != BlockCategory.META) {
280             String categoryStr = blockCategory.toString();
281             categoryStr = categoryStr.charAt(0)
282                 + categoryStr.substring(1).toLowerCase();
283             sb.append(BLOCK_TYPE_PREFIX + categoryStr + ".");
284           }
285 
286           if (metricType.compactionAware) {
287             sb.append(isCompaction ? "compaction" : "fs");
288           }
289 
290           // A special-case for meta blocks for backwards-compatibility.
291           if (blockCategory == BlockCategory.META) {
292             sb.append(META_BLOCK_CATEGORY_STR);
293           }
294 
295           sb.append(metricType);
296 
297           int i = getBlockMetricIndex(blockCategory, isCompaction, metricType);
298           blockMetricNames[i] = sb.toString();
299           blockMetricTimeVarying[i] = metricType.timeVarying;
300         }
301       }
302     }
303 
304     for (boolean isInBloom : BOOL_VALUES) {
305       bloomMetricNames[isInBloom ? 1 : 0] = metricPrefix
306           + (isInBloom ? "keyMaybeInBloomCnt" : "keyNotInBloomCnt");
307     }
308 
309     for (StoreMetricType storeMetric : StoreMetricType.values()) {
310       String coreName = metricPrefix + storeMetric.toString();
311       storeMetricNames[storeMetric.ordinal()] = coreName;
312       storeMetricNamesMax[storeMetric.ordinal()] = coreName + ".max";
313     }
314   }
315 
316   /**
317    * Returns a {@link SchemaMetrics} object for the given table and column
318    * family, instantiating it if necessary.
319    *
320    * @param tableName table name (null is interpreted as "unknown"). This is
321    *          ignored
322    * @param cfName column family name (null is interpreted as "unknown")
323    */
324   public static SchemaMetrics getInstance(String tableName, String cfName) {
325     if (tableName == null) {
326       tableName = UNKNOWN;
327     }
328 
329     if (cfName == null) {
330       cfName = UNKNOWN;
331     }
332 
333     tableName = getEffectiveTableName(tableName);
334 
335     final String instanceKey = tableName + "\t" + cfName;
336     SchemaMetrics schemaMetrics = tableAndFamilyToMetrics.get(instanceKey);
337     if (schemaMetrics != null) {
338       return schemaMetrics;
339     }
340 
341     schemaMetrics = new SchemaMetrics(tableName, cfName);
342     SchemaMetrics existingMetrics =
343         tableAndFamilyToMetrics.putIfAbsent(instanceKey, schemaMetrics);
344     return existingMetrics != null ? existingMetrics : schemaMetrics;
345   }
346 
347   private static final int getCacheHitMetricIndex (BlockCategory blockCategory,
348       boolean isCompaction) {
349     return blockCategory.ordinal() * BOOL_VALUES.length + (isCompaction ? 1 : 0);
350   }
351   
352   private static final int getBlockMetricIndex(BlockCategory blockCategory,
353       boolean isCompaction, BlockMetricType metricType) {
354     int i = 0;
355     i = i * NUM_BLOCK_CATEGORIES + blockCategory.ordinal();
356     i = i * BOOL_VALUES.length + (isCompaction ? 1 : 0);
357     i = i * NUM_METRIC_TYPES + metricType.ordinal();
358     return i;
359   }
360 
361   public String getBlockMetricName(BlockCategory blockCategory,
362       boolean isCompaction, BlockMetricType metricType) {
363     if (isCompaction && !metricType.compactionAware) {
364       throw new IllegalArgumentException("isCompaction cannot be true for "
365           + metricType);
366     }
367     return blockMetricNames[getBlockMetricIndex(blockCategory, isCompaction,
368         metricType)];
369   }
370 
371   public String getBloomMetricName(boolean isInBloom) {
372     return bloomMetricNames[isInBloom ? 1 : 0];
373   }
374 
375   /**
376    * Increments the given metric, both per-CF and aggregate, for both the given
377    * category and all categories in aggregate (four counters total).
378    */
379   private void incrNumericMetric(BlockCategory blockCategory,
380       boolean isCompaction, BlockMetricType metricType) {
381     incrNumericMetric (blockCategory, isCompaction, metricType, 1);
382   }
383   
384   /**
385    * Increments the given metric, both per-CF and aggregate, for both the given
386    * category and all categories in aggregate (four counters total).
387    */
388   private void incrNumericMetric(BlockCategory blockCategory,
389       boolean isCompaction, BlockMetricType metricType, long amount) {
390     if (blockCategory == null) {
391       blockCategory = BlockCategory.UNKNOWN;  // So that we see this in stats.
392     }
393     RegionMetricsStorage.incrNumericMetric(getBlockMetricName(blockCategory,
394         isCompaction, metricType), amount);
395 
396     if (blockCategory != BlockCategory.ALL_CATEGORIES) {
397       incrNumericMetric(BlockCategory.ALL_CATEGORIES, isCompaction,
398           metricType, amount);
399     }
400   }
401 
402   private void addToReadTime(BlockCategory blockCategory,
403       boolean isCompaction, long timeMs) {
404     RegionMetricsStorage.incrTimeVaryingMetric(getBlockMetricName(blockCategory,
405         isCompaction, BlockMetricType.READ_TIME), timeMs);
406 
407     // Also update the read time aggregated across all block categories
408     if (blockCategory != BlockCategory.ALL_CATEGORIES) {
409       addToReadTime(BlockCategory.ALL_CATEGORIES, isCompaction, timeMs);
410     }
411   }
412 
413   /**
414    * Used to accumulate store metrics across multiple regions in a region
415    * server.  These metrics are not "persistent", i.e. we keep overriding them
416    * on every update instead of incrementing, so we need to accumulate them in
417    * a temporary map before pushing them to the global metric collection.
418    * @param tmpMap a temporary map for accumulating store metrics
419    * @param storeMetricType the store metric type to increment
420    * @param val the value to add to the metric
421    */
422   public void accumulateStoreMetric(final Map<String, MutableDouble> tmpMap,
423       StoreMetricType storeMetricType, double val) {
424     final String key = getStoreMetricName(storeMetricType);
425     if (tmpMap.get(key) == null) {
426       tmpMap.put(key, new MutableDouble(val));
427     } else {
428       tmpMap.get(key).add(val);
429     }
430 
431     if (this == ALL_SCHEMA_METRICS) {
432       // also compute the max value across all Stores on this server
433       final String maxKey = getStoreMetricNameMax(storeMetricType);
434       MutableDouble cur = tmpMap.get(maxKey);
435       if (cur == null) {
436         tmpMap.put(maxKey, new MutableDouble(val));
437       } else if (cur.doubleValue() < val) {
438         cur.setValue(val);
439       }
440     } else {
441       ALL_SCHEMA_METRICS.accumulateStoreMetric(tmpMap, storeMetricType, val);
442     }
443   }
444 
445   public String getStoreMetricName(StoreMetricType storeMetricType) {
446     return storeMetricNames[storeMetricType.ordinal()];
447   }
448 
449   public String getStoreMetricNameMax(StoreMetricType storeMetricType) {
450     return storeMetricNamesMax[storeMetricType.ordinal()];
451   }
452 
453   /**
454    * Update a metric that does not get reset on every poll.
455    * @param storeMetricType the store metric to update
456    * @param value the value to update the metric to
457    */
458   public void updatePersistentStoreMetric(StoreMetricType storeMetricType,
459       long value) {
460     RegionMetricsStorage.incrNumericPersistentMetric(
461         storeMetricNames[storeMetricType.ordinal()], value);
462   }
463 
464   /**
465    * Updates the number of hits and the total number of block reads on a block
466    * cache hit.
467    */
468   public void updateOnCacheHit(BlockCategory blockCategory,
469       boolean isCompaction) {
470     updateOnCacheHit(blockCategory, isCompaction, 1);
471   }
472   
473   /**
474    * Updates the number of hits and the total number of block reads on a block
475    * cache hit.
476    */
477   public void updateOnCacheHit(BlockCategory blockCategory,
478       boolean isCompaction, long count) {
479     blockCategory.expectSpecific();
480     int idx = getCacheHitMetricIndex(blockCategory, isCompaction);
481     
482     if (this.onHitCacheMetrics.addAndGet(idx, count) > THRESHOLD_METRICS_FLUSH) {
483       flushCertainOnCacheHitMetrics(blockCategory, isCompaction);
484     }
485     
486     if (this != ALL_SCHEMA_METRICS) {
487       ALL_SCHEMA_METRICS.updateOnCacheHit(blockCategory, isCompaction, count);
488     }
489   }
490   
491   private void flushCertainOnCacheHitMetrics(BlockCategory blockCategory, boolean isCompaction) {
492     int idx = getCacheHitMetricIndex(blockCategory, isCompaction);
493     long tempCount = this.onHitCacheMetrics.getAndSet(idx, 0);
494     
495     if (tempCount > 0) {
496       incrNumericMetric(blockCategory, isCompaction, BlockMetricType.CACHE_HIT, tempCount);
497       incrNumericMetric(blockCategory, isCompaction, BlockMetricType.READ_COUNT, tempCount);
498     }
499   }
500   
501   /**
502    * Flush the on cache hit metrics;
503    */
504   private void flushOnCacheHitMetrics() {
505     for (BlockCategory blockCategory : BlockCategory.values()) {
506       for (boolean isCompaction : BOOL_VALUES) {
507         flushCertainOnCacheHitMetrics (blockCategory, isCompaction);
508       }
509     }
510     
511     if (this != ALL_SCHEMA_METRICS) {
512       ALL_SCHEMA_METRICS.flushOnCacheHitMetrics();
513     }
514   }
515 
516   /**
517    * Notify the SchemaMetrics to flush all of the the metrics
518    */
519   public void flushMetrics() {
520     // currently only for "on cache hit metrics"
521     flushOnCacheHitMetrics();
522   }
523   
524   /**
525    * Updates read time, the number of misses, and the total number of block
526    * reads on a block cache miss.
527    */
528   public void updateOnCacheMiss(BlockCategory blockCategory,
529       boolean isCompaction, long timeMs) {
530     blockCategory.expectSpecific();
531     addToReadTime(blockCategory, isCompaction, timeMs);
532     incrNumericMetric(blockCategory, isCompaction, BlockMetricType.CACHE_MISS);
533     incrNumericMetric(blockCategory, isCompaction, BlockMetricType.READ_COUNT);
534     if (this != ALL_SCHEMA_METRICS) {
535       ALL_SCHEMA_METRICS.updateOnCacheMiss(blockCategory, isCompaction,
536           timeMs);
537     }
538   }
539 
540   /**
541    * Adds the given delta to the cache size for the given block category and
542    * the aggregate metric for all block categories. Updates both the per-CF
543    * counter and the counter for all CFs (four metrics total). The cache size
544    * metric is "persistent", i.e. it does not get reset when metrics are
545    * collected.
546    */
547   public void addToCacheSize(BlockCategory category, long cacheSizeDelta) {
548     if (category == null) {
549       category = BlockCategory.ALL_CATEGORIES;
550     }
551     RegionMetricsStorage.incrNumericPersistentMetric(getBlockMetricName(category, false,
552         BlockMetricType.CACHE_SIZE), cacheSizeDelta);
553 
554     if (category != BlockCategory.ALL_CATEGORIES) {
555       addToCacheSize(BlockCategory.ALL_CATEGORIES, cacheSizeDelta);
556     }
557   }
558 
559   public void updateOnCachePutOrEvict(BlockCategory blockCategory,
560       long cacheSizeDelta, boolean isEviction) {
561     addToCacheSize(blockCategory, cacheSizeDelta);
562     incrNumericMetric(blockCategory, false,
563         isEviction ? BlockMetricType.EVICTED : BlockMetricType.CACHED);
564     if (this != ALL_SCHEMA_METRICS) {
565       ALL_SCHEMA_METRICS.updateOnCachePutOrEvict(blockCategory, cacheSizeDelta,
566           isEviction);
567     }
568   }
569 
570   /**
571    * Increments both the per-CF and the aggregate counter of bloom
572    * positives/negatives as specified by the argument.
573    */
574   public void updateBloomMetrics(boolean isInBloom) {
575     RegionMetricsStorage.incrNumericMetric(getBloomMetricName(isInBloom), 1);
576     if (this != ALL_SCHEMA_METRICS) {
577       ALL_SCHEMA_METRICS.updateBloomMetrics(isInBloom);
578     }
579   }
580 
581   /**
582    * Sets the flag whether to use table name in metric names according to the
583    * given configuration. This must be called at least once before
584    * instantiating HFile readers/writers.
585    */
586   public static void configureGlobally(Configuration conf) {
587     if (conf != null) {
588       final boolean useTableNameNew =
589           conf.getBoolean(SHOW_TABLE_NAME_CONF_KEY, true);
590       setUseTableName(useTableNameNew);
591     } else {
592       setUseTableName(true);
593     }
594   }
595 
596   /**
597    * Determine the table name to be included in metric keys. If the global
598    * configuration says that we should not use table names in metrics,
599    * we always return {@link #TOTAL_KEY} even if nontrivial table name is
600    * provided.
601    *
602    * @param tableName a table name or {@link #TOTAL_KEY} when aggregating
603    * across all tables
604    * @return the table name to use in metric keys
605    */
606   private static String getEffectiveTableName(String tableName) {
607     if (!tableName.equals(TOTAL_KEY)) {
608       // We are provided with a non-trivial table name (including "unknown").
609       // We need to know whether table name should be included into metrics.
610       if (useTableNameGlobally == null) {
611         throw new IllegalStateException("The value of the "
612             + SHOW_TABLE_NAME_CONF_KEY + " conf option has not been specified "
613             + "in SchemaMetrics");
614       }
615       final boolean useTableName = useTableNameGlobally;
616       if (!useTableName) {
617         // Don't include table name in metric keys.
618         tableName = TOTAL_KEY;
619       }
620     }
621     return tableName;
622   }
623 
624   /**
625    * Method to transform a combination of a table name and a column family name
626    * into a metric key prefix. Tables/column family names equal to
627    * {@link #TOTAL_KEY} are omitted from the prefix.
628    *
629    * @param tableName the table name or {@link #TOTAL_KEY} for all tables
630    * @param cfName the column family name or {@link #TOTAL_KEY} for all CFs
631    * @return the metric name prefix, ending with a dot.
632    */
633   public static String generateSchemaMetricsPrefix(String tableName,
634       final String cfName) {
635     tableName = getEffectiveTableName(tableName);
636     String schemaMetricPrefix =
637         tableName.equals(TOTAL_KEY) ? "" : TABLE_PREFIX + tableName + ".";
638     schemaMetricPrefix +=
639         cfName.equals(TOTAL_KEY) ? "" : CF_PREFIX + cfName + ".";
640     return schemaMetricPrefix;
641   }
642 
643   public static String generateSchemaMetricsPrefix(byte[] tableName,
644       byte[] cfName) {
645     return generateSchemaMetricsPrefix(Bytes.toString(tableName),
646         Bytes.toString(cfName));
647   }
648 
649   /**
650    * Method to transform a set of column families in byte[] format with table
651    * name into a metric key prefix.
652    *
653    * @param tableName the table name or {@link #TOTAL_KEY} for all tables
654    * @param families the ordered set of column families
655    * @return the metric name prefix, ending with a dot, or an empty string in
656    *         case of invalid arguments. This is OK since we always expect
657    *         some CFs to be included.
658    */
659   public static String generateSchemaMetricsPrefix(String tableName,
660       Set<byte[]> families) {
661     if (families == null || families.isEmpty() ||
662         tableName == null || tableName.isEmpty()) {
663       return "";
664     }
665 
666     if (families.size() == 1) {
667       return generateSchemaMetricsPrefix(tableName,
668           Bytes.toString(families.iterator().next()));
669     }
670 
671     tableName = getEffectiveTableName(tableName);
672     List<byte[]> sortedFamilies = new ArrayList<byte[]>(families);
673     Collections.sort(sortedFamilies, Bytes.BYTES_COMPARATOR);
674 
675     StringBuilder sb = new StringBuilder();
676 
677     int numCFsLeft = families.size();
678     for (byte[] family : sortedFamilies) {
679       if (sb.length() > MAX_METRIC_PREFIX_LENGTH) {
680         sb.append(MORE_CFS_OMITTED_STR);
681         break;
682       }
683       --numCFsLeft;
684       sb.append(Bytes.toString(family));
685       if (numCFsLeft > 0) {
686         sb.append("~");
687       }
688     }
689 
690     return SchemaMetrics.generateSchemaMetricsPrefix(tableName, sb.toString());
691   }
692 
693   /**
694    * Get the prefix for metrics generated about a single region.
695    * 
696    * @param tableName
697    *          the table name or {@link #TOTAL_KEY} for all tables
698    * @param regionName
699    *          regionName
700    * @return the prefix for this table/region combination.
701    */
702   static String generateRegionMetricsPrefix(String tableName, String regionName) {
703     tableName = getEffectiveTableName(tableName);
704     String schemaMetricPrefix = tableName.equals(TOTAL_KEY) ? "" : TABLE_PREFIX + tableName + ".";
705     schemaMetricPrefix += regionName.equals(TOTAL_KEY) ? "" : REGION_PREFIX + regionName + ".";
706 
707     return schemaMetricPrefix;
708   }
709   
710   /**
711    * Sets the flag of whether to use table name in metric names. This flag
712    * is specified in configuration and is not expected to change at runtime,
713    * so we log an error message when it does change.
714    */
715   private static void setUseTableName(final boolean useTableNameNew) {
716     if (useTableNameGlobally == null) {
717       // This configuration option has not yet been set.
718       useTableNameGlobally = useTableNameNew;
719     } else if (useTableNameGlobally != useTableNameNew
720         && !loggedConfInconsistency) {
721       // The configuration is inconsistent and we have not reported it
722       // previously. Once we report it, just keep ignoring the new setting.
723       LOG.error("Inconsistent configuration. Previous configuration "
724           + "for using table name in metrics: " + useTableNameGlobally + ", "
725           + "new configuration: " + useTableNameNew);
726       loggedConfInconsistency = true;
727     }
728   }
729 
730   // Methods used in testing
731 
732   private static final String regexEscape(String s) {
733     return s.replace(".", "\\.");
734   }
735 
736   /**
737    * Assume that table names used in tests don't contain dots, except for the
738    * META table.
739    */
740   private static final String WORD_AND_DOT_RE_STR = "([^.]+|" +
741       regexEscape(Bytes.toString(HConstants.META_TABLE_NAME)) +
742       ")\\.";
743 
744   /** "tbl.<table_name>." */
745   private static final String TABLE_NAME_RE_STR =
746       "\\b" + regexEscape(TABLE_PREFIX) + WORD_AND_DOT_RE_STR;
747 
748   /** "cf.<cf_name>." */
749   private static final String CF_NAME_RE_STR =
750       "\\b" + regexEscape(CF_PREFIX) + WORD_AND_DOT_RE_STR;
751   private static final Pattern CF_NAME_RE = Pattern.compile(CF_NAME_RE_STR);
752 
753   /** "tbl.<table_name>.cf.<cf_name>." */
754   private static final Pattern TABLE_AND_CF_NAME_RE = Pattern.compile(
755       TABLE_NAME_RE_STR + CF_NAME_RE_STR);
756 
757   private static final Pattern BLOCK_CATEGORY_RE = Pattern.compile(
758       "\\b" + regexEscape(BLOCK_TYPE_PREFIX) + "[^.]+\\." +
759       // Also remove the special-case block type marker for meta blocks
760       "|" + META_BLOCK_CATEGORY_STR + "(?=" +
761       BlockMetricType.BLOCK_METRIC_TYPE_RE + ")");
762 
763   /**
764    * A suffix for the "number of operations" part of "time-varying metrics". We
765    * only use this for metric verification in unit testing. Time-varying
766    * metrics are handled by a different code path in production.
767    */
768   private static String NUM_OPS_SUFFIX = "numops";
769 
770   /**
771    * A custom suffix that we use for verifying the second component of
772    * a "time-varying metric".
773    */
774   private static String TOTAL_SUFFIX = "_total";
775   private static final Pattern TIME_VARYING_SUFFIX_RE = Pattern.compile(
776       "(" + NUM_OPS_SUFFIX + "|" + TOTAL_SUFFIX + ")$");
777 
778   void printMetricNames() {
779     for (BlockCategory blockCategory : BlockCategory.values()) {
780       for (boolean isCompaction : BOOL_VALUES) {
781         for (BlockMetricType metricType : BlockMetricType.values()) {
782           int i = getBlockMetricIndex(blockCategory, isCompaction, metricType);
783           LOG.debug("blockCategory=" + blockCategory + ", "
784               + "metricType=" + metricType + ", isCompaction=" + isCompaction
785               + ", metricName=" + blockMetricNames[i]);
786         }
787       }
788     }
789   }
790 
791   private Collection<String> getAllMetricNames() {
792     List<String> allMetricNames = new ArrayList<String>();
793     for (int i = 0; i < blockMetricNames.length; ++i) {
794       final String blockMetricName = blockMetricNames[i];
795       final boolean timeVarying = blockMetricTimeVarying[i];
796       if (blockMetricName != null) {
797         if (timeVarying) {
798           allMetricNames.add(blockMetricName + NUM_OPS_SUFFIX);
799           allMetricNames.add(blockMetricName + TOTAL_SUFFIX);
800         } else {
801           allMetricNames.add(blockMetricName);
802         }
803       }
804     }
805     allMetricNames.addAll(Arrays.asList(bloomMetricNames));
806     return allMetricNames;
807   }
808 
809   private static final boolean isTimeVaryingKey(String metricKey) {
810     return metricKey.endsWith(NUM_OPS_SUFFIX)
811         || metricKey.endsWith(TOTAL_SUFFIX);
812   }
813 
814   private static final String stripTimeVaryingSuffix(String metricKey) {
815     return TIME_VARYING_SUFFIX_RE.matcher(metricKey).replaceAll("");
816   }
817 
818   public static Map<String, Long> getMetricsSnapshot() {
819     Map<String, Long> metricsSnapshot = new TreeMap<String, Long>();
820     for (SchemaMetrics cfm : tableAndFamilyToMetrics.values()) {
821       cfm.flushMetrics();
822       for (String metricName : cfm.getAllMetricNames()) {
823         long metricValue;
824         if (isTimeVaryingKey(metricName)) {
825           Pair<Long, Integer> totalAndCount =
826               RegionMetricsStorage.getTimeVaryingMetric(stripTimeVaryingSuffix(metricName));
827           metricValue = metricName.endsWith(TOTAL_SUFFIX) ?
828               totalAndCount.getFirst() : totalAndCount.getSecond();
829         } else {
830           metricValue = RegionMetricsStorage.getNumericMetric(metricName);
831         }
832 
833         metricsSnapshot.put(metricName, metricValue);
834       }
835     }
836     return metricsSnapshot;
837   }
838 
839   public static long getLong(Map<String, Long> m, String k) {
840     Long l = m.get(k);
841     return l != null ? l : 0;
842   }
843 
844   private static void putLong(Map<String, Long> m, String k, long v) {
845     if (v != 0) {
846       m.put(k, v);
847     } else {
848       m.remove(k);
849     }
850   }
851 
852   /**
853    * @return the difference between two sets of metrics (second minus first).
854    *         Only includes keys that have nonzero difference.
855    */
856   public static Map<String, Long> diffMetrics(Map<String, Long> a,
857       Map<String, Long> b) {
858     Set<String> allKeys = new TreeSet<String>(a.keySet());
859     allKeys.addAll(b.keySet());
860     Map<String, Long> diff = new TreeMap<String, Long>();
861     for (String k : allKeys) {
862       long aVal = getLong(a, k);
863       long bVal = getLong(b, k);
864       if (aVal != bVal) {
865         diff.put(k, bVal - aVal);
866       }
867     }
868     return diff;
869   }
870 
871   public static void validateMetricChanges(Map<String, Long> oldMetrics) {
872     final Map<String, Long> newMetrics = getMetricsSnapshot();
873     final Map<String, Long> allCfDeltas = new TreeMap<String, Long>();
874     final Map<String, Long> allBlockCategoryDeltas =
875         new TreeMap<String, Long>();
876     final Map<String, Long> deltas = diffMetrics(oldMetrics, newMetrics);
877     final Pattern cfTableMetricRE =
878         useTableNameGlobally ? TABLE_AND_CF_NAME_RE : CF_NAME_RE;
879     final Set<String> allKeys = new TreeSet<String>(oldMetrics.keySet());
880     allKeys.addAll(newMetrics.keySet());
881 
882     for (SchemaMetrics cfm : tableAndFamilyToMetrics.values()) {
883       for (String metricName : cfm.getAllMetricNames()) {
884         if (metricName.startsWith(CF_PREFIX + CF_PREFIX)) {
885           throw new AssertionError("Column family prefix used twice: " +
886               metricName);
887         }
888 
889         final long oldValue = getLong(oldMetrics, metricName);
890         final long newValue = getLong(newMetrics, metricName);
891         final long delta = newValue - oldValue;
892 
893         // Re-calculate values of metrics with no column family (or CF/table)
894         // specified based on all metrics with CF (or CF/table) specified.
895         if (delta != 0) {
896           if (cfm != ALL_SCHEMA_METRICS) {
897             final String aggregateMetricName =
898                 cfTableMetricRE.matcher(metricName).replaceAll("");
899             if (!aggregateMetricName.equals(metricName)) {
900               LOG.debug("Counting " + delta + " units of " + metricName
901                   + " towards " + aggregateMetricName);
902 
903               putLong(allCfDeltas, aggregateMetricName,
904                   getLong(allCfDeltas, aggregateMetricName) + delta);
905             }
906           } else {
907             LOG.debug("Metric=" + metricName + ", delta=" + delta);
908           }
909         }
910 
911         Matcher matcher = BLOCK_CATEGORY_RE.matcher(metricName);
912         if (matcher.find()) {
913            // Only process per-block-category metrics
914           String metricNoBlockCategory = matcher.replaceAll("");
915 
916           putLong(allBlockCategoryDeltas, metricNoBlockCategory,
917               getLong(allBlockCategoryDeltas, metricNoBlockCategory) + delta);
918         }
919       }
920     }
921 
922     StringBuilder errors = new StringBuilder();
923     for (String key : ALL_SCHEMA_METRICS.getAllMetricNames()) {
924       long actual = getLong(deltas, key);
925       long expected = getLong(allCfDeltas, key);
926       if (actual != expected) {
927         if (errors.length() > 0)
928           errors.append("\n");
929         errors.append("The all-CF metric " + key + " changed by "
930             + actual + " but the aggregation of per-CF/table metrics "
931             + "yields " + expected);
932       }
933     }
934 
935     // Verify metrics computed for all block types based on the aggregation
936     // of per-block-type metrics.
937     for (String key : allKeys) {
938       if (BLOCK_CATEGORY_RE.matcher(key).find() ||
939           key.contains(ALL_SCHEMA_METRICS.getBloomMetricName(false)) ||
940           key.contains(ALL_SCHEMA_METRICS.getBloomMetricName(true))){
941         // Skip per-block-category metrics. Also skip bloom filters, because
942         // they are not aggregated per block type.
943         continue;
944       }
945       long actual = getLong(deltas, key);
946       long expected = getLong(allBlockCategoryDeltas, key);
947       if (actual != expected) {
948         if (errors.length() > 0)
949           errors.append("\n");
950         errors.append("The all-block-category metric " + key
951             + " changed by " + actual + " but the aggregation of "
952             + "per-block-category metrics yields " + expected);
953       }
954     }
955 
956     if (errors.length() > 0) {
957       throw new AssertionError(errors.toString());
958     }
959   }
960 
961   /**
962    * Creates an instance pretending both the table and column family are
963    * unknown. Used in unit tests.
964    */
965   public static SchemaMetrics getUnknownInstanceForTest() {
966     return getInstance(UNKNOWN, UNKNOWN);
967   }
968 
969   /**
970    * Set the flag to use or not use table name in metric names. Used in unit
971    * tests, so the flag can be set arbitrarily.
972    */
973   public static void setUseTableNameInTest(final boolean useTableNameNew) {
974     useTableNameGlobally = useTableNameNew;
975   }
976 
977   /** Formats the given map of metrics in a human-readable way. */
978   public static String formatMetrics(Map<String, Long> metrics) {
979     StringBuilder sb = new StringBuilder();
980     for (Map.Entry<String, Long> entry : metrics.entrySet()) {
981       if (sb.length() > 0) {
982         sb.append('\n');
983       }
984       sb.append(entry.getKey() + " : " + entry.getValue());
985     }
986     return sb.toString();
987   }
988 
989 }