Mackerel Advent Calendar 2025 9日目だよ〜
こんにちは、Mackerel CRE の
id:yohfee です。
11月に開発チームから CRE チームに移籍していました。
改めましてよろしくお願いいたします。
今年は 開発者コミュニティのみなさんと交流した1年 — Mackerel 2025年のイベント活動ふりかえり - Mackerel ブログ #mackerelio にもある通り、カンファレンススポンサーとして何度かブース出展し、 デモとして実際の Mackerel APM の画面をたくさんの方に触れていただくことができました。
最初の Go Conference 2025 では GitHub - mackerelio-labs/mackerel-demo-gocon-2025: Mackerel Demo Go Conference 2025 を用意したのですが、 あらかじめトレースを投稿しておく準備が必要なことや、Go であるため他のカンファレンスには持っていきづらいなど、継続的に利用するには課題が多くありました。
ということで、まずは以下のコンセプトのものを用意できないかと考えました。
- 継続的・自動的にトレースを投稿し続けること
- 構成が異なる複数サービスにまたがる分散トレーシングであること
- パフォーマンスやエラーなどの典型的な課題を見所として紹介し、改善や解決のイメージを得てもらえること
とはいえ、見所を実装したサービスを複数運用して、常にアクセスされ続ける環境を保持するのは大変です。 これまで普段お見せしていたデモオーガニゼーションや、先日公開した デモ体験 が、 継続的・自動的にトレースを投稿し続けてはいるものの、単一サービスのみで、見所も限られているのは、そのあたりも理由として挙げられます。
そこで思いついたのが、実際に稼働するサービスから正直にトレースデータを生成しなくても、任意のデータをでっち上げて送り付ければいいじゃんということです。 こうして、Azure Functions で5分おきに向こう5分間の3サービス分のトレースを捏造して Mackerel に投稿する仕組みが爆誕しました。
このエントリのタイトルから察せられる通り、実用には至らなかったので、年末お焚き上げで供養します。
まずは準備のコードから。複数サービスを定義するために、Resource に任意の属性を設定して、それぞれに TracerProvider を割り当てます。
その際 ActivitySource を DI に登録しておくのがポイントです。
open System // トレースを生成する処理の実装を DI するためのインターフェース type IRunner = interface abstract member Run: now: DateTime -> unit end open System open Microsoft.Azure.Functions.Worker // Azure Functions の TimerTrigger のエントリーポイント // DI された処理を回すだけ type TimerFunction(runners: IRunner seq) = [<Function(nameof (TimerFunction))>] member _.Run([<TimerTrigger("0 */5 * * * *")>] _timer: TimerInfo) = for i = 0 to 4 do let now = DateTime.UtcNow.AddMinutes i runners |> Seq.iter _.Run(now) open System open System.Diagnostics open System.Runtime.CompilerServices open Microsoft.Azure.Functions.Worker open Microsoft.Azure.Functions.Worker.Builder open Microsoft.Extensions.DependencyInjection open Microsoft.Extensions.Hosting open OpenTelemetry open OpenTelemetry.Exporter open OpenTelemetry.Resources open OpenTelemetry.Trace let configureServices (builder: FunctionsApplicationBuilder) = let mackerelApiKey = Environment.GetEnvironmentVariable "MACKEREL_APIKEY" let serviceNamespace = "mackerel-demo" let registerTracerProvider (resourceBuilder: unit -> ResourceBuilder) name version attributes = let source = new ActivitySource(sprintf "%s-%s" name version) builder.Services.AddKeyedSingleton(source.Name, source) |> ignore Sdk .CreateTracerProviderBuilder() .SetResourceBuilder( resourceBuilder() .AddAttributes(attributes) .AddService(name, serviceNamespace, version) ) .AddSource(source.Name) .SetSampler(AlwaysOnSampler()) .AddOtlpExporter(fun options -> options.Endpoint <- Uri "https://otlp-vaxila.mackerelio.com/v1/traces" options.Protocol <- OtlpExportProtocol.HttpProtobuf options.Headers <- $"Mackerel-Api-Key={mackerelApiKey},Accept=*/*") .Build() |> builder.Services.AddSingleton |> ignore // Ruby の shop サービス V2 registerTracerProvider ResourceBuilder.CreateEmpty "shop" "2.0" (dict [ "deployment.environment.name", "production" "process.runtime.name", "ruby" "process.runtime.version", "3.3.1" "telemetry.sdk.language", "ruby" "telemetry.sdk.name", "opentelemetry" "telemetry.sdk.version", "1.5.0" ]) // Ruby の shop サービス V3 registerTracerProvider ResourceBuilder.CreateEmpty "shop" "3.0" (dict [ "deployment.environment.name", "production" "process.runtime.name", "ruby" "process.runtime.version", "3.4.2" "telemetry.sdk.language", "ruby" "telemetry.sdk.name", "opentelemetry" "telemetry.sdk.version", "1.6.0" ]) // .NET の catalog サービス registerTracerProvider ResourceBuilder.CreateDefault "catalog" "1.1" (dict [ "deployment.environment.name", "production" ]) // Java の basket サービス registerTracerProvider ResourceBuilder.CreateEmpty "basket" "2.51" (dict [ "deployment.environment.name", "production" "host.arch", "amd64" "os.description", "Windows Server 2019 Datacenter" "os.type", "windows" "process.runtime.description", "Microsoft OpenJDK 64-Bit Server VM 17.0.16+8-LTS" "process.runtime.name", "OpenJDK Runtime Environment" "process.runtime.version", "17.0.16+8-LTS" "telemetry.distro.name", "opentelemetry-java-instrumentation" "telemetry.distro.version", "2.21.0" "telemetry.sdk.language", "java" "telemetry.sdk.name", "opentelemetry" "telemetry.sdk.version", "1.55.0" ]) builder.Services .AddApplicationInsightsTelemetryWorkerService() .ConfigureFunctionsApplicationInsights() .AddTransient<IRunner, Index>() .AddTransient<IRunner, UpdateBasket>() // .AddTransient<IRunner, などなど>() [<Extension>] type FunctionsApplicationBuilderExtensions() = [<Extension>] static member ConfigureServices(builder: FunctionsApplicationBuilder) = configureServices builder |> ignore builder [<EntryPoint>] let main args = FunctionsApplication .CreateBuilder(args) .ConfigureFunctionsWebApplication() .ConfigureServices() .Build() .Run() 0
改めて眺めると、処理が増えてくると実行時間が気になってくるので、一つのエントリーポイントでまとめて実行するよりも、処理ごとにエントリーポイントを分けることを考えるのもよさそうです。
また、トレースを生成する処理は次のように、DI された ActivitySource の StartActivity で、任意の属性を設定してスパンを開始し、開始終了時刻も辻褄が合う程度に適当に指定します。
子スパンとしたいものには親としたいスパンのコンテキストを渡すことで親子関係を表現できます。
open System open System.Diagnostics open Microsoft.Extensions.DependencyInjection type Index ( [<FromKeyedServices("shop-2.0")>] shop: ActivitySource, [<FromKeyedServices("catalog-1.1")>] catalog: ActivitySource, [<FromKeyedServices("basket-2.51")>] basket: ActivitySource ) = interface IRunner with member _.Run(now: DateTime) = let now = Random.Shared.NextDouble() |> (*) 60. |> now.AddSeconds use root = shop .StartActivity( "GET /", ActivityKind.Server, ActivityContext( ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded ), dict [ "http.request.method", box "GET" "http.response.status_code", 200 "http.route", "/" "network.protocol.version", "2" "server.address", "shop.example.com" "server.port", 7276 "url.path", "/" "url.scheme", "https" "user_agent.original", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0" ] ) .SetStartTime(now) .SetEndTime(now.AddMilliseconds 23.87) let t = now.AddMilliseconds 3.47 use catalogClient = shop .StartActivity( "GET", ActivityKind.Client, root.Context, dict [ "http.request.method", box "GET" "http.response.status_code", 200 "network.protocol.version", 1.1 "server.address", "catalog.example.com" "server.port", 7241 "url.full", "https://catalog.example.com:7241/api/v1/catalog/items/type/all" ] ) .SetStartTime(t) .SetEndTime(t.AddMilliseconds 18.04) let t = now.AddMilliseconds 8.69 use catalogRoot = catalog .StartActivity( "GET /api/v1/catalog/items/type/all", ActivityKind.Server, catalogClient.Context, dict [ "http.request.method", box "GET" "http.response.status_code", 200 "http.route", "/api/v1/catalog/items/type/all" "network.protocol.version", 1.1 "server.address", "catalog.example.com" "server.port", 7241 "url.path", "/api/v1/catalog/items/type/all" "url.scheme", "https" ] ) .SetStartTime(t) .SetEndTime(t.AddMilliseconds 12.7) let t = now.AddMilliseconds 16.86 let catalogQuery = catalog .StartActivity( "catalogdb", ActivityKind.Client, catalogRoot.Context, dict [ "db.connection_string", box "Host=localhost;Port=52288;Username=postgres;Database=catalogdb" "db.name", "catalogdb" "db.statement", """SELECT c."Id", c."AvailableStock", c."CatalogBrandId", c."CatalogTypeId", c."Description", c."MaxStockThreshold", c."Name", c."OnReorder", c."PictureFileName", c."Price", c."RestockThreshold" FROM "Catalog" AS c ORDER BY c."Id" LIMIT @__pageSize + 1""" "db.system", "postgresql" "db.user", "postgres" "net.transport", "ip_tcp" ] ) .SetStartTime(t) .SetEndTime(t.AddMilliseconds 2.25) .AddEvent(ActivityEvent("received-first-response", t.AddMilliseconds 1.79)) catalogQuery.Stop() catalogRoot.Stop() catalogClient.Stop() let t = now.AddMilliseconds 3.49 use basketClient = shop .StartActivity( "BasketApi.Basket/GetBasketById", ActivityKind.Client, root.Context, dict [ "rpc.grpc.status_code", box 0 "rpc.method", "GetBasketById" "rpc.service", "BasketApi.Basket" "rpc.system", "grpc" "server.address", "basket.example.com" "server.port", 443 ] ) .SetStartTime(t) .SetEndTime(t.AddMilliseconds 8.76) let t = now.AddMilliseconds 9.07 use basketRoot = basket .StartActivity( "POST /BasketApi.Basket/GetBasketById", ActivityKind.Server, basketClient.Context, dict [ "grpc.method", box "/BasketApi.Basket/GetBasketById" "grpc.status_code", 0 "http.request.method", "POST" "http.response.status_code", 200 "http.route", "/BasketApi.Basket/GetBasketById" "network.protocol.version", 2 "server.address", "basket.example.com" "url.path", "/BasketApi.Basket/GetBasketById" "url.scheme", "https" ] ) .SetStartTime(t) .SetEndTime(t.AddMilliseconds 2.81) let t = now.AddMilliseconds 10.43 use basketQuery = basket .StartActivity( "GET", ActivityKind.Client, basketRoot.Context, dict [ "db.redis.database_index", box 0 "db.redis.flags", "None" "db.statement", "GET" "db.system", "redis" ] ) .SetStartTime(t) .SetEndTime(t.AddMilliseconds 1.35) basketQuery.Stop() basketRoot.Stop() basketClient.Stop() root.Stop()
これで投稿されるトレースはこんな感じになります。


さて、まぁまぁいい感じかとは思いますが、いちトレースでこの記述量だと大変ですね。 なので、去年のMackerel Advent Calendar 2024 で紹介した Mackerel DSLを作ろう - @yohfee.blog! のように、コンピュテーション式で DSL を作り始めたら、そっちの方が楽しくなって、見所の盛り込みが次の会に間に合わなくなってしまいました。というオチ。