agentsclimarketplace

Opentelemetry net instrumentation vb ja

Skill oreguchi/vb-winforms-skills/skills/opentelemetry-net-instrumentation-vb-ja

VB.NET コードベースで OpenTelemetry の計装を実装するための指針を提供する。トレース(Activities/Spans)、メトリクス、命名規則、エラー処理、性能、API 設計のベストプラクティスをカバーする。WinForms 環境特有の補足(UI スレッド境界、Generic Host 統合、長期稼働時ライフサイクル)も別ファイルで提供する。From its SKILL.md

Install
npx -y skills add oreguchi/vb-winforms-skills --skill opentelemetry-net-instrumentation-vb-ja

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

24.3 KB, ~7.3k tokens by cl100k_base, as published. Nobody here has run it

VB.NET 向け OpenTelemetry 計装スキル

概要

VB.NET コードベースで OpenTelemetry の計装を実装するための指針を提供する。トレース(Activities/Spans)、メトリクス、命名規則、エラー処理、性能、API 設計のベストプラクティスをカバーする。

いつ使うか

  • VB.NET コードに OpenTelemetry 計装を追加するとき
  • ActivitySource やメトリクスを作成・修正するとき
  • 既存のテレメトリ実装を準拠性の観点でレビューするとき
  • 計装の性能を最適化するとき
  • 公開 API 表面の一部となるテレメトリ API を設計するとき

前提条件

  • OpenTelemetry SDK を利用する VB.NET アプリケーション
  • System.Diagnostics.Metrics および ActivitySource API の理解
  • オブザーバビリティのバックエンド(Jaeger、Prometheus、Grafana など)へのアクセス

以下のサンプルコードは Imports System.DiagnosticsImports System.Diagnostics.MetricsImports System.Collections.GenericImports System.Linq を前提とする。

コア原則

回復性ファースト

重要:診断/トレース/メトリクスのロジックで発生した例外が、アプリケーション処理に影響を 絶対に与えてはならない

  • Activity 拡張メソッド内を除き、常に null の Activity 参照に対して保護する(activity?.ExtensionMethod() を使う)
  • Activity インスタンスは null になり得ると想定する(リスナーが購読したときのみ作成される)
  • すべての計装コードを適切な null チェックで保護する

API 表面の認識

  • 出力(emit)されたあらゆるテレメトリは公開 API 表面の一部となる
  • 変更は破壊的変更ガイドラインの対象となる
  • テレメトリは既定で出力すべき(収集側は OpenTelemetry SDK で利用者がオプトインする)
  • 例外:高カーディナリティのメトリクスディメンションは明示的なオプトインを要する場合がある

標準準拠

トレース / スパン(Activity)

ActivitySource のセットアップ

' ✅ 正しい:DiagnosticSource ではなく ActivitySource を使う
Public Class MyFeature
    ' プライマリ ActivitySource — 名前は通常コンポーネント名または NuGet パッケージ名と一致させる
    Private Shared ReadOnly ActivitySource As New ActivitySource("MyApp.MyComponent", "1.0.0")

    ' オプトイン用の追加 ActivitySource(用途を絞った特殊用途向け)
    Private Shared ReadOnly DetailedActivitySource As New ActivitySource("MyApp.MyComponent.Detailed", "1.0.0")
End Class

ルール

  • すべてのコンポーネントは主要なアクティビティ用にプライマリ ActivitySource を定義する
  • 名前は通常コンポーネントまたは NuGet パッケージ名と一致させる(例:"MyCompany.MyLibrary"
  • ActivitySource は SemVer でバージョニングする
  • 特殊用途やオプトイン専用のシナリオには別の ActivitySource を作成する

Activity の作成

' ✅ 正しい:作成前に HasListeners をチェックする
If ActivitySource.HasListeners() Then
    Using activity As Activity = ActivitySource.StartActivity("ProcessItem", ActivityKind.Internal)
        If activity IsNot Nothing Then
            activity.DisplayName = "Processing order #12345"

            ' 要求されたときのみ重いタグ計算を行う
            If activity.IsAllDataRequested Then
                activity.SetTag("app.item_id", itemId)
                activity.SetTag("app.item_type", itemType)
            End If
        End If
    End Using
End If

' ❌ 誤り:非同期ヘルパーメソッド内で Activity を開始しない(呼び出し元から見て親子関係が壊れる)
Private Async Function HelperAsync() As Task
    Using activity As Activity = ActivitySource.StartActivity("Helper") ' ❌ よくない
        Await DoWorkAsync()
    End Using
End Function

ルール

  • 作成前に ActivitySource.HasListeners() をチェックする(ゼロアロケーションの高速パス)
  • 作成後は必ず activityNothing でないか確認する
  • 非同期ヘルパーメソッド内で Activity を開始しない(Activity.CurrentAsyncLocal(Of T) ベース。ヘルパー内で Activity を Dispose すると Activity.Current が呼び出し側で本来の値に戻らない場合があり、呼び出し側のトレース親子関係が崩れる)
  • 高コストな計算の前に activity.IsAllDataRequested をチェックする
  • 常に W3C ID 形式を使う(親が階層形式の場合は形式変更を強制する)

Activity の命名

' ✅ 正しい:一意の操作名 + 親しみやすい表示名
Using activity As Activity = ActivitySource.StartActivity(
    name:="ProcessItem",              ' 一意、スパンのクラスを識別
    kind:=ActivityKind.Internal
)
    activity.DisplayName = "Processing order #12345" ' 人間に読みやすい、具体的でよい
End Using

' ❌ 誤り:操作名に実行時データを含めない
Using activity As Activity = ActivitySource.StartActivity($"Process_{itemId}") ' ❌ よくない
End Using

ルール

  • 各スパン種別は一意の OperationName を持つ(統計的に意味のあるスパンのクラスを識別)
  • 操作名に実行時データを含めない(コンパイル時/設定時の情報のみ)
  • 具体情報は人間に読みやすい DisplayName で表現する
  • OpenTelemetry スパン命名規約 に従う

スパン属性(タグ)

' ✅ 正しい:名前空間付き、小文字、アンダースコア区切り
activity?.SetTag("myapp.order_id", orderId)
activity?.SetTag("myapp.order_type", orderType)
activity?.SetTag("myapp.db.table_name", tableName)

' 該当する場合は標準 semantic conventions を使う
activity?.SetTag("db.system", "postgresql")
activity?.SetTag("http.method", "GET")

' ❌ 誤り:さまざまな命名規則違反
activity?.SetTag("MyApp.OrderId", orderId)         ' ❌ 大小文字違反
activity?.SetTag("myapp.order-id", orderId)        ' ❌ 区切り文字違反
activity?.SetTag("myapp.orders", count)            ' ❌ 複数形
activity?.SetTag("unrelated.ip_address", ip)       ' ❌ このアクティビティと無関係

命名規則

  • コンポーネントに合わせた名前空間 prefix を使う:myapp.*myapp.db.*
  • 全て小文字
  • 複数語の属性はアンダースコア(_)区切り
  • 単数形
  • このアクティビティに直接関連するタグのみ設定する
  • 該当する標準 OpenTelemetry semantic conventions を独自属性より優先する
  • 標準 semantic convention は、下流のライブラリが同じ規約を設定しないと確信できる場合のみ自前で設定する(重複設定を避けるため)

Activity のステータスとエラー

' ✅ 正しい:ステータスを設定し例外を記録する
Try
    Await ProcessItemAsync()
    activity?.SetStatus(ActivityStatusCode.Ok)
Catch ex As Exception
    If activity IsNot Nothing Then
        activity.SetStatus(ActivityStatusCode.Error)
        activity.SetTag("otel.status_code", "error")
        activity.SetTag("otel.status_description", ex.Message)

        ' OTel 仕様に従って exception イベントを記録
        activity.AddEvent(New ActivityEvent(
            "exception",
            tags:=New ActivityTagsCollection From {
                {"exception.type", ex.GetType().FullName},
                {"exception.message", ex.Message},
                {"exception.stacktrace", ex.ToString()}
            }
        ))
    End If
    Throw
End Try

ルール

  • 成功時は ActivityStatusCode.Ok を設定する
  • 例外時は ActivityStatusCode.Error を設定する
  • 必ず otel.status_codeotel.status_description タグを付与する
  • OTel 例外規約 に従って exception イベントを記録する

Activity のイベント

' ✅ 正しい:追加コンテキストにイベントを使う(控えめに)
activity?.AddEvent(New ActivityEvent("ItemRetried", tags:=New ActivityTagsCollection From {
    {"retry_attempt", retryCount},
    {"next_retry_delay", delayMs}
}))

' ❌ 誤り:詳細ログにイベントを使わない
activity?.AddEvent(New ActivityEvent($"Step {i} completed")) ' ❌ ログ機能を使うべき

ルール

  • イベントは送信までインメモリに保持される(控えめに使う)
  • 追加コンテキスト用のみ。複数イベントが必要ならネストしたスパンを検討する
  • 詳細情報にはログ機能を使う

Activity へのアクセス

' ❌ 誤り:特定のスパンが必要な場合、Activity.Current に依存しない
Public Async Function HandleAsync(context As Context) As Task
    Dim activity As Activity = Activity.Current ' ❌ ユーザー作成のスパンかもしれず、自分が開始したものとは限らない
    activity?.SetTag("custom", "value")
End Function

' ✅ 正しい:Activity を明示的に渡す、または専用 context オブジェクトに格納する
Public Async Function HandleAsync(context As Context) As Task
    Dim activity As Activity = Nothing
    If context.TryGetActivity(activity) Then
        activity?.SetTag("custom", "value")
    End If
End Function

メトリクス

Meter とメトリクスクラスのセットアップ

' ✅ 正しい:機能/コンポーネント単位でメトリクスをグルーピング
Public NotInheritable Class OrderProcessingMetrics
    Implements IDisposable

    Private ReadOnly meter As Meter
    Private ReadOnly processingDuration As Histogram(Of Double)
    Private ReadOnly itemsProcessed As Counter(Of Long)

    Public Sub New()
        meter = New Meter("MyApp.OrderProcessing", "1.0.0")

        ' 単数形の名前、適切な単位、ネスト階層
        processingDuration = meter.CreateHistogram(Of Double)(
            "myapp.order.processing.duration",
            unit:="s",
            description:="Duration of order processing"
        )

        itemsProcessed = meter.CreateCounter(Of Long)(
            "myapp.order.processing.count",
            unit:="{order}",
            description:="Number of orders processed"
        )
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        meter.Dispose()
    End Sub
End Class

命名規則OTel semantic conventions に従う):

  • 単数形の名前を使う(数量を示したいときは複数形ではなく _count 接尾辞を付ける)
  • ネスト階層:myapp.order.processing.duration
  • 単位を定義する(s、ms、{item}、{connection})
  • 技術的接尾辞を避ける(_counter_histogram
  • 採用が証明されるまでは pre-1.0.0 バージョンから始める

メトリクス記録メソッドの命名

' ✅ 正しい:アクション/結果ベースの命名、結果ごとに別メソッド
Public NotInheritable Class OrderProcessingMetrics
    ' 発生したイベント:何が起こったかを記述
    Public Sub OrderProcessingSucceeded(orderType As String, duration As TimeSpan)
        processingDuration.Record(duration.TotalSeconds,
            New KeyValuePair(Of String, Object)("myapp.order_type", orderType),
            New KeyValuePair(Of String, Object)("outcome", "success")
        )
    End Sub

    Public Sub OrderProcessingFailed(orderType As String, exception As Exception, duration As TimeSpan)
        processingDuration.Record(duration.TotalSeconds,
            New KeyValuePair(Of String, Object)("myapp.order_type", orderType),
            New KeyValuePair(Of String, Object)("outcome", "failure"),
            New KeyValuePair(Of String, Object)("exception.type", exception.GetType().Name)
        )
    End Sub

    Public Sub ConnectionOpened()
        connectionsOpen.Add(1)
    End Sub

    Public Sub ConnectionClosed()
        connectionsOpen.Add(-1)
    End Sub
End Class

' ❌ 誤り:さまざまな命名アンチパターン
Public Sub RecordOrderProcessingDuration(...) ' ❌ メトリクス名を関数名にしない
Public Sub RecordError(succeeded As Boolean, ex As Exception) ' ❌ 紛らわしいシグネチャ

ルール(ASP.NET Core パターンを参考):

  • アクション/結果で命名する:OrderProcessingSucceededRetryAttemptedConnectionFailed
  • メトリクス名で命名しない:RecordXxxIncrementXxx を避ける
  • 結果ごとに別メソッドにする(boolean フラグ+オプション例外の組み合わせを避ける)
  • 状態変化はイベントベース命名:ConnectionOpened()ItemQueued()

メトリクスのディメンション

' ✅ 正しい:低カーディナリティ、事前定義のディメンション
Public Sub OrderProcessingSucceeded(orderType As String, duration As TimeSpan)
    processingDuration.Record(duration.TotalSeconds,
        New KeyValuePair(Of String, Object)("myapp.order_type", orderType),
        New KeyValuePair(Of String, Object)("myapp.region", region),
        New KeyValuePair(Of String, Object)("outcome", "success")
    )
End Sub

' ❌ 誤り:高カーディナリティのディメンション(無制限な値はカーディナリティ爆発を引き起こす)
Public Sub OrderFailed(orderId As String, exceptionMessage As String)
    failureCount.Add(1,
        New KeyValuePair(Of String, Object)("order_id", orderId),               ' ❌ 無制限
        New KeyValuePair(Of String, Object)("exception_message", exceptionMessage) ' ❌ 無制限
    )
End Sub

ルール

  • ディメンションは Instrument(Counter / Histogram 等)の作成時に事前定義しなければならない
  • 動的/無制限な値を避ける(カーディナリティ爆発:一意の値ごとに新しい時系列行が作られる)
  • 高カーディナリティのディメンションはオプトイン設定にしなければならない
  • 低カーディナリティの識別子を使う:item type、queue name、outcome
  • ディメンション名はコンポーネント間で一貫させる:myapp.region はどこでも同じ意味
  • 機微情報を避ける
  • メトリクスエンリッチメントの代替手段 を検討する
  • 利用者は相関のために メトリクス exemplar を有効化できる(ディメンション経由ではない)

性能要件

計装は既定で軽量でなければならない。オーバーヘッドを最小化するために以下のルールに従う。

ゼロアロケーションの高速パス

' ✅ 正しい:軽いチェックでガードする
If ActivitySource.HasListeners() Then
    Using activity As Activity = ActivitySource.StartActivity("Operation")
        ' ... 重い処理
    End Using
End If

' ✅ 正しい:メトリクスには TagList(structure)を使う
Dim tags As New TagList()
tags.Add("myapp.order_type", orderType)
tags.Add("outcome", "success")
counter.Add(1, tags)

タイミング計測

.NET 7 以降では Stopwatch.GetElapsedTime で簡潔に計測できる。古いランタイム(.NET 6 / .NET Framework 4.8)には同 API が無いため、GetTimestamp の差分を Stopwatch.Frequency で割る互換実装を使う。

' ✅ 正しい:.NET 7+ — タイムスタンプ計算(割り当てなし)
Dim startTime = Stopwatch.GetTimestamp()
Try
    Await ProcessAsync()
Finally
    Dim duration = Stopwatch.GetElapsedTime(startTime)
    metrics.OrderProcessingSucceeded(orderType, duration)
End Try

' ✅ 正しい:.NET 6 / .NET Framework 4.8 互換版(GetElapsedTime が無い環境)
Dim startTime2 = Stopwatch.GetTimestamp()
Try
    Await ProcessAsync()
Finally
    ' GetElapsedTime が無い環境向けの計算(ticks → 秒 → TimeSpan)
    Dim elapsedTicks = Stopwatch.GetTimestamp() - startTime2
    Dim duration = TimeSpan.FromSeconds(elapsedTicks / CDbl(Stopwatch.Frequency))
    metrics.OrderProcessingSucceeded(orderType, duration)
End Try

' ❌ 誤り:Stopwatch オブジェクトを割り当てる
Dim stopwatchObj = Stopwatch.StartNew() ' ❌ 割り当てが発生

' ❌ 誤り:IDisposable のタイミングクラス(使用ごとに割り当て)
Using New MetricScope(metrics, "ProcessOrder") ' ❌ よくない
    ProcessOrder()
End Using

隠れた割り当てを避ける

' ❌ 誤り:文字列補間で割り当てが発生
activity?.SetTag("item", $"Processing {itemId}") ' ❌ 割り当てが発生

' ✅ 正しい:先に IsAllDataRequested を確認
If activity IsNot Nothing AndAlso activity.IsAllDataRequested Then
    activity.SetTag("item", $"Processing {itemId}")
End If

' ❌ 誤り:LINQ で列挙子が割り当てられる
activity?.SetTag("handlers", handlers.Select(Function(h) h.Name).ToArray()) ' ❌ よくない

' ✅ 正しい:手動構築または事前確認
If activity IsNot Nothing AndAlso activity.IsAllDataRequested Then
    activity.SetTag("handlers", String.Join(",", handlers.Select(Function(h) h.Name)))
End If

ルール

  • Stopwatch.StartNew() を使わない(タイムスタンプ計算を使う)
  • IDisposable のタイミングラッパークラスを使わない
  • 配列/辞書よりも TagList(structure)を優先する
  • 隠れた処理を避ける:ホットパスでは LINQ、文字列補間、async ステートマシンを避ける

テスト要件

スパンのテスト

<Test>
Public Async Function Should_create_processing_span_with_correct_parent() As Task
    ' Arrange
    Using parent As Activity = New Activity("Parent").Start()
        ' Act
        Await handler.Handle(item)

        ' Assert
        Dim processingSpan = recordedActivities.Single(Function(a) a.OperationName = "ProcessItem")
        Assert.AreEqual(parent.Id, processingSpan.ParentId)
        Assert.AreEqual("myapp.item_type", processingSpan.Tags.First().Key)
    End Using
End Function

<Test>
Public Sub Should_not_introduce_breaking_changes_to_span_names()
    ' スパン名の文字列値がテスト対象であることを保証
    Assert.AreEqual("ProcessItem", MyFeature.SpanName)
End Sub

ルール

  • アクティビティがどのスパンに接続するかをテストする
  • 文字列値(スパン名、タグ名)をテストして破壊的変更を防ぐ
  • テレメトリは公開 API の一部であることを忘れない

バージョニング

  • テレメトリのバージョニングはパッケージバージョンと切り離す
  • SemVer のセマンティクスを使う
  • トレースとメトリクスは別バージョンを使う(独立に進化させる)
  • 採用度/有用性が証明されるまで pre-1.0.0 バージョンから始める
Private Shared ReadOnly ActivitySource As New ActivitySource("MyApp.MyComponent", "0.9.0")
Private ReadOnly meter As New Meter("MyApp.MyComponent", "0.8.0")

VB.NET WinForms 特有の補足

本スキル中核は VB.NET 全般向け。WinForms 環境では以下の追加考慮が必要となるため、要点を以下に示す。詳細パターン・コードサンプル全文は references/winforms-supplement.md を参照

SDK セットアップ概要

WinForms アプリで OpenTelemetry SDK を有効化する標準パターン:

  • Generic Host 経由(推奨)services.AddOpenTelemetry().WithTracing(...).WithMetrics(...)Microsoft.Extensions.Hosting の DI と統合。MainForm も DI 解決し Application.Run に渡す
  • Sdk.CreateTracerProviderBuilder 直接版:Hosting なしの最小構成

AddSqlClientInstrumentation() で SQL Server 接続を 手書き計装ゼロでスパン化 できる。AddHttpClientInstrumentation() も同様に外部 API 呼び出しをカバー。詳細は references/winforms-supplement.md §SDK セットアップ。

UI スレッドと Activity の境界

  • Async Sub は UI ハンドラー(Button_Click 等)専用。ビジネスロジックは Async Function ... As Task を使う(Async Sub 内例外は呼び出し側で捕捉できず、計装の SetStatus(Error) も呼ばれない/BC42356)
  • UI ハンドラー内では Try/Catch 必須、例外時 activity?.SetStatus(ActivityStatusCode.Error) を呼ぶ
  • Activity.CurrentAsyncLocal(Of T)Await continuation には伝搬するが、Control.Invoke / BeginInvoke 越境では見えない場合がある
  • UI 越境でタグ付けする場合は Activity 参照を明示的にクロージャーでキャプチャ する。新規スパンを張るなら parentContext で親を明示
  • ワーカースレッドから UI コントロールに触ると InvalidOperationException(クロススレッド)。UI 値は Task.Run 突入前にローカル変数へスナップショット

詳細とコードサンプルは references/winforms-supplement.md §UI スレッドと Activity の境界。

長期稼働時のライフサイクル管理

  • ActivitySource / Meterプロセス単一インスタンス(シングルトン)にする。フォーム単位で New するとリスナーがコールバック保持しメモリリーク
  • アプリ終了時は IHostedService.StopAsync または Application.ApplicationExit でフラッシュ + Dispose を呼ぶ。ForceFlushDispose の前に置くこと
  • 長期稼働ほどカーディナリティ累積が深刻。「動的/無制限な値をディメンションにしない」原則の遵守が必須

詳細は references/winforms-supplement.md §長期稼働時のライフサイクル管理。

参考資料

What ships with it: 1 file

12.9 KB alongside SKILL.md

references/

Gives 0 of the 12 instructions most monitoring observability skills give in ~7.3k tokens

Counted across 530 of the 532 authors here whose files we hold, read 2026-09-06

  • Use structured JSON loggingin 40 of 530, across 36 files
  • Link every alert to a runbookin 29 of 530, across 27 files
  • Attach correlation IDs to every log linein 19 of 530, across 16 files
  • Alert on symptoms rather than causesin 19 of 530, across 17 files
  • Use OpenTelemetry for distributed tracingin 15 of 530, across 14 files
  • Alert on symptoms users feelin 15 of 530, across 13 files
  • Implement health check endpointsin 14 of 530, across 10 files
  • Inspect existing dashboards firstin 12 of 530, across 4 files
  • Build the minimum useful boardin 12 of 530, across 4 files
  • Start from operator questionsin 12 of 530, across 4 files
  • Propagate trace context across boundariesin 11 of 530, across 10 files
  • Include trace id in all log entriesin 10 of 530, across 9 files

Said here and by no other author read

  • Verify activity is not Nothing after creation
  • Use W3C ID format
  • Set ActivityStatusCode.Ok on success
  • Set ActivityStatusCode.Error on exception
  • Predefine metric dimensions
  • Call ForceFlush and Dispose on exit

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.