Akka.Netでのアクターの監視、ただしF#

ハブにはF#のハブがないことをすぐに言わなければならないので、C#で記述します。



F#に精通していないが、C#に精通している人には、Microsoftの最新記事をお勧めします。

読むときにWTFが少なくなるのを助けます。 私の記事は構文のチュートリアルではありません。



タスクコンテキスト



Akka.NETで記述されたサービスがあり、さまざまなテキストログに大量の情報をダンプします。 運用部門はこれらのログにバグを付け、regexpでそれらをログに記録して、エラーの数(ビジネスではない)、サービスに含まれるメッセージの数、および発信メッセージの数について学習します。 さらに、この情報はElasticDB、InfluxDBに注がれ、GrafanaとKibanaで異なるスライスと集計で表示されます。



複雑に聞こえますが、1日に数十GBのテキストガベージを生成するサービスのテキストログを解析することは恩恵のない作業です。 そのため、タスクが発生しました-サービスはエンドポイントを上げることができるはずです。エンドポイントをプルして、それに関するすべての情報を一度に取得できます。



次のように問題を解決します。



  1. メトリクスのドメインモデルを書きましょう
  2. メトリックのドメインモデルをApp.Metricsの実装にマッピングし、Cookieボックスを上げます
  3. 構造化ドメインロガーを作成しましょう。これは、内部のAkkaロガーを使用します
  4. 機能的なアクターのラッパーを作成しましょう。これにより、メトリックとロガーを使用して作業が隠されます。
  5. すべてをまとめて実行する


メトリックのドメインモデル



App.Metricsには6種類のビューがあります。





最初のイテレーションでは、カウンター、タイマー、...メーターで十分です:)

最初に、タイプとインターフェースについて説明します(すべてを提供するわけではありません。リポジトリの最後のリンクを参照してください)。



また、メトリックに関するすべてのメッセージは、EventStream(Akka.Net自体のメッセージバス)を介して特別なアクター(後で定義します)に送られることに同意します。



たとえば、オブジェクトの特定の時間を測定できるはずのタイマー:



type IMetricsTimer = abstract member Measure : Amount -> unit abstract member Measure : Amount * Item -> unit
      
      





または、数量の指示がある場合とない場合の両方で増減できるカウンタ:



  type IMetricsCounter = abstract member Decrement : unit -> unit abstract member Decrement : Amount -> unit abstract member Decrement : Amount * Item -> unit abstract member Increment : unit -> unit abstract member Increment : Amount -> unit abstract member Increment : Amount * Item -> unit
      
      





バス用のコマンドの例:



  type DecrementCounterCommand = { CounterId : CounterId DecrementAmount : Amount Item : Item } type CreateCounterCommand = { CounterId : CounterId Context : ContextName Name : MetricName MeasurementUnit : MeasurementUnit ReportItemPercentages : bool ReportSetItems : bool ResetOnReporting : bool }
      
      





最も重要なことは、バスを通過する可能性のあるメッセージと、メトリックアクターが応答するメッセージを決定することです。 これを行うには、差別組合を使用します。



  type MetricsMessage = | DecrementCounter of DecrementCounterCommand | IncrementCounter of IncrementCounterCommand | MarkMeter of MarkMeterCommand | MeasureTime of MeasureTimeCommand | CreateCounter of CreateCounterCommand | CreateMeter of CreateMeterCommand | CreateTimer of CreateTimerCommand
      
      





ここで、インターフェイスを実装する必要があり、これが最初の段落の終わりです。 機能的なスタイルで実装します。 機能を通して。



メーターの作成例:



  let private createMeter (evtStream: EventStream) meterId = { new IMetricsMeter with member this.Mark amount = this.Mark (amount, Item None) member this.Mark item = this.Mark (Amount 1L, item) member this.Mark (amount, item) = evtStream.Publish <| MarkMeter { MeterId = meterId; Amount = amount; Item = item }
      
      





C#の世界の人々のために、私はアナログを与えます:



  private IMetricsMeter createMeter(EventStream evtStream, MeterId meterId) { private class TempClass : IMetricsMeter { public void Mark(long amount) { Mark(amount, ""); } public void Mark(string item) { Mark(1, item); } public void Mark(long amount, string item) { evtStream.Publish(new MarkMeter {...});//omitted } } return new TempClass(); }
      
      





アナログがコンパイルされないことを気にしないでください、これは正常です メソッドの本体にあるプライベートクラスは、コンパイラによって混乱します。 しかし、F#では、インターフェイスを介して匿名クラスを返すことができます。



注意すべき主なことは、メーターを移動する必要があるというメッセージをバスに投げることです。これは、MeterIdによって決定されます。



IMetricsAdapterでも同じことを行いますが、 彼には多くの方法があります。



  member this.CreateMeter (name, measureUnit, rateUnit) = let cmd = { MeterId = MeterId (toId name) Context = context Name = name MeasurementUnit = measureUnit RateUnit = rateUnit } evtStream.Publish <| CreateMeter cmd createMeter evtStream cmd.MeterId
      
      





タイマーの作成を要求すると、作成メッセージをバスに送信し、evtStreamとcmd.MeterIdを使用してcreateMeterメソッドの結果を呼び出し元に返します。

その結果、上記のように-IMetricsMeter。



その後、ActorSystemの拡張を作成して、どこからでもIMetricsAdapterを呼び出すことができるようにします。



  type IActorContext with member x.GetMetricsProducer context = createAdapter x.System.EventStream context
      
      





メトリックとブリーフケースのアクター



次の2つのアクターが必要です。





ApiControllerをすぐに実現します。簡単です。



  type public MetricController(metrics: IMetrics) = inherit ApiController() [<HttpGet>] [<Route("metrics")>] member __.GetMetrics() = __.Ok(metrics.Snapshot.Get())
      
      





次に、EventStreamからすべてのMetricsMessageを読み取り、それらで何かを実行するアクター関数を宣言します。 引数を使用して関数にIMetrics依存関係を実装します。内部では、通常の辞書を使用してすべてのメトリックのキャッシュを作成します。



なぜConcurrentDictionaryではないのですか? そして、アクターはメッセージを順番に処理するためです。 アクター内の競合状態をキャッチするには、意図的に自分の足を撃つ必要があります。



  let createRecorder (metrics: IMetrics) (mailbox: Actor<_>) = let self = mailbox.Self let counters = new Dictionary<CounterId, ICounter>() let meters = new Dictionary<MeterId, IMeter>() let timers = new Dictionary<TimerId, ITimer * TimeUnit>() //    ... let handle = function | DecrementCounter evt -> match counters.TryGetValue evt.CounterId with | (false, _) -> () | (true, c) -> let (Amount am) = evt.DecrementAmount match evt.Item with | Item (Some i) -> c.Decrement (i, am) | Item None -> c.Decrement (am) | CreateMeter cmd -> match meters.TryGetValue cmd.MeterId with | (false, _) -> let (ContextName ctxName) = cmd.Context let (MetricName name) = cmd.Name let options = new MeterOptions( Context = ctxName, MeasurementUnit = toUnit cmd.MeasurementUnit, Name = name, RateUnit = toTimeUnit cmd.RateUnit) let m = metrics.Provider.Meter.Instance options meters.Add(cmd.MeterId, m) | _ -> () //    match  subscribe typedefof<MetricsMessage> self mailbox.Context.System.EventStream |> ignore let rec loop() = actor { let! msg = mailbox.Receive() handle msg return! loop() } loop()
      
      





簡単な意味-彼らは異なるメトリックの辞書の形で内部状態を発表し、メッセージ処理関数MetricsMessageを発表し、MetricsMessageにサブスクライブし、メールボックスから再帰メッセージ処理関数を返しました。



メトリックを操作するためのメッセージは、次のように処理されます。



  1. どのメッセージを確認します(一致パターンを使用)
  2. このIDを持つこのディクショナリのメトリックを探しています(このため、FでTryGetValueを返すカップル(bool、obj)に美しいパターンがあります)
  3. これがメトリックの作成要求であり、そこにない場合-作成、辞書に追加
  4. これがメトリックを使用するリクエストであり、そうである場合、使用します


また、コントローラーが高いOwinホストを上げるアクターも必要です。

これを行うために、configおよびIDependencyResolverの形式で依存関係を取る関数を作成します。 開始時に失敗しないように、アクターは自分にメッセージを送信し、古いAPIの可能なDispose()と新しいAPIの作成を開始します。 そして再び、なぜなら それ自体の中のアクターは同期的であるため、可変状態を使用できます。



  type IMetricApiConfig = abstract member Host: string abstract member Port: int type ApiMessage = ReStartApiMessage let createReader (config: IMetricApiConfig) resolver (mailbox: Actor<_>) = let startUp (app: IAppBuilder) = let httpConfig = new HttpConfiguration(DependencyResolver = resolver) httpConfig.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new MetricDataConverter()) httpConfig.Formatters.JsonFormatter.Indent <- true httpConfig.MapHttpAttributeRoutes() httpConfig.EnsureInitialized() app.UseWebApi(httpConfig) |> ignore let uri = sprintf "http://%s:%d" config.Host config.Port let mutable api = {new IDisposable with member this.Dispose() = ()} let handleMsg (ReStartApiMessage) = api.Dispose() api <- WebApp.Start(uri, startUp) mailbox.Defer api.Dispose mailbox.Self <! ReStartApiMessage let rec loop() = actor { let! msg = mailbox.Receive() handleMsg msg return! loop() } loop()
      
      





また、mailbox.Deferを使用してアクターが最終的に停止したときに、api.Disposeメソッドを遅延タスクにスローします。 また、api変数の初期状態には、オブジェクト式を介してスタブを使用します。これにより、空のIDisposableオブジェクトが作成されます。



構造化ロガーの作成



タスクの意味は、Akka.Netからロガーのラッパーを作成することです(ILoggingAdapterインターフェイスを介して表示されます)。これを使用して、操作の時間と情報の入力ドリフト(鼻緒だけでなく、個別のビジネスケース)を測定できます。



ロガーのすべての入力は、1つの結合に囲まれています。



 type Fragment = | OperationName of string | OperationDuration of TimeSpan | TotalDuration of TimeSpan | ReceivedOn of DateTimeOffset | MessageType of Type | Exception of exn
      
      





ロガー自体はこのインターフェースで動作します:



 type ILogBuilder = abstract OnOperationBegin: unit -> unit abstract OnOperationCompleted: unit -> unit abstract Set: LogLevel -> unit abstract Set: Fragment -> unit abstract Fail: exn -> unit abstract Supress: unit -> unit abstract TryGet: Fragment -> Fragment option
      
      





通常のクラスを介して作成します。



 type LogBuilder(logger: ILoggingAdapter) = let logFragments = new Dictionary<System.Type, Fragment>() let stopwatch = new Stopwatch() let mutable logLevel = LogLevel.DebugLevel interface ILogBuilder with // 
      
      





おそらく、通常の辞書が必要なのはなぜでしょうか? 前述のように、このLogBuilderは、単一の操作を処理するときにアクター内で使用することを目的としています。 競争力のあるデータ構造を使用しても意味がありません。



インターフェースの実装方法の例を示します。



  let set fragment = logFragments.[fragment.GetType()] <- fragment member x.OnOperationBegin() = stopwatch.Start() member this.Fail e = logLevel <- LogLevel.ErrorLevel set <| Exception e member this.OnOperationCompleted() = stopwatch.Stop() set <| OperationDuration stopwatch.Elapsed match tryGet <| ReceivedOn DateTimeOffset.MinValue with | Some (ReceivedOn date) -> set <| TotalDuration (DateTimeOffset.UtcNow - date) | _ -> () match status with | Active -> match (logLevel) with | LogLevel.DebugLevel -> logger.Debug(message()) | LogLevel.InfoLevel -> logger.Info(message()) | LogLevel.WarningLevel -> logger.Warning(message()) | LogLevel.ErrorLevel -> logger.Error(message()) | x -> failwith(sprintf "Log level %s is not supported" <| string x) | Supressed -> ()
      
      





最も興味深いのは、OnOperationCompleted()のロジックです。





機能的なアクターのラッパーを作成する



最も魔法の部分。これにより、完全なロギングを実行する定型文なしで、単純なメッセージ処理関数を作成できます。



何を達成したいですか? まず、誓約したい:





上記のすべてを行うには、Linq.Expressionsが役立ちます。 F#のQuotationExpressionsでこれを行う方法はわかりません、tk。 それらをコンパイルする簡単な方法が見つかりませんでした。 誰かがオプションを提供してくれたら嬉しいです。



したがって、最初に、ヘルパータイプのペアと1つのメソッドを宣言します。



 type Expr<'T,'TLog when 'TLog :> ILogBuilder> = Expression<System.Action<Actor<'T>, 'T, 'TLog>> type Wrap = static member Handler(e: Expression<System.Action<Actor<'T>, 'T, #ILogBuilder>>) = e let toExprName (expr: Expr<_,_>) = match expr.Body with | :? MethodCallExpression as methodCall -> methodCall.Method.Name | x -> x.ToString()
      
      





Exprは、メールボックスからのアクション(子を生成する、自分自身または子を一般的に停止する必要がある場合)、処理中のメッセージ、およびロガー(特別なアクションを実行する必要がある場合)を含む式です。



Wrap.Handler(Expr)-「fun mb msg log->()」などの通常のF#式をそこに書き込み、出力でLinq.Expressionsを取得できます。



toExprNameは、式がメソッド呼び出し(MethodCallExpression)である場合、または単に式を文字列にキャストしようとしている場合に、メソッドの名前を取得するメソッドです。

「fun mb msg log-> handleMsg msg」のような式の場合、toExprNameは「handleMsg」を返します。



次に、ラッパーを作成して機能的なアクターを作成します。 広告の開始は次のようになります。



 let loggerActor<'TMsg> (handler: Expr<'TMsg,_>) (mailbox: Actor<'TMsg>) = let exprName = handler |> toExprName let metrics = mailbox.Context.GetMetricsProducer (ContextName exprName) let logger = mailbox.Log.Value
      
      





次のように、入力にハンドラーのみを送信します。 メールボックスはAkka自体(部分アプリケーション)によってスローされます。



作成したActorSystemの拡張機能を使用して、値メトリックでIMetricsAdapterのインスタンスを取得します。 また、ロガー値でAkkaロガーを取得します。



次に、このアクターに必要なすべてのメトリックを作成し、すぐに使用します。



  let errorMeter = metrics.CreateMeter (MetricName "Error Rate", Errors) let instanceCounter = metrics.CreateCounter (MetricName "Instances Counter", Items) let messagesMeter = metrics.CreateMeter (MetricName "Message Processing Rate", Items) let operationsTimer = metrics.CreateTimer (MetricName "Operation Durations", Requests, MilliSeconds, MilliSeconds) instanceCounter.Increment() mailbox.Defer instanceCounter.Decrement
      
      





ご覧のとおり、instanceCounterの値を増やし、アクターの停止時にこのカウンターの減少を設定します。



ロガーで知っているパラメーターを入力し、必要なメトリックを取得するメソッドがさらに必要です。



このコードでは、操作の名前をロガーにスローし、ログの終わりを呼び出し、操作時間をタイマーメトリックにドロップし、メッセージタイプをメッセージメトリックにドロップします。



  let completeOperation (msgType: Type) (logger: #ILogBuilder) = logger.Set (OperationName exprName) logger.OnOperationCompleted() match logger.TryGet(OperationDuration TimeSpan.Zero) with | Some(OperationDuration dur) -> operationsTimer.Measure(Amount (int64 dur.TotalMilliseconds), Item (Some exprName)) | _ -> () messagesMeter.Mark(Item (Some msgType.Name))
      
      





アクター内の例外を処理するには、次のメソッドが役立ちます。



  let registerExn (msgType: Type) e (logger: #ILogBuilder) = errorMeter.Mark(Item (Some msgType.Name)) logger.Fail e
      
      





それを機能させるために少し残っています。 ハンドラーのラッパーを介してすべてをバインドします。



  let wrapHandler handler mb (logBuilder: unit -> #ILogBuilder) = let innherHandler mb msg = let logger = logBuilder() let msgType = msg.GetType() logger.Set (MessageType msgType) try try logger.OnOperationBegin() handler mb msg logger with | e -> registerExn msgType e logger; reraise() finally completeOperation msgType logger innherHandler mb
      
      





wrapHandlerには複雑な署名があります。 C#では、次のようになります。



 Func<TMsg, TResult> wrapHandler<Tmsg, TResult, TLogBuilder, TMailbox>( Func<TMailbox, TMsg, TLogBuilder, TResult> handler, TMailbox mb, Func<TLogBuilder> logBuilder) where TLogBuilder: ILogBuilder
      
      





同時に、他のすべてのタイプに制限はありません



意味に関しては、wrapHandlerはTMsgを受信して​​TResultsを生成する関数を出力する必要があります。 この関数の手順は次のとおりです。





ExpressionをActionに変換し、ロガーの新しいインスタンスのアクターを各アクションに送信するには、別の補助関数を作成します。



  let wrapExpr (expr: Expr<_,_>) mailbox logger = let action = expr.Compile() wrapHandler (fun mb msg log -> action.Invoke(mailbox, msg, log)) mailbox (fun () -> new LogBuilder(logger))
      
      





その中で、Expressionを取得してコンパイルし、メールボックスと新しいLogBuilder()を取得する関数と共に、上記のwrapHandlerに送信します。



このメソッドの署名も単純ではありません。 C#では、次のようになります。



 Action<TMsg> wrapExpr<TMsg>( Expr<TMsg, LogBuilder> expr, Actor<TMsg> mb, ILoggingAdapterlogger)
      
      





TMsgにはまだ制限がありません。



再帰関数を作成するためだけに残っています:)

  let rec loop() = actor { let! msg = mailbox.Receive() wrapExpr handler mailbox akkaLogger msg return! loop() } loop()
      
      





この表現「wrapExprハンドラメールボックスakkaLogger」は、上記の説明からわかるように、Actionを返します。 任意のタイプを入力に送信してユニットを取得できるメソッド(c#のvoid)。



最後に式「msg」を追加したら、msg引数をこの関数にスローし、受信したメッセージに対してアクションを実行します。



タスクをコーディングしてこれを終了し、例に進みます!



これをすべて開始する方法は?



これが機能するためには、多くのコードを書く必要はありません。

通常、メールボックス、ロガー、またはエラー処理が必要であることを知らなくても、メッセージハンドラーのみを記述することができます。



簡単な場合は次のようになります。



 type ActorMessages = | Wait of int | Stop let waitProcess = function | Wait d -> Async.Sleep d |> Async.RunSynchronously | Stop -> ()
      
      







そして、この関数をloggerActorでラップし、私たちが一生懸命努力したすべてのグッズを取得するには、次のように記述できます。



 let spawnWaitWorker() = loggerActor <| Wrap.Handler(fun mb msg log -> waitProcess msg) let waitWorker = spawn system "worker-wait" <| spawnWaitWorker() waitWorker <! Wait 1000 //    ~1000 waitWorker <! Wait 500
      
      





複雑なロジックがあり、メールボックスとロガーにアクセスする必要がある場合:



 let failOrStopProcess (mailbox: Actor<_>) msg (log: ILogBuilder) = try match msg with | Wait d -> failwith "can't wait!" | Stop -> mailbox.Context.Stop mailbox.Self with | e -> log.Fail e let spawnFailOrStopWorker() = loggerActor <| Wrap.Handler(fun mb msg log -> failOrStopProcess mb msg log) let failOrStopWorker = spawn system "worker-vocal" <| spawnFailOrStopWorker() failOrStopWorker <! Wait 1000 //   "can't wait!" failOrStopWorker <! Wait 500 //   "can't wait!" failOrStopWorker <! Stop failOrStopWorker <! Wait 500 //     DeadLetters
      
      





プログラム自体のEntryPoint、ActorSystemの作成、メトリックとアクターの引き上げは、ネタバレの下で見ることができますが、目立ったものはありません。



Program.fs
 open Akka.FSharp open SimpleInjector open App.Metrics; open Microsoft.Extensions.DependencyInjection open SimpleInjector.Integration.WebApi open System.Reflection open System open Metrics.MetricActors open ExampleActors let createSystem = let configStr = System.IO.File.ReadAllText("system.json") System.create "system-for-metrics" (Configuration.parse(configStr)) let createMetricActors system container = let dependencyResolver = new SimpleInjectorWebApiDependencyResolver(container) let apiConfig = { new IMetricApiConfig with member x.Host = "localhost" member x.Port = 10001 } let metricsReaderSpawner = createReader apiConfig dependencyResolver let metricsReader = spawn system "metrics-reader" metricsReaderSpawner let metricsRecorderSpawner = createRecorder (container.GetInstance<IMetrics>()) let metricsRecorder = spawn system "metrics-recorder" metricsRecorderSpawner () type Container with member x.AddMetrics() = let serviceCollection = new ServiceCollection() let entryAssemblyName = Assembly.GetEntryAssembly().GetName() let metricsHostBuilder = serviceCollection.AddMetrics(entryAssemblyName) serviceCollection.AddLogging() |> ignore let provider = serviceCollection.BuildServiceProvider() x.Register(fun () -> provider.GetRequiredService<IMetrics>()) [<EntryPoint>] let main argv = let container = new Container() let system = createSystem container.RegisterSingleton system container.AddMetrics() container.Verify() createMetricActors system container let waitWorker1 = spawn system "worker-wait1" <| spawnWaitWorker() let waitWorker2 = spawn system "worker-wait2" <| spawnWaitWorker() let waitWorker3 = spawn system "worker-wait3" <| spawnWaitWorker() let waitWorker4 = spawn system "worker-wait4" <| spawnWaitWorker() let failWorker = spawn system "worker-fail" <| spawnFailWorker() let waitOrStopWorker = spawn system "worker-silent" <| spawnWaitOrStopWorker() let failOrStopWorker = spawn system "worker-vocal" <| spawnFailOrStopWorker() waitWorker1 <! Wait 1000 waitWorker2 <! Wait 500 waitWorker3 <! Wait 5000 waitWorker4 <! Wait 8000 failWorker <! Wait 5000 waitOrStopWorker <! Wait 1000 waitOrStopWorker <! Wait 500 waitOrStopWorker <! Stop waitOrStopWorker <! Wait 500 failOrStopWorker <! Wait 1000 failOrStopWorker <! Wait 500 failOrStopWorker <! Stop failOrStopWorker <! Wait 500 Console.ReadKey() |> ignore 0
      
      







最も重要なことはメトリックです!



操作中にlocalhost:10001 / metricsリンクに移動すると、十分な大きさのjsonが表示され、そこには多くの情報があります。 waitProcess関数の一部を次に示します。



非表示のテキスト
 { "Context": "waitProcess", "Counters": [ { "Name": "Instances Counter", "Unit": "items", "Count": 4 } ], "Meters": [ { "Name": "Message Processing Rate", "Unit": "items", "Count": 4, "FifteenMinuteRate": 35.668327519112893, "FiveMinuteRate": 35.01484385742755, "Items": [ { "Count": 4, "FifteenMinuteRate": 0.0, "FiveMinuteRate": 0.0, "Item": "Wait", "MeanRate": 13.082620551464204, "OneMinuteRate": 0.0, "Percent": 100.0 } ], "MeanRate": 13.082613248856632, "OneMinuteRate": 31.356094372926623, "RateUnit": "min" } ], "Timers": [ { "Name": "Operation Durations", "Unit": "req", "ActiveSessions": 0, "Count": 4, "DurationUnit": "ms", "Histogram": { "LastUserValue": "waitProcess", "LastValue": 8001.0, "Max": 8001.0, "MaxUserValue": "waitProcess", "Mean": 3927.1639786164278, "Median": 5021.0, "Min": 1078.0, "MinUserValue": "waitProcess", "Percentile75": 8001.0, "Percentile95": 8001.0, "Percentile98": 8001.0, "Percentile99": 8001.0, "Percentile999": 8001.0, "SampleSize": 4, "StdDev": 2932.0567172627871, "Sum": 15190.0 }, "Rate": { "FifteenMinuteRate": 0.00059447212531854826, "FiveMinuteRate": 0.00058358073095712587, "MeanRate": 0.00021824579927905906, "OneMinuteRate": 0.00052260157288211038 } } ] }
      
      







それからあなたはそれを見つけることができます:





コンソールは次のようになります。



おわりに



この記事には多くのコードがあり、説明はほとんどありません(明確でない場合はコメントで答えます)が、これはこの記事が実際のプロジェクトのいくつかの日常的なタスクの解決策を示すことを目的としているためです。



特にこのコードは元々C#のアクター向けに書かれているため、誰かが役に立つかもしれません。必要に応じてこれをすべて転送できます(ヒントをお伝えします。



複雑なドメインモデルのモデリングに携わっている人には、 型システムははるかに豊富であり、nullと型の設計がないため、モデルをプログラマーのエラーに耐えることができます。



例のあるリポジトリはこちらです。



ご清聴ありがとうございました!



All Articles