Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/discord-events.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Discord Events

on:
issues:
types:
- opened
pull_request:
types:
- opened
Comment thread
coderabbitai[bot] marked this conversation as resolved.
release:
types:
- published

jobs:
notify-discord:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
permissions: {}
uses: ./.github/workflows/discord-notify.yml
secrets:
discord_webhook: ${{ secrets.DISCORD_WEBHOOK }}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
160 changes: 160 additions & 0 deletions .github/workflows/discord-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
name: Discord Notify

on:
workflow_call:
inputs:
username:
description: Discord webhook username
required: false
default: DEVtheOPS Bot
type: string
avatar_url:
description: Discord webhook avatar URL
required: false
default: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
type: string
color:
description: Decimal embed color override
required: false
default: "5793266"
type: string
title_prefix:
description: Optional prefix added to the embed title
required: false
default: ""
type: string
include_body:
description: Include issue, PR, or release body in the embed description
required: false
default: true
type: boolean
secrets:
discord_webhook:
description: Discord webhook URL
required: true

jobs:
notify:
name: Send Discord notification
runs-on: ubuntu-latest
permissions:
contents: read
issues: read
pull-requests: read
steps:
- name: Build payload
id: payload
uses: actions/github-script@v7
env:
DISCORD_USERNAME: ${{ inputs.username }}
DISCORD_AVATAR_URL: ${{ inputs.avatar_url }}
DISCORD_COLOR: ${{ inputs.color }}
DISCORD_TITLE_PREFIX: ${{ inputs.title_prefix }}
DISCORD_INCLUDE_BODY: ${{ inputs.include_body }}
with:
script: |
const eventName = context.eventName;
const action = context.payload.action || "";
const repo = context.repo;
const prefix = process.env.DISCORD_TITLE_PREFIX || "";
const defaultColor = Number.parseInt(process.env.DISCORD_COLOR || "5793266", 10) || 5793266;
const includeBody = (process.env.DISCORD_INCLUDE_BODY || "true") === "true";

const truncate = (value, limit) => {
if (!value) return "";
const normalized = String(value).replace(/\r\n/g, "\n").trim();
if (!normalized) return "";
return normalized.length > limit ? `${normalized.slice(0, limit - 1)}…` : normalized;
};

const author = {
name: `${repo.owner}/${repo.repo}`,
url: `https://github.com/${repo.owner}/${repo.repo}`,
icon_url: `https://github.com/${repo.owner}.png`
};

const fields = [
{ name: "Repository", value: `[${repo.owner}/${repo.repo}](https://github.com/${repo.owner}/${repo.repo})`, inline: true },
{ name: "Actor", value: `[${context.actor}](https://github.com/${context.actor})`, inline: true },
{ name: "Event", value: `${eventName}${action ? `.${action}` : ""}`, inline: true }
];

let title = `GitHub event in ${repo.repo}`;
let url = `https://github.com/${repo.owner}/${repo.repo}`;
let description = "";
let color = defaultColor;

if (eventName === "issues" && context.payload.issue) {
const issue = context.payload.issue;
title = `Issue #${issue.number} opened: ${issue.title}`;
url = issue.html_url;
description = includeBody ? truncate(issue.body, 4000) : "";
color = 16098851;
fields.push(
{ name: "Author", value: `[${issue.user.login}](${issue.user.html_url})`, inline: true },
{ name: "Labels", value: issue.labels.length ? issue.labels.map((label) => label.name).join(", ") : "None", inline: true }
);
}

if (eventName === "pull_request" && context.payload.pull_request) {
const pr = context.payload.pull_request;
title = `PR #${pr.number} opened: ${pr.title}`;
url = pr.html_url;
description = includeBody ? truncate(pr.body, 4000) : "";
color = 3447003;
fields.push(
{ name: "Author", value: `[${pr.user.login}](${pr.user.html_url})`, inline: true },
{ name: "Branch", value: `\`${pr.head.ref}\` -> \`${pr.base.ref}\``, inline: true }
);
}

if (eventName === "release" && context.payload.release) {
const release = context.payload.release;
title = `Release published: ${release.name || release.tag_name}`;
url = release.html_url;
description = includeBody ? truncate(release.body, 4000) : "";
color = 10181046;
fields.push(
{ name: "Tag", value: `\`${release.tag_name}\``, inline: true },
{ name: "Prerelease", value: release.prerelease ? "Yes" : "No", inline: true }
);
}

if (prefix) {
title = `${prefix} ${title}`;
}

const payload = {
username: process.env.DISCORD_USERNAME || "DEVtheOPS Bot",
avatar_url: process.env.DISCORD_AVATAR_URL || "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
embeds: [
{
title,
url,
description,
color,
author,
fields,
timestamp: new Date().toISOString(),
footer: {
text: "GitHub Actions"
}
}
]
};

core.setOutput("json", JSON.stringify(payload));

- name: Post to Discord
env:
DISCORD_WEBHOOK: ${{ secrets.discord_webhook }}
DISCORD_PAYLOAD: ${{ steps.payload.outputs.json }}
run: |
curl --fail --silent --show-error \
--connect-timeout 10 \
--max-time 30 \
--retry 3 \
--retry-delay 2 \
-H "Content-Type: application/json" \
-d "$DISCORD_PAYLOAD" \
"$DISCORD_WEBHOOK"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
58 changes: 51 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![npm downloads](https://img.shields.io/npm/dm/@devtheops/opencode-plugin-otel.svg)](https://www.npmjs.com/package/@devtheops/opencode-plugin-otel)
[![GitHub stars](https://img.shields.io/github/stars/DEVtheOPS/opencode-plugin-otel.svg)](https://github.com/DEVtheOPS/opencode-plugin-otel/stargazers)
[![Build status](https://img.shields.io/github/actions/workflow/status/DEVtheOPS/opencode-plugin-otel/release-please.yml?branch=main)](https://github.com/DEVtheOPS/opencode-plugin-otel/actions/workflows/release-please.yml)
[![Discord notifications](https://img.shields.io/badge/discord-notifications-5865F2?logo=discord&logoColor=white)](https://discord.gg/zavuskz8xB)
[![License](https://img.shields.io/npm/l/@devtheops/opencode-plugin-otel.svg)](https://github.com/DEVtheOPS/opencode-plugin-otel/blob/main/LICENSE)

An [opencode](https://opencode.ai) plugin that exports telemetry via OpenTelemetry (OTLP over gRPC or HTTP/protobuf), mirroring the same signals as [Claude Code's monitoring](https://code.claude.com/docs/en/monitoring-usage).
Expand All @@ -21,6 +22,7 @@ An [opencode](https://opencode.ai) plugin that exports telemetry via OpenTelemet
- [Honeycomb example](#honeycomb-example)
- [Claude Code dashboard compatibility](#claude-code-dashboard-compatibility)
- [Local development](#local-development)
- [GitHub Discord notifications](#github-discord-notifications)

## What it instruments

Expand Down Expand Up @@ -83,17 +85,17 @@ All configuration is via environment variables. Set them in your shell profile (

| Variable | Default | Description |
|----------|---------|-------------|
| `OPENCODE_ENABLE_TELEMETRY` | _(unset)_ | Set to any non-empty value to enable the plugin |
| `OPENCODE_ENABLE_TELEMETRY` | *(unset)* | Set to any non-empty value to enable the plugin |
| `OPENCODE_OTLP_ENDPOINT` | `http://localhost:4317` | OTLP collector endpoint. For `grpc`, use the collector host/port. For `http/protobuf`, use the base URL and the plugin will append `/v1/traces`, `/v1/metrics`, and `/v1/logs`. |
| `OPENCODE_OTLP_PROTOCOL` | `grpc` | OTLP transport protocol: `grpc` or `http/protobuf` |
| `OPENCODE_OTLP_METRICS_INTERVAL` | `60000` | Metrics export interval in milliseconds |
| `OPENCODE_OTLP_LOGS_INTERVAL` | `5000` | Logs export interval in milliseconds |
| `OPENCODE_METRIC_PREFIX` | `opencode.` | Prefix for all metric names (e.g. set to `claude_code.` for Claude Code dashboard compatibility) |
| `OPENCODE_DISABLE_METRICS` | _(unset)_ | Comma-separated list of metric name suffixes to disable (e.g. `cache.count,session.duration`) |
| `OPENCODE_OTLP_HEADERS` | _(unset)_ | Comma-separated `key=value` headers added to all OTLP exports. **Keep out of version control — may contain sensitive auth tokens.** |
| `OPENCODE_OTLP_HEADERS_HELPER` | _(unset)_ | Executable script/binary that returns dynamic OTLP headers as JSON after an auth failure. Helper headers override `OPENCODE_OTLP_HEADERS`. |
| `OPENCODE_RESOURCE_ATTRIBUTES` | _(unset)_ | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` |
| `OPENCODE_OTLP_METRICS_TEMPORALITY` | _(unset)_ | Metrics aggregation temporality: `delta`, `cumulative`, or `lowmemory`. Required for Datadog (`delta`). Copied to `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. |
| `OPENCODE_DISABLE_METRICS` | *(unset)* | Comma-separated list of metric name suffixes to disable (e.g. `cache.count,session.duration`) |
| `OPENCODE_OTLP_HEADERS` | *(unset)* | Comma-separated `key=value` headers added to all OTLP exports. **Keep out of version control — may contain sensitive auth tokens.** |
| `OPENCODE_OTLP_HEADERS_HELPER` | *(unset)* | Executable script/binary that returns dynamic OTLP headers as JSON after an auth failure. Helper headers override `OPENCODE_OTLP_HEADERS`. |
| `OPENCODE_RESOURCE_ATTRIBUTES` | *(unset)* | Comma-separated `key=value` pairs merged into the OTel resource. Example: `service.version=1.2.3,deployment.environment=production` |
| `OPENCODE_OTLP_METRICS_TEMPORALITY` | *(unset)* | Metrics aggregation temporality: `delta`, `cumulative`, or `lowmemory`. Required for Datadog (`delta`). Copied to `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`. |

### Quick start

Expand Down Expand Up @@ -187,7 +189,7 @@ export OPENCODE_OTLP_HEADERS="signoz-ingestion-key=<SIGNOZ_INGESTION_KEY>"
```

> Use `https://ingest.in.signoz.cloud:443` for India, `https://ingest.eu2.signoz.cloud:443` for EU2, etc.
> See [SigNoz setup docs](https://signoz.io/docs/cloud/) for all regions.
> See [SigNoz setup docs](https://signoz.io/docs/cloud/) for all regions.

### Datadog example

Expand Down Expand Up @@ -231,3 +233,45 @@ export OPENCODE_METRIC_PREFIX=claude_code.
## Local development

See [CONTRIBUTING.md](./CONTRIBUTING.md).

## GitHub Discord notifications

This repo includes a reusable workflow at `.github/workflows/discord-notify.yml` that posts a Discord embed for supported GitHub events. The included `.github/workflows/discord-events.yml` file wires it up for:

- `issues.opened`
- `pull_request.opened`
- `release.published`

Set an org or repo secret named `DISCORD_WEBHOOK` and the workflow will post to that webhook automatically.

To reuse it from another repository in the `DEVtheOPS` org:

```yaml
name: Discord Events

on:
issues:
types: [opened]
pull_request:
types: [opened]
release:
types: [published]

jobs:
notify-discord:
uses: DEVtheOPS/opencode-plugin-otel/.github/workflows/discord-notify.yml@main
with:
username: DEVtheOPS Bot
title_prefix: "[DEVtheOPS]"
include_body: true
secrets:
discord_webhook: ${{ secrets.DISCORD_WEBHOOK }}
```

Available workflow inputs:

- `username`: webhook display name
- `avatar_url`: webhook avatar image URL
- `title_prefix`: optional title prefix for the embed
- `include_body`: include the issue, PR, or release body in the card
- `color`: fallback embed color for unsupported events
Loading