以下の内容はhttps://yohfee.hatenadiary.org/entry/2025/12/09/000000より取得しました。


Mackerel APM のデモを支えなかった技術

Mackerel Advent Calendar 2025 9日目だよ〜

qiita.com

こんにちは、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 された ActivitySourceStartActivity で、任意の属性を設定してスパンを開始し、開始終了時刻も辻褄が合う程度に適当に指定します。 子スパンとしたいものには親としたいスパンのコンテキストを渡すことで親子関係を表現できます。

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()

これで投稿されるトレースはこんな感じになります。

リソースが設定した通り Ruby の shop サービス V2 になっている

属性が設定した通り gRPC の呼び出しになっている

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




以上の内容はhttps://yohfee.hatenadiary.org/entry/2025/12/09/000000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14