From vb-winforms-skills
VB.NET コードベースで OpenTelemetry の計装を実装するための指針を提供する。トレース(Activities/Spans)、メトリクス、命名規則、エラー処理、性能、API 設計のベストプラクティスをカバーする。WinForms 環境特有の補足(UI スレッド境界、Generic Host 統合、長期稼働時ライフサイクル)も別ファイルで提供する。
How this skill is triggered — by the user, by Claude, or both
Slash command
/vb-winforms-skills:opentelemetry-net-instrumentation-vb-jaThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
VB.NET コードベースで OpenTelemetry の計装を実装するための指針を提供する。トレース(Activities/Spans)、メトリクス、命名規則、エラー処理、性能、API 設計のベストプラクティスをカバーする。
VB.NET コードベースで OpenTelemetry の計装を実装するための指針を提供する。トレース(Activities/Spans)、メトリクス、命名規則、エラー処理、性能、API 設計のベストプラクティスをカバーする。
ActivitySource やメトリクスを作成・修正するときSystem.Diagnostics.Metrics および ActivitySource API の理解以下のサンプルコードは
Imports System.Diagnostics、Imports System.Diagnostics.Metrics、Imports System.Collections.Generic、Imports System.Linqを前提とする。
重要:診断/トレース/メトリクスのロジックで発生した例外が、アプリケーション処理に影響を 絶対に与えてはならない。
Activity 拡張メソッド内を除き、常に null の Activity 参照に対して保護する(activity?.ExtensionMethod() を使う)Activity インスタンスは null になり得ると想定する(リスナーが購読したときのみ作成される)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 を定義する"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() をチェックする(ゼロアロケーションの高速パス)activity が Nothing でないか確認するActivity.Current は AsyncLocal(Of T) ベース。ヘルパー内で Activity を Dispose すると Activity.Current が呼び出し側で本来の値に戻らない場合があり、呼び出し側のトレース親子関係が崩れる)activity.IsAllDataRequested をチェックする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 で表現する' ✅ 正しい:名前空間付き、小文字、アンダースコア区切り
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) ' ❌ このアクティビティと無関係
命名規則:
myapp.*、myapp.db.*_)区切り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_code と otel.status_description タグを付与する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_counter、_histogram)' ✅ 正しい:アクション/結果ベースの命名、結果ごとに別メソッド
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 パターンを参考):
OrderProcessingSucceeded、RetryAttempted、ConnectionFailedRecordXxx、IncrementXxx を避ける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
ルール:
Counter / Histogram 等)の作成時に事前定義しなければならないmyapp.region はどこでも同じ意味計装は既定で軽量でなければならない。オーバーヘッドを最小化するために以下のルールに従う。
' ✅ 正しい:軽いチェックでガードする
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)を優先する<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
ルール:
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 環境では以下の追加考慮が必要となるため、要点を以下に示す。詳細パターン・コードサンプル全文は references/winforms-supplement.md を参照。
WinForms アプリで OpenTelemetry SDK を有効化する標準パターン:
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 セットアップ。
Async Sub は UI ハンドラー(Button_Click 等)専用。ビジネスロジックは Async Function ... As Task を使う(Async Sub 内例外は呼び出し側で捕捉できず、計装の SetStatus(Error) も呼ばれない/BC42356)Try/Catch 必須、例外時 activity?.SetStatus(ActivityStatusCode.Error) を呼ぶActivity.Current は AsyncLocal(Of T) で Await continuation には伝搬するが、Control.Invoke / BeginInvoke 越境では見えない場合があるparentContext で親を明示InvalidOperationException(クロススレッド)。UI 値は Task.Run 突入前にローカル変数へスナップショット詳細とコードサンプルは references/winforms-supplement.md §UI スレッドと Activity の境界。
ActivitySource / Meter は プロセス単一インスタンス(シングルトン)にする。フォーム単位で New するとリスナーがコールバック保持しメモリリークIHostedService.StopAsync または Application.ApplicationExit でフラッシュ + Dispose を呼ぶ。ForceFlush を Dispose の前に置くこと詳細は references/winforms-supplement.md §長期稼働時のライフサイクル管理。
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub oreguchi/vb-winforms-skills --plugin vb-winforms-skills