{"slug": "meta-muse-code-monitoring-with-opentelemetry-and-signoz", "title": "Meta Muse Code Monitoring with OpenTelemetry and SigNoz", "summary": "SigNoz published a guide for exporting telemetry from Meta Muse Code, Meta's terminal coding agent powered by the Muse Spark model family, to SigNoz using OpenTelemetry. Because Muse Code's built-in OpenTelemetry exporter cannot send the signoz-ingestion-key header required by SigNoz Cloud, the guide uses Muse Code's hook system and a Python 3.9+ script at ~/.local/share/muse-otel/muse_otel_hook.py to convert lifecycle events into OpenTelemetry spans posted to SigNoz over OTLP/HTTP JSON. The setup lets operators observe sessions, turns, model calls, token and prompt cache usage, and tool activity, since cost is driven by context replay and latency by hidden reasoning.", "body_md": "## What is Meta Muse Code Monitoring?\n\n[Meta Muse Code](https://www.meta.ai/) is Meta's terminal coding agent, powered by the Muse Spark model family. It runs from your shell, reads and edits files, runs commands, and spawns subagents, which means a single prompt can turn into a dozen model calls and as many tool executions before you see a reply.\n\nThat is exactly what makes it hard to reason about without telemetry. Cost is driven by context replay rather than by how much anyone typed, latency is dominated by reasoning the user never sees, and a failing tool shows up as an agent that quietly takes longer.\n\nThis guide walks you through exporting Muse Code telemetry to SigNoz using [OpenTelemetry](https://opentelemetry.io/), so you can observe sessions, turns, model calls, token and prompt cache usage, and tool activity.\n\n## Prerequisites\n\n- A [SigNoz Cloud account](https://signoz.io/teams/) with an active ingestion key, or a[self-hosted SigNoz instance](https://signoz.io/docs/install/self-host/)\n- Muse Code installed and signed in. Run `muse --version` to confirm\n- Python 3.9 or newer, already present on macOS and most Linux distributions\n\n## Monitor Meta Muse Code with OpenTelemetry\n\nMuse Code ships its own OpenTelemetry exporter, but it cannot send the `signoz-ingestion-key` header that SigNoz Cloud requires, so this guide uses the hook system instead. Muse Code runs a hook command at each point in its lifecycle and passes the event to that command as JSON on stdin. The script below turns those events into OpenTelemetry spans and posts them to SigNoz over OTLP/HTTP. No SDK is required, because SigNoz accepts OTLP/HTTP JSON directly.\n\n**Step 1:** Create the hook script at `~/.local/share/muse-otel/muse_otel_hook.py`.\n\n``` bash\n#!/usr/bin/env python3\n\"\"\"Export Meta Muse Code hook events to SigNoz as OpenTelemetry spans.\"\"\"\nimport hashlib, json, os, pathlib, sys, time, urllib.request\n \nSTATE = pathlib.Path(os.path.expanduser(\"~/.local/state/muse-otel\"))\nCFG_PATHS = [pathlib.Path(__file__).resolve().parent / \"config.json\",\n             pathlib.Path(os.path.expanduser(\"~/.config/muse-otel/config.json\"))]\n \n \ndef config():\n    for p in CFG_PATHS:\n        try:\n            return json.loads(p.read_text())\n        except Exception:\n            continue\n    return {}\n \n \nCFG = config()\nENDPOINT = CFG.get(\"endpoint\", \"https://ingest.<region>.signoz.cloud:443\")\nKEY = CFG.get(\"ingestion_key\")\nSERVICE = CFG.get(\"service_name\", \"muse-code\")\n \n \ndef h16(*parts):\n    return hashlib.sha256(\"|\".join(str(p) for p in parts).encode()).hexdigest()[:16]\n \n \ndef attrs(d):\n    out = []\n    for k, v in d.items():\n        if v is None:\n            continue\n        if isinstance(v, bool):\n            val = {\"boolValue\": v}\n        elif isinstance(v, int):\n            val = {\"intValue\": str(v)}\n        elif isinstance(v, (list, tuple)):\n            val = {\"arrayValue\": {\"values\": [{\"stringValue\": str(x)} for x in v]}}\n        else:\n            val = {\"stringValue\": str(v)}\n        out.append({\"key\": k, \"value\": val})\n    return out\n \n \ndef post(spans, ev):\n    if not (KEY and spans):\n        return\n    payload = {\"resourceSpans\": [{\n        \"resource\": {\"attributes\": attrs({\"service.name\": SERVICE, \"surface\": \"tui\"})},\n        \"scopeSpans\": [{\"scope\": {\"name\": \"muse-otel-hook\"}, \"spans\": spans}]}]}\n    if os.fork() != 0:            # return immediately, never block the agent\n        return\n    os.setsid()\n    if os.fork() != 0:\n        os._exit(0)\n    try:\n        req = urllib.request.Request(\n            ENDPOINT.rstrip(\"/\") + \"/v1/traces\", data=json.dumps(payload).encode(),\n            headers={\"content-type\": \"application/json\", \"signoz-ingestion-key\": KEY})\n        urllib.request.urlopen(req, timeout=10).read()\n    except Exception:\n        pass\n    os._exit(0)\n \n \ndef mark(sess, key):\n    d = STATE / str(sess)\n    d.mkdir(parents=True, exist_ok=True)\n    (d / key).write_text(json.dumps({\"t\": time.time_ns()}))\n \n \ndef take(sess, key):\n    p = STATE / str(sess) / key\n    try:\n        v = json.loads(p.read_text())\n        p.unlink(missing_ok=True)\n        return v\n    except Exception:\n        return None\n \n \ndef span(name, tid, sid, parent, start, end, a, kind=1, err=False):\n    s = {\"traceId\": tid, \"spanId\": sid, \"name\": name, \"kind\": kind,\n         \"startTimeUnixNano\": str(int(start)), \"endTimeUnixNano\": str(int(end)),\n         \"attributes\": attrs(a), \"status\": {\"code\": 2 if err else 1}}\n    if parent:\n        s[\"parentSpanId\"] = parent\n    return s\n \n \ndef main():\n    ev = json.loads(sys.stdin.read())\n    name, sess, turn = ev.get(\"hook_event_name\"), ev.get(\"session_id\"), ev.get(\"turn_id\")\n    tid = (str(turn).replace(\"-\", \"\") if turn else hashlib.sha256(\n        str(sess).encode()).hexdigest()[:32])\n    root, now = h16(\"turn\", turn or sess), time.time_ns()\n    tkey = \"turn_\" + h16(turn or sess)\n    base = {\"session.id\": sess, \"turn.id\": turn, \"muse.model\": ev.get(\"model\")}\n    have_root = (STATE / str(sess) / tkey).exists()\n \n    if name == \"UserPromptSubmit\":\n        mark(sess, tkey)\n    elif name == \"PreLLMCall\":\n        mark(sess, \"llm_\" + h16(ev.get(\"request_id\"), ev.get(\"attempt\")))\n    elif name == \"PreToolUse\":\n        mark(sess, \"tool_\" + h16(ev.get(\"tool_use_id\")))\n \n    elif name == \"PostLLMCall\":\n        st = take(sess, \"llm_\" + h16(ev.get(\"request_id\"), ev.get(\"attempt\")))\n        u, status = ev.get(\"usage\") or {}, str(ev.get(\"status\") or \"\")\n        sid = h16(\"llm\", ev.get(\"request_id\"))\n        tp = (ev.get(\"options\") or {}).get(\"meta.traceparent\")\n        if isinstance(tp, str) and tp.count(\"-\") == 3:      # reuse Muse's own ids\n            _, tp_trace, tp_span, _ = tp.split(\"-\")\n            tid, sid = tp_trace, tp_span\n        fr = [ev[\"finish_reason\"]] if ev.get(\"finish_reason\") else (\n            [\"stop\"] if status == \"success\" else None)\n        post([span(\"chat \" + str(ev.get(\"model\")), tid, sid,\n                   root if have_root else None, (st or {}).get(\"t\", now - 1), now,\n                   {**base, \"gen_ai.operation.name\": \"chat\",\n                    \"gen_ai.request.model\": ev.get(\"model\"),\n                    \"gen_ai.provider.name\": ev.get(\"model_provider\"),\n                    \"gen_ai.response.id\": ev.get(\"response_id\"),\n                    \"gen_ai.response.finish_reasons\": fr,\n                    \"gen_ai.usage.input_tokens\": u.get(\"input_tokens\"),\n                    \"gen_ai.usage.output_tokens\": u.get(\"output_tokens\"),\n                    \"gen_ai.usage.cache_read.input_tokens\": u.get(\"cache_read_tokens\"),\n                    \"gen_ai.usage.reasoning.output_tokens\": u.get(\"reasoning_tokens\"),\n                    \"gen_ai.request.reasoning_effort\":\n                        (ev.get(\"options\") or {}).get(\"meta.reasoning.effort\"),\n                    \"muse.llm.status\": status, \"muse.llm.attempt\": ev.get(\"attempt\")},\n                   kind=3, err=bool(ev.get(\"error\")))], ev)\n \n    elif name in (\"PostToolUse\", \"PostToolUseFailure\"):\n        st = take(sess, \"tool_\" + h16(ev.get(\"tool_use_id\")))\n        failed = name == \"PostToolUseFailure\"\n        post([span(\"execute_tool \" + str(ev.get(\"tool_name\")), tid,\n                   h16(\"tool\", ev.get(\"tool_use_id\")), root if have_root else None,\n                   (st or {}).get(\"t\", now - 1), now,\n                   {**base, \"gen_ai.tool.name\": ev.get(\"tool_name\"),\n                    \"gen_ai.tool.call.id\": ev.get(\"tool_use_id\"),\n                    \"muse.tool.status\": \"failed\" if failed else \"success\"},\n                   err=failed)], ev)\n \n    elif name in (\"Stop\", \"StopFailure\"):\n        st = take(sess, tkey)\n        post([span(\"turn\", tid, root, None, (st or {}).get(\"t\", now - 1), now,\n                   {**base, \"muse.turn.outcome\":\n                       \"failed\" if name == \"StopFailure\" else \"completed\"},\n                   err=name == \"StopFailure\")], ev)\n \n    elif name == \"SessionStart\":\n        mark(sess, \"session\")\n    elif name == \"SessionEnd\":\n        st = take(sess, \"session\")\n        post([span(\"muse session\", hashlib.sha256(\n            (\"session:\" + str(sess)).encode()).hexdigest()[:32], h16(\"sess\", sess),\n            None, (st or {}).get(\"t\", now - 1), now,\n            {**base, \"muse.session.end_reason\": ev.get(\"reason\")})], ev)\n \n    print(\"{}\")\n \n \nif __name__ == \"__main__\":\n    try:\n        main()\n    except Exception:\n        print(\"{}\")          # a telemetry fault must never block the agent\n    sys.exit(0)\n```\n\nMake it executable:\n\n```\nchmod +x ~/.local/share/muse-otel/muse_otel_hook.py\n```\n\n**Step 2:** Create `~/.local/share/muse-otel/config.json` next to the script.\n\n```\n{\n  \"endpoint\": \"https://ingest.<region>.signoz.cloud:443\",\n  \"ingestion_key\": \"<your-ingestion-key>\",\n  \"service_name\": \"muse-code\"\n}\nchmod 600 ~/.local/share/muse-otel/config.json\n```\n\n**Verify these values:**\n\n- `<region>` : Your[SigNoz Cloud region](https://signoz.io/docs/ingestion/signoz-cloud/overview/#endpoint) .\n- `<your-ingestion-key>` : Your SigNoz[ingestion key](https://signoz.io/docs/ingestion/signoz-cloud/keys/) .\n- `service_name` : What the agent appears as in SigNoz. Set a different value per team or per repository if you want to compare them.\n\n**Step 3:** Register the hooks in `~/.config/muse/settings.json`.\n\n```\n{\n  \"schema_version\": 1,\n  \"hooks\": {\n    \"SessionStart\":       [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"UserPromptSubmit\":   [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"PreLLMCall\":         [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"PostLLMCall\":        [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"PreToolUse\":         [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"PostToolUse\":        [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"PostToolUseFailure\": [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"Stop\":               [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }],\n    \"SessionEnd\":         [{ \"hooks\": [{ \"type\": \"command\", \"command\": \"~/.local/share/muse-otel/muse_otel_hook.py\" }] }]\n  }\n}\n```\n\nHooks belong in `settings.json`. A project-level `.muse/hooks.json` is silently ignored.\n\n**Step 4:** Start Muse Code and run a prompt.\n\n```\nmuse\n```\n\nEach turn emits a `turn` span with `chat {model}` and `execute_tool {tool}` children. Because the script reuses the `meta.traceparent` that Muse Code already attaches to every model call, the trace id matches the agent's own turn id and your spans line up with its internal trace context. Allow a few seconds for the data to appear.\n\n## View Meta Muse Code Traces in SigNoz\n\nOnce configured, Muse Code emits traces on every turn. In SigNoz, look for the service name you set in `config.json`.\n\nMuse Code traces are available in SigNoz under the Traces tab:\n\nClicking a trace opens the waterfall for one turn, with the model calls and tool executions that made it up, plus the `gen_ai.*` and `muse.*` attributes on each span.\n\n## Meta Muse Code Monitoring Dashboard\n\nYou can also check out our custom [Meta Muse Code dashboard](https://signoz.io/docs/dashboards/dashboard-templates/muse-code-dashboard/) which provides specialized visualizations for monitoring your Muse Code usage. The dashboard includes pre-built charts for token usage, prompt cache efficiency, latency, and tool activity, along with import instructions to get started quickly.\n\n## ## Troubleshooting Meta Muse Code Monitoring\n\n### No spans appear in SigNoz\n\nConfirm the hooks are actually firing. Muse Code records every hook run in its own diagnostic log:\n\n```\ngrep hook.execution.terminal ~/.local/share/muse/local-tracing/bootstrap/*.log | tail\n```\n\nA `status=\"completed\"` line for each event means the hook ran. If there are no lines at all, re-check the `hooks` block in `~/.config/muse/settings.json`.\n\n### Hooks run but nothing reaches SigNoz\n\nThis is almost always the configuration file. The hook cannot read environment variables, so verify the file exists next to the script and parses:\n\n``` python\npython3 -c \"import json;print(json.load(open('$HOME/.local/share/muse-otel/config.json'))['service_name'])\"\n```\n\n### Muse Code reports a malformed settings file\n\nMuse Code validates `settings.json` on startup and names the offending line. `schema_version` must be `1`, and every matcher group must declare a `hooks` array.\n\n### Spans arrive but the agent feels slower\n\nThe script forks before sending, so the hook returns immediately. If you removed the fork, each event would block the agent for the duration of an HTTPS round trip.\n\n## ## Setup OpenTelemetry Collector (Optional)\n\n### What is the OpenTelemetry Collector?\n\nThe [OpenTelemetry Collector](https://signoz.io/docs/opentelemetry-collection-agents/get-started/) is a vendor-neutral service that receives, processes, and exports telemetry. It is optional here, because the hook sends directly to SigNoz.\n\n### Why use it?\n\nRun one if you want to enrich Muse Code spans with host or environment attributes, batch across many developer machines, or route the same telemetry to more than one backend. Point `endpoint` in `config.json` at the collector and configure an OTLP exporter to SigNoz.\n\n## Related integrations\n\n- [Meta Muse Spark Monitoring](https://signoz.io/docs/muse-spark-monitoring/) for the Model API behind Muse Code\n- [Setting up alerts](https://signoz.io/docs/alerts-management/trace-based-alerts/) on agent latency and tool failures\n- [Querying traces](https://signoz.io/docs/userguide/query-builder-v5/) to slice usage by model, tool, or session", "url": "https://wpnews.pro/news/meta-muse-code-monitoring-with-opentelemetry-and-signoz", "canonical_source": "https://signoz.io/docs/muse-code-monitoring", "published_at": "2026-09-14 00:00:00+00:00", "updated_at": "2026-09-16 06:06:59.376711+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops"], "entities": ["Meta", "Meta Muse Code", "Muse Spark", "SigNoz", "OpenTelemetry", "SigNoz Cloud"], "alternates": {"html": "https://wpnews.pro/news/meta-muse-code-monitoring-with-opentelemetry-and-signoz", "markdown": "https://wpnews.pro/news/meta-muse-code-monitoring-with-opentelemetry-and-signoz.md", "text": "https://wpnews.pro/news/meta-muse-code-monitoring-with-opentelemetry-and-signoz.txt", "jsonld": "https://wpnews.pro/news/meta-muse-code-monitoring-with-opentelemetry-and-signoz.jsonld"}}