src/utils_vl_lookup.[ch]: Support selecting values by regex.
authorFlorian Forster <octo@collectd.org>
Mon, 19 Nov 2012 21:37:18 +0000 (22:37 +0100)
committerFlorian Forster <octo@collectd.org>
Mon, 19 Nov 2012 21:37:18 +0000 (22:37 +0100)
The name generated by the Aggregation plugin currently includes the
regular expression, which is suboptimal. We need to come up with something
clever there I guess.

src/aggregation.c
src/collectd.conf.pod
src/utils_vl_lookup.c
src/utils_vl_lookup.h
src/utils_vl_lookup_test.c

index db33c17..7ca26ca 100644 (file)
 
 #include <pthread.h>
 
+#define AGG_MATCHES_ALL(str) (strcmp ("/.*/", str) == 0)
+
 struct aggregation_s /* {{{ */
 {
   identifier_t ident;
+  unsigned int group_by;
 
   _Bool calc_num;
   _Bool calc_sum;
@@ -135,17 +138,17 @@ static agg_instance_t *agg_instance_create (data_set_t const *ds, /* {{{ */
 
   inst->ds_type = ds->ds[0].type;
 
-#define COPY_FIELD(fld) do { \
-  sstrncpy (inst->ident.fld, \
-      LU_IS_ANY (agg->ident.fld) ? vl->fld : agg->ident.fld, \
-      sizeof (inst->ident.fld)); \
+#define COPY_FIELD(field, group_mask) do { \
+  sstrncpy (inst->ident.field, \
+      (agg->group_by & group_mask) ? vl->field : agg->ident.field, \
+      sizeof (inst->ident.field)); \
 } while (0)
 
-  COPY_FIELD (host);
-  COPY_FIELD (plugin);
-  COPY_FIELD (plugin_instance);
-  COPY_FIELD (type);
-  COPY_FIELD (type_instance);
+  COPY_FIELD (host, LU_GROUP_BY_HOST);
+  COPY_FIELD (plugin, LU_GROUP_BY_PLUGIN);
+  COPY_FIELD (plugin_instance, LU_GROUP_BY_PLUGIN_INSTANCE);
+  COPY_FIELD (type, /* group_mask = */ 0);
+  COPY_FIELD (type_instance, LU_GROUP_BY_TYPE_INSTANCE);
 
 #undef COPY_FIELD
 
@@ -291,23 +294,23 @@ static int agg_instance_read (agg_instance_t *inst, cdtime_t t) /* {{{ */
   }
   meta_data_add_boolean (vl.meta, "aggregation:created", 1);
 
-  if (LU_IS_ALL (inst->ident.host))
+  if (AGG_MATCHES_ALL (inst->ident.host))
     sstrncpy (vl.host, "global", sizeof (vl.host));
   else
     sstrncpy (vl.host, inst->ident.host, sizeof (vl.host));
 
   sstrncpy (vl.plugin, "aggregation", sizeof (vl.plugin));
 
-  if (LU_IS_ALL (inst->ident.plugin))
+  if (AGG_MATCHES_ALL (inst->ident.plugin))
   {
-    if (LU_IS_ALL (inst->ident.plugin_instance))
+    if (AGG_MATCHES_ALL (inst->ident.plugin_instance))
       sstrncpy (pi_prefix, "", sizeof (pi_prefix));
     else
       sstrncpy (pi_prefix, inst->ident.plugin_instance, sizeof (pi_prefix));
   }
   else
   {
-    if (LU_IS_ALL (inst->ident.plugin_instance))
+    if (AGG_MATCHES_ALL (inst->ident.plugin_instance))
       sstrncpy (pi_prefix, inst->ident.plugin, sizeof (pi_prefix));
     else
       ssnprintf (pi_prefix, sizeof (pi_prefix),
@@ -316,7 +319,7 @@ static int agg_instance_read (agg_instance_t *inst, cdtime_t t) /* {{{ */
 
   sstrncpy (vl.type, inst->ident.type, sizeof (vl.type));
 
-  if (!LU_IS_ALL (inst->ident.type_instance))
+  if (!AGG_MATCHES_ALL (inst->ident.type_instance))
     sstrncpy (vl.type_instance, inst->ident.type_instance,
         sizeof (vl.type_instance));
 
@@ -424,14 +427,13 @@ static int agg_config_handle_group_by (oconfig_item_t const *ci, /* {{{ */
     value = ci->values[i].value.string;
 
     if (strcasecmp ("Host", value) == 0)
-      sstrncpy (agg->ident.host, LU_ANY, sizeof (agg->ident.host));
+      agg->group_by |= LU_GROUP_BY_HOST;
     else if (strcasecmp ("Plugin", value) == 0)
-      sstrncpy (agg->ident.plugin, LU_ANY, sizeof (agg->ident.plugin));
+      agg->group_by |= LU_GROUP_BY_PLUGIN;
     else if (strcasecmp ("PluginInstance", value) == 0)
-      sstrncpy (agg->ident.plugin_instance, LU_ANY,
-          sizeof (agg->ident.plugin_instance));
+      agg->group_by |= LU_GROUP_BY_PLUGIN_INSTANCE;
     else if (strcasecmp ("TypeInstance", value) == 0)
-      sstrncpy (agg->ident.type_instance, LU_ANY, sizeof (agg->ident.type_instance));
+      agg->group_by |= LU_GROUP_BY_TYPE_INSTANCE;
     else if (strcasecmp ("Type", value) == 0)
       ERROR ("aggregation plugin: Grouping by type is not supported.");
     else
@@ -457,12 +459,12 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */
   }
   memset (agg, 0, sizeof (*agg));
 
-  sstrncpy (agg->ident.host, LU_ALL, sizeof (agg->ident.host));
-  sstrncpy (agg->ident.plugin, LU_ALL, sizeof (agg->ident.plugin));
-  sstrncpy (agg->ident.plugin_instance, LU_ALL,
+  sstrncpy (agg->ident.host, "/.*/", sizeof (agg->ident.host));
+  sstrncpy (agg->ident.plugin, "/.*/", sizeof (agg->ident.plugin));
+  sstrncpy (agg->ident.plugin_instance, "/.*/",
       sizeof (agg->ident.plugin_instance));
-  sstrncpy (agg->ident.type, LU_ALL, sizeof (agg->ident.type));
-  sstrncpy (agg->ident.type_instance, LU_ALL,
+  sstrncpy (agg->ident.type, "/.*/", sizeof (agg->ident.type));
+  sstrncpy (agg->ident.type_instance, "/.*/",
       sizeof (agg->ident.type_instance));
 
   for (i = 0; i < ci->children_num; i++)
@@ -505,7 +507,7 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */
 
   /* Sanity checking */
   is_valid = 1;
-  if (LU_IS_ALL (agg->ident.type)) /* {{{ */
+  if (strcmp ("/.*/", agg->ident.type) == 0) /* {{{ */
   {
     ERROR ("aggregation plugin: It appears you did not specify the required "
         "\"Type\" option in this aggregation. "
@@ -518,15 +520,15 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */
   else if (strchr (agg->ident.type, '/') != NULL)
   {
     ERROR ("aggregation plugin: The \"Type\" may not contain the '/' "
-        "character. Especially, it may not be a wildcard. The current "
+        "character. Especially, it may not be a regex. The current "
         "value is \"%s\".", agg->ident.type);
     is_valid = 0;
   } /* }}} */
 
-  if (!LU_IS_ALL (agg->ident.host) /* {{{ */
-      && !LU_IS_ALL (agg->ident.plugin)
-      && !LU_IS_ALL (agg->ident.plugin_instance)
-      && !LU_IS_ALL (agg->ident.type_instance))
+  if (!AGG_MATCHES_ALL (agg->ident.host) /* {{{ */
+      && !AGG_MATCHES_ALL (agg->ident.plugin)
+      && !AGG_MATCHES_ALL (agg->ident.plugin_instance)
+      && !AGG_MATCHES_ALL (agg->ident.type_instance))
   {
     ERROR ("aggregation plugin: An aggregation must contain at least one "
         "wildcard. This is achieved by leaving at least one of the \"Host\", "
@@ -557,7 +559,7 @@ static int agg_config_aggregation (oconfig_item_t *ci) /* {{{ */
     return (-1);
   } /* }}} */
 
-  status = lookup_add (lookup, &agg->ident, agg);
+  status = lookup_add (lookup, &agg->ident, agg->group_by, agg);
   if (status != 0)
   {
     ERROR ("aggregation plugin: lookup_add failed with status %i.", status);
index aef9226..1c8b7a4 100644 (file)
@@ -297,6 +297,12 @@ aggregations. The following options are valid inside B<Aggregation> blocks:
 Selects the value lists to be added to this aggregation. B<Type> must be a
 valid data set name, see L<types.db(5)> for details.
 
+If the string starts with and ends with a slash (C</>), the string is
+interpreted as a I<regular expression>. The regex flavor used are POSIX
+extended regular expressions as described in L<regex(7)>. Example usage:
+
+ Host "/^db[0-9]\\.example\\.com$/"
+
 =item B<GroupBy> B<Host>|B<Plugin>|B<PluginInstance>|B<TypeInstance>
 
 Group valued by the specified field. The B<GroupBy> option may be repeated to
index 2dada24..722c452 100644 (file)
@@ -25,6 +25,9 @@
  **/
 
 #include "collectd.h"
+
+#include <regex.h>
+
 #include "common.h"
 #include "utils_vl_lookup.h"
 #include "utils_avltree.h"
 /*
  * Types
  */
+struct part_match_s
+{
+  char str[DATA_MAX_NAME_LEN];
+  regex_t regex;
+  _Bool is_regex;
+};
+typedef struct part_match_s part_match_t;
+
+struct identifier_match_s
+{
+  part_match_t host;
+  part_match_t plugin;
+  part_match_t plugin_instance;
+  part_match_t type;
+  part_match_t type_instance;
+
+  unsigned int group_by;
+};
+typedef struct identifier_match_s identifier_match_t;
+
 struct lookup_s
 {
   c_avl_tree_t *by_type_tree;
@@ -64,7 +87,7 @@ struct user_obj_s
 struct user_class_s
 {
   void *user_class;
-  identifier_t ident;
+  identifier_match_t match;
   user_obj_t *user_obj_list; /* list of user_obj */
 };
 typedef struct user_class_s user_class_t;
@@ -87,6 +110,87 @@ typedef struct by_type_entry_s by_type_entry_t;
 /*
  * Private functions
  */
+static _Bool lu_part_matches (part_match_t const *match, /* {{{ */
+    char const *str)
+{
+  if (match->is_regex)
+  {
+    /* Short cut popular catch-all regex. */
+    if (strcmp (".*", match->str) == 0)
+      return (1);
+
+    int status = regexec (&match->regex, str,
+        /* nmatch = */ 0, /* pmatch = */ NULL,
+        /* flags = */ 0);
+    if (status == 0)
+      return (1);
+    else
+      return (0);
+  }
+  else if (strcmp (match->str, str) == 0)
+    return (1);
+  else
+    return (0);
+} /* }}} _Bool lu_part_matches */
+
+static int lu_copy_ident_to_match_part (part_match_t *match_part, /* {{{ */
+    char const *ident_part)
+{
+  size_t len = strlen (ident_part);
+  int status;
+
+  if ((len < 3) || (ident_part[0] != '/') || (ident_part[len - 1] != '/'))
+  {
+    sstrncpy (match_part->str, ident_part, sizeof (match_part->str));
+    match_part->is_regex = 0;
+    return (0);
+  }
+
+  /* Copy string without the leading slash. */
+  sstrncpy (match_part->str, ident_part + 1, sizeof (match_part->str));
+  assert (sizeof (match_part->str) > len);
+  /* strip trailing slash */
+  match_part->str[len - 2] = 0;
+  
+  status = regcomp (&match_part->regex, match_part->str,
+      /* flags = */ REG_EXTENDED);
+  if (status != 0)
+  {
+    char errbuf[1024];
+    regerror (status, &match_part->regex, errbuf, sizeof (errbuf));
+    ERROR ("utils_vl_lookup: Compiling regular expression \"%s\" failed: %s",
+        match_part->str, errbuf);
+    return (EINVAL);
+  }
+  match_part->is_regex = 1;
+  
+  return (0);
+} /* }}} int lu_copy_ident_to_match_part */
+
+static int lu_copy_ident_to_match (identifier_match_t *match, /* {{{ */
+    identifier_t const *ident, unsigned int group_by)
+{
+  memset (match, 0, sizeof (*match));
+
+  match->group_by = group_by;
+
+#define COPY_FIELD(field) do { \
+  int status = lu_copy_ident_to_match_part (&match->field, ident->field); \
+  if (status != 0) \
+    return (status); \
+} while (0)
+
+  COPY_FIELD (host);
+  COPY_FIELD (plugin);
+  COPY_FIELD (plugin_instance);
+  COPY_FIELD (type);
+  COPY_FIELD (type_instance);
+
+#undef COPY_FIELD
+
+  return (0);
+} /* }}} int lu_copy_ident_to_match */
+
 static void *lu_create_user_obj (lookup_t *obj, /* {{{ */
     data_set_t const *ds, value_list_t const *vl,
     user_class_t *user_class)
@@ -110,21 +214,21 @@ static void *lu_create_user_obj (lookup_t *obj, /* {{{ */
     return (NULL);
   }
 
-  sstrncpy (user_obj->ident.host,
-    LU_IS_ALL (user_class->ident.host) ?  "/all/" : vl->host,
-    sizeof (user_obj->ident.host));
-  sstrncpy (user_obj->ident.plugin,
-    LU_IS_ALL (user_class->ident.plugin) ?  "/all/" : vl->plugin,
-    sizeof (user_obj->ident.plugin));
-  sstrncpy (user_obj->ident.plugin_instance,
-    LU_IS_ALL (user_class->ident.plugin_instance) ?  "/all/" : vl->plugin_instance,
-    sizeof (user_obj->ident.plugin_instance));
-  sstrncpy (user_obj->ident.type,
-    LU_IS_ALL (user_class->ident.type) ?  "/all/" : vl->type,
-    sizeof (user_obj->ident.type));
-  sstrncpy (user_obj->ident.type_instance,
-    LU_IS_ALL (user_class->ident.type_instance) ?  "/all/" : vl->type_instance,
-    sizeof (user_obj->ident.type_instance));
+#define COPY_FIELD(field, group_mask) do { \
+  if (user_class->match.field.is_regex \
+      && ((user_class->match.group_by & group_mask) == 0)) \
+    sstrncpy (user_obj->ident.field, "/.*/", sizeof (user_obj->ident.field)); \
+  else \
+    sstrncpy (user_obj->ident.field, vl->field, sizeof (user_obj->ident.field)); \
+} while (0)
+
+  COPY_FIELD (host, LU_GROUP_BY_HOST);
+  COPY_FIELD (plugin, LU_GROUP_BY_PLUGIN);
+  COPY_FIELD (plugin_instance, LU_GROUP_BY_PLUGIN_INSTANCE);
+  COPY_FIELD (type, 0);
+  COPY_FIELD (type_instance, LU_GROUP_BY_TYPE_INSTANCE);
+
+#undef COPY_FIELD
 
   if (user_class->user_obj_list == NULL)
   {
@@ -150,14 +254,21 @@ static user_obj_t *lu_find_user_obj (user_class_t *user_class, /* {{{ */
       ptr != NULL;
       ptr = ptr->next)
   {
-    if (!LU_IS_ALL (ptr->ident.host)
-        && (strcmp (ptr->ident.host, vl->host) != 0))
+    if (user_class->match.host.is_regex
+        && (user_class->match.group_by & LU_GROUP_BY_HOST)
+        && (strcmp (vl->host, ptr->ident.host) != 0))
+      continue;
+    if (user_class->match.plugin.is_regex
+        && (user_class->match.group_by & LU_GROUP_BY_PLUGIN)
+        && (strcmp (vl->plugin, ptr->ident.plugin) != 0))
       continue;
-    if (!LU_IS_ALL (ptr->ident.plugin_instance)
-        && (strcmp (ptr->ident.plugin_instance, vl->plugin_instance) != 0))
+    if (user_class->match.plugin_instance.is_regex
+        && (user_class->match.group_by & LU_GROUP_BY_PLUGIN_INSTANCE)
+        && (strcmp (vl->plugin_instance, ptr->ident.plugin_instance) != 0))
       continue;
-    if (!LU_IS_ALL (ptr->ident.type_instance)
-        && (strcmp (ptr->ident.type_instance, vl->type_instance) != 0))
+    if (user_class->match.type_instance.is_regex
+        && (user_class->match.group_by & LU_GROUP_BY_TYPE_INSTANCE)
+        && (strcmp (vl->type_instance, ptr->ident.type_instance) != 0))
       continue;
 
     return (ptr);
@@ -173,21 +284,14 @@ static int lu_handle_user_class (lookup_t *obj, /* {{{ */
   user_obj_t *user_obj;
   int status;
 
-  assert (strcmp (vl->type, user_class->ident.type) == 0);
-  assert (LU_IS_WILDCARD (user_class->ident.plugin)
-      || (strcmp (vl->plugin, user_class->ident.plugin) == 0));
+  assert (strcmp (vl->type, user_class->match.type.str) == 0);
+  assert (user_class->match.plugin.is_regex
+      || (strcmp (vl->plugin, user_class->match.plugin.str)) == 0);
 
-  /* When we get here, type and plugin already match the user class. Now check
-   * the rest of the fields. */
-  if (!LU_IS_WILDCARD (user_class->ident.type_instance)
-      && (strcmp (vl->type_instance, user_class->ident.type_instance) != 0))
-    return (1);
-  if (!LU_IS_WILDCARD (user_class->ident.plugin_instance)
-      && (strcmp (vl->plugin_instance,
-          user_class->ident.plugin_instance) != 0))
-    return (1);
-  if (!LU_IS_WILDCARD (user_class->ident.host)
-      && (strcmp (vl->host, user_class->ident.host) != 0))
+  if (!lu_part_matches (&user_class->match.type_instance, vl->type_instance)
+      || !lu_part_matches (&user_class->match.plugin_instance, vl->plugin_instance)
+      || !lu_part_matches (&user_class->match.plugin, vl->plugin)
+      || !lu_part_matches (&user_class->match.host, vl->host))
     return (1);
 
   user_obj = lu_find_user_obj (user_class, vl);
@@ -292,14 +396,15 @@ static by_type_entry_t *lu_search_by_type (lookup_t *obj, /* {{{ */
 } /* }}} by_type_entry_t *lu_search_by_type */
 
 static int lu_add_by_plugin (by_type_entry_t *by_type, /* {{{ */
-    identifier_t const *ident, user_class_list_t *user_class_list)
+    user_class_list_t *user_class_list)
 {
   user_class_list_t *ptr = NULL;
+  identifier_match_t const *match = &user_class_list->entry.match;
 
   /* Lookup user_class_list from the per-plugin structure. If this is the first
    * user_class to be added, the blocks return immediately. Otherwise they will
    * set "ptr" to non-NULL. */
-  if (LU_IS_WILDCARD (ident->plugin))
+  if (match->plugin.is_regex)
   {
     if (by_type->wildcard_plugin_list == NULL)
     {
@@ -314,11 +419,11 @@ static int lu_add_by_plugin (by_type_entry_t *by_type, /* {{{ */
     int status;
 
     status = c_avl_get (by_type->by_plugin_tree,
-        ident->plugin, (void *) &ptr);
+        match->plugin.str, (void *) &ptr);
 
     if (status != 0) /* plugin not yet in tree */
     {
-      char *plugin_copy = strdup (ident->plugin);
+      char *plugin_copy = strdup (match->plugin.str);
 
       if (plugin_copy == NULL)
       {
@@ -478,7 +583,7 @@ void lookup_destroy (lookup_t *obj) /* {{{ */
 } /* }}} void lookup_destroy */
 
 int lookup_add (lookup_t *obj, /* {{{ */
-    identifier_t const *ident, void *user_class)
+    identifier_t const *ident, unsigned int group_by, void *user_class)
 {
   by_type_entry_t *by_type = NULL;
   user_class_list_t *user_class_obj;
@@ -495,11 +600,11 @@ int lookup_add (lookup_t *obj, /* {{{ */
   }
   memset (user_class_obj, 0, sizeof (*user_class_obj));
   user_class_obj->entry.user_class = user_class;
-  memmove (&user_class_obj->entry.ident, ident, sizeof (*ident));
+  lu_copy_ident_to_match (&user_class_obj->entry.match, ident, group_by);
   user_class_obj->entry.user_obj_list = NULL;
   user_class_obj->next = NULL;
 
-  return (lu_add_by_plugin (by_type, ident, user_class_obj));
+  return (lu_add_by_plugin (by_type, user_class_obj));
 } /* }}} int lookup_add */
 
 /* returns the number of successful calls to the callback function */
index c006fc7..31787f5 100644 (file)
@@ -63,12 +63,11 @@ struct identifier_s
 };
 typedef struct identifier_s identifier_t;
 
-#define LU_ANY "/any/"
-#define LU_ALL "/all/"
-
-#define LU_IS_ANY(str) (strcmp (str, LU_ANY) == 0)
-#define LU_IS_ALL(str) (strcmp (str, LU_ALL) == 0)
-#define LU_IS_WILDCARD(str) (LU_IS_ANY(str) || LU_IS_ALL(str))
+#define LU_GROUP_BY_HOST            0x01
+#define LU_GROUP_BY_PLUGIN          0x02
+#define LU_GROUP_BY_PLUGIN_INSTANCE 0x04
+/* #define LU_GROUP_BY_TYPE            0x00 */
+#define LU_GROUP_BY_TYPE_INSTANCE   0x10
 
 /*
  * Functions
@@ -81,7 +80,7 @@ lookup_t *lookup_create (lookup_class_callback_t,
 void lookup_destroy (lookup_t *obj);
 
 int lookup_add (lookup_t *obj,
-    identifier_t const *ident, void *user_class);
+    identifier_t const *ident, unsigned int group_by, void *user_class);
 
 /* TODO(octo): Pass lookup_obj_callback_t to lookup_search()? */
 int lookup_search (lookup_t *obj,
index 6265b32..bbb3a67 100644 (file)
@@ -82,7 +82,8 @@ static void *lookup_class_callback (data_set_t const *ds,
 static void checked_lookup_add (lookup_t *obj, /* {{{ */
     char const *host,
     char const *plugin, char const *plugin_instance,
-    char const *type, char const *type_instance)
+    char const *type, char const *type_instance,
+    unsigned int group_by)
 {
   identifier_t ident;
   void *user_class;
@@ -98,7 +99,7 @@ static void checked_lookup_add (lookup_t *obj, /* {{{ */
   user_class = malloc (sizeof (ident));
   memmove (user_class, &ident, sizeof (ident));
 
-  status = lookup_add (obj, &ident, user_class);
+  status = lookup_add (obj, &ident, group_by, user_class);
   assert (status == 0);
 } /* }}} void test_add */
 
@@ -143,7 +144,7 @@ static void testcase0 (void)
 {
   lookup_t *obj = checked_lookup_create ();
 
-  checked_lookup_add (obj, "/any/", "test", "", "test", "/all/");
+  checked_lookup_add (obj, "/.*/", "test", "", "test", "/.*/", LU_GROUP_BY_HOST);
   checked_lookup_search (obj, "host0", "test", "", "test", "0",
       /* expect new = */ 1);
   checked_lookup_search (obj, "host0", "test", "", "test", "1",
@@ -160,7 +161,7 @@ static void testcase1 (void)
 {
   lookup_t *obj = checked_lookup_create ();
 
-  checked_lookup_add (obj, "/any/", "/all/", "/all/", "test", "/all/");
+  checked_lookup_add (obj, "/.*/", "/.*/", "/.*/", "test", "/.*/", LU_GROUP_BY_HOST);
   checked_lookup_search (obj, "host0", "plugin0", "", "test", "0",
       /* expect new = */ 1);
   checked_lookup_search (obj, "host0", "plugin0", "", "test", "1",
@@ -186,8 +187,8 @@ static void testcase2 (void)
   lookup_t *obj = checked_lookup_create ();
   int status;
 
-  checked_lookup_add (obj, "/any/", "plugin0", "", "test", "/all/");
-  checked_lookup_add (obj, "/any/", "/all/", "", "test", "ti0");
+  checked_lookup_add (obj, "/.*/", "plugin0", "", "test", "/.*/", LU_GROUP_BY_HOST);
+  checked_lookup_add (obj, "/.*/", "/.*/", "", "test", "ti0", LU_GROUP_BY_HOST);
 
   status = checked_lookup_search (obj, "host0", "plugin1", "", "test", "",
       /* expect new = */ 0);
@@ -205,10 +206,39 @@ static void testcase2 (void)
   lookup_destroy (obj);
 }
 
+static void testcase3 (void)
+{
+  lookup_t *obj = checked_lookup_create ();
+
+  checked_lookup_add (obj, "/^db[0-9]\\./", "cpu", "/.*/", "cpu", "/.*/",
+      LU_GROUP_BY_TYPE_INSTANCE);
+  checked_lookup_search (obj, "db0.example.com", "cpu", "0", "cpu", "user",
+      /* expect new = */ 1);
+  checked_lookup_search (obj, "db0.example.com", "cpu", "0", "cpu", "idle",
+      /* expect new = */ 1);
+  checked_lookup_search (obj, "db0.example.com", "cpu", "1", "cpu", "user",
+      /* expect new = */ 0);
+  checked_lookup_search (obj, "db0.example.com", "cpu", "1", "cpu", "idle",
+      /* expect new = */ 0);
+  checked_lookup_search (obj, "app0.example.com", "cpu", "0", "cpu", "user",
+      /* expect new = */ 0);
+  checked_lookup_search (obj, "app0.example.com", "cpu", "0", "cpu", "idle",
+      /* expect new = */ 0);
+  checked_lookup_search (obj, "db1.example.com", "cpu", "0", "cpu", "user",
+      /* expect new = */ 0);
+  checked_lookup_search (obj, "db1.example.com", "cpu", "0", "cpu", "idle",
+      /* expect new = */ 0);
+  checked_lookup_search (obj, "db1.example.com", "cpu", "0", "cpu", "system",
+      /* expect new = */ 1);
+
+  lookup_destroy (obj);
+}
+
 int main (int argc, char **argv) /* {{{ */
 {
   testcase0 ();
   testcase1 ();
   testcase2 ();
+  testcase3 ();
   return (EXIT_SUCCESS);
 } /* }}} int main */