Skip to content

honeyhive.adapters.copilot_studio

Convert Microsoft Copilot Studio diagnostic export records to OpenTelemetry spans.

Copilot Studio agents emit telemetry to Azure Application Insights. Customers route that telemetry through Azure Monitor diagnostic settings, which export records to an Event Hub or to Blob storage. This module converts those exported records into :class:ReadableSpan objects that can be handed directly to an OTLP exporter and shipped to HoneyHive.

Azure Monitor diagnostic export wire format (confirmed from live records): every record is a flat object — all Log Analytics columns (Name, Id, OperationId, UserId, etc.) appear at the top level, not nested inside a sub-object. Properties (capital P) is a dict of Copilot Studio-specific custom key/value pairs. The type discriminant is category on newer exports and Type on older ones; the adapter accepts both.

copilot_studio_records_to_spans

copilot_studio_records_to_spans(
    records: list[dict[str, object]],
) -> list[ReadableSpan]

Convert Application Insights diagnostic export records from a Copilot Studio agent to spans.

Accepts records in the Azure Monitor diagnostic export format — the records array from an Event Hub message already unpacked by the caller. Each record has a flat structure with all Log Analytics columns at the top level (Type or category, Name, Id, OperationId, etc.).

Only records with a type of AppEvents, AppRequests, or AppDependencies are converted. All other types (AppExceptions, AppTraces, etc.) are silently skipped — they carry log-signal data, not span data.

Each valid record is mapped to a single :class:ReadableSpan. See the module docstring for the full field mapping.

Records missing a required field (OperationId, Id, or time) are skipped with a warning.

Returns an empty list if no records survive filtering and validation.

Parameters:

Name Type Description Default
records list[dict[str, object]]

Diagnostic export records, each a JSON object parsed into a dict.

required

Returns:

Type Description
list[ReadableSpan]

A list of converted spans, one per valid record. May be empty.

Source code in src/honeyhive/adapters/copilot_studio.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def copilot_studio_records_to_spans(
    records: list[dict[str, object]],
) -> list[ReadableSpan]:
    """Convert Application Insights diagnostic export records from a Copilot Studio agent to spans.

    Accepts records in the Azure Monitor diagnostic export format — the ``records`` array from
    an Event Hub message already unpacked by the caller. Each record has a flat structure with
    all Log Analytics columns at the top level (``Type`` or ``category``, ``Name``, ``Id``,
    ``OperationId``, etc.).

    Only records with a type of ``AppEvents``, ``AppRequests``, or ``AppDependencies``
    are converted. All other types (``AppExceptions``, ``AppTraces``, etc.) are silently
    skipped — they carry log-signal data, not span data.

    Each valid record is mapped to a single :class:`ReadableSpan`. See the module docstring
    for the full field mapping.

    Records missing a required field (``OperationId``, ``Id``, or ``time``) are skipped with a
    warning.

    Returns an empty list if no records survive filtering and validation.

    Args:
        records: Diagnostic export records, each a JSON object parsed into a dict.

    Returns:
        A list of converted spans, one per valid record. May be empty.
    """
    resource_cache: dict[tuple[str, str], Resource] = {}
    spans: list[ReadableSpan] = []

    for record in records:
        record_type = _get_str(record, "category") or _get_str(record, "Type")
        if record_type not in _SPAN_TYPES:
            _LOG.debug("Skipping record type %r", record_type or "(missing)")
            continue
        name = _get_str(record, "Name")
        if name == "n/a":
            _LOG.debug("Skipping unnamed dependency record")
            continue
        keep_topics = (
            os.environ.get("COPILOT_STUDIO_ADAPTER_KEEP_TOPIC_SPANS", "").strip()
            in _TRUTHY
        )
        # Topic/root orchestration spans are noise; drop them unless opted in. Error topics
        # (e.g. "...topic.OnError") are exempt — they may carry error content not otherwise
        # captured, so we never silently discard them.
        is_topic_span = name in _TOPIC_SPAN_NAMES or ".topic." in name
        if not keep_topics and is_topic_span and "error" not in name.lower():
            _LOG.debug(
                "Skipping topic span %r (set COPILOT_STUDIO_ADAPTER_KEEP_TOPIC_SPANS=1 to retain)",
                name,
            )
            continue
        try:
            spans.append(_to_readable_span(record, resource_cache))
        except (ValueError, KeyError) as exc:
            _LOG.warning("Skipping malformed record: %s%.300s", exc, str(record))

    return spans