Automations
All questions related to Workflow Automation, AutomationEngine, and EdgeConnect, as well as integrations with various tools.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Propagating host tags to extension entities via Workflow — looking for feedback/suggestions

VMorrison101
Observer

Title: Propagating host tags to extension entities via Workflow — looking for feedback/suggestions


Hey all — we've put together a workflow to propagate patch-group tags from HOST entities down to their extension entities (WMI, SQL, IIS, HyperV, etc.) and wanted to share the approach and get some feedback before we scale it out.

The goal is to make sure extension entities are included in our maintenance windows during patching — without the tag on the extension entity, they keep alerting even when the host they run on is in a maintenance window. Propagating the tag automatically keeps everything in sync without manual tagging.

How it works

A JS action uses queryExecutionClient to query dt.system.data_objects to dynamically discover all extension entity types, then loops over each type running this DQL:

fetch `dt.entity.{entityType}`
| fieldsAdd runs_on, tags
| filter isNotNull(runs_on[`dt.entity.host`])
| fieldsAdd hostId = runs_on[`dt.entity.host`]
| lookup [fetch dt.entity.host | fieldsAdd tags], sourceField: hostId, lookupField: id
| expand tag = lookup.tags
| filter startsWith(tag, "patch-group:")
| fieldsAdd patchGroupValue = replaceString(tag, "patch-group:", "")
| fields entity.name, id, entity.type, hostId, tags, patchGroupValue

Entity types that don't have a runs_on field throw a FIELD_DOES_NOT_EXIST error and get skipped. The JS action validates existing tags and only passes entities that actually need updating to a follow-up HTTP action that calls POST /api/v2/tags.

A few things we're curious about

  1. Is dt.system.data_objects a stable/supported table to use as an entity type catalog, or should we be pulling this differently?
  2. Is there a cleaner way to pre-filter entity types that have a runs_on[dt.entity.host] relationship rather than querying each type and catching the error?
  3. Any recommended pattern for tag replacement (delete + add) in a workflow? The tags API only adds, so a changed host tag leaves the old value on the extension entity.

Happy to share more detail if useful — just looking for any gotchas or better approaches before we push this to production. Thanks!


Full JS action code

import { queryExecutionClient } from '@dynatrace-sdk/client-query';
import { execution } from '@dynatrace-sdk/automation-utils';

export default async function ({ execution_id }) {
  const ex = await execution(execution_id);

  // Configuration — update this value to retarget the workflow
  const SOURCE_TAG_PREFIX = 'patch-group';   // Tag prefix to look for on the host and write to extension entities

  // Step 1: Get all extension entity types from the Dynatrace data object catalog
  const typeResponse = await queryExecutionClient.queryExecute({
    body: {
      query: `fetch dt.system.data_objects
| filter contains(name, "dt.entity.") and contains(name, ":")
| fieldsAdd entityType = replaceString(name, "dt.entity.", "")
| fields name, entityType`,
      requestTimeoutMilliseconds: 60000,
      fetchTimeoutSeconds: 60
    }
  });

  const types = typeResponse.result.records ?? [];
  const output = [];
  const skipped = [];
  let totalFound = 0;
  let totalAlreadyTagged = 0;

  // Step 2: For each entity type, query extension entities that run on a host
  // and have a patch-group tag on the host. Types without a runs_on relationship
  // will throw a DQL error and be skipped gracefully.
  for (const t of types) {
    const entityType = t.entityType;

    try {
      const entityResponse = await queryExecutionClient.queryExecute({
        body: {
          query: `fetch \`dt.entity.${entityType}\`
| fieldsAdd runs_on, tags
| filter isNotNull(runs_on[\`dt.entity.host\`])
| fieldsAdd hostId = runs_on[\`dt.entity.host\`]
| lookup [fetch dt.entity.host | fieldsAdd tags], sourceField: hostId, lookupField: id
| expand tag = lookup.tags
| filter startsWith(tag, "${SOURCE_TAG_PREFIX}:")
| fieldsAdd patchGroupValue = replaceString(tag, "${SOURCE_TAG_PREFIX}:", "")
| fields entity.name, id, entity.type, hostId, tags, patchGroupValue`,
          requestTimeoutMilliseconds: 60000,
          fetchTimeoutSeconds: 60
        }
      });

      const records = entityResponse.result?.records ?? [];
      totalFound += records.length;

      // Step 3: Validate each entity — only add to output if tag is missing or incorrect
      for (const r of records) {
        const existingTags = Array.isArray(r.tags) ? r.tags : [];
        const expectedTag = `${SOURCE_TAG_PREFIX}:${r.patchGroupValue}`;
        const alreadyTagged = existingTags.some(t => t === expectedTag);

        if (alreadyTagged) {
          totalAlreadyTagged++;
        } else {
          output.push({
            entityId: r.id,
            entityName: r['entity.name'],
            entityType: r['entity.type'],
            tagKey: SOURCE_TAG_PREFIX,
            tagValue: r.patchGroupValue,
            entitySelector: `entityId("${r.id}")`
          });
        }
      }

    } catch (e) {
      // Entity type does not have a runs_on field — skip silently
      skipped.push(entityType);
    }
  }

  // Step 4: Detect and exclude entities where the host has duplicate patch-group tags
  const entityIdCounts = output.reduce((acc, r) => {
    acc[r.entityId] = (acc[r.entityId] || 0) + 1;
    return acc;
  }, {});

  const duplicateEntities = [...new Set(
    output.filter(r => entityIdCounts[r.entityId] > 1).map(r => r.entityId)
  )];

  const cleanOutput = output.filter(r => entityIdCounts[r.entityId] === 1);

  console.log(`Types processed: ${types.length}, Skipped (no runs_on field): ${skipped.length}`);
  console.log(`Entities found: ${totalFound}, Already tagged: ${totalAlreadyTagged}, Needs update: ${cleanOutput.length}`);

  if (duplicateEntities.length > 0) {
    console.log(`WARNING: ${duplicateEntities.length} entities skipped — host has duplicate patch-group tags`);
    duplicateEntities.forEach(id => console.log(`  Duplicate: ${id}`));
  }

  // Log each entity queued for tag write
  cleanOutput.forEach(r => console.log(`Queued: ${r.entityName} (${r.entityId}) [${r.entityType}] → ${r.tagKey}:${r.tagValue}`));

  return { total: cleanOutput.length, duplicates: duplicateEntities.length, records: cleanOutput };
}
0 REPLIES 0

Featured Posts