Использование TraceSource в многопоточной среде

Я написал приложение на F# с использованием асинхронных рабочих процессов. Теперь я хотел бы добавить немного трассировки к нему!

В основном существует класс A, который может быть создан несколько раз. Каждый из экземпляров работает независимо от асинхронного (сам по себе) и параллельного (с другими). Моя основная идея сейчас состоит в том, чтобы добавить экземпляр TraceSource для каждого экземпляра A, что, скорее всего, то, что я хочу сделать. Мне удалось решить проблему распространения TraceSource с объектами Async через https://github.com/matthid/fsharpasynctrace

Однако если каждому экземпляру TraceSource присвоено одно и то же имя, некоторые из них будут записаны в один и тот же файл (log.txt), а другие будут записаны в {guid} log.txt.

Если я даю каждому экземпляру другое имя, пользователь должен отредактировать файл app.config, чтобы получить правильное ведение журнала. Каждый экземпляр A имеет логическое имя, данное пользователем, поэтому в идеале я бы сохранил журнал для экземпляра в name_log.txt. (Это потому, что пользователь в основном создает экземпляры A во время выполнения)

Итак, мой вопрос: есть ли лучший способ сделать это, то есть без взаимодействия с пользователем и получения желаемого результата и гибкости (через app.config)?

Примечание. Поскольку в основном все находится в пуле потоков, и поскольку одновременно может быть много действий между экземплярами, трассировка классов или потоков вообще не подходит.

Примечание 2: я могу подумать о том, чтобы каким-то образом расширить app.config и сделать это самостоятельно, это мой единственный вариант?

РЕДАКТИРОВАТЬ: Чтобы сделать вопрос более ясным:

Представьте себе следующий класс:

module OtherModule = 
    let doSomethingAsync m = async{return()}
[<AbstractClass>]
type A (name:string) as x = 
    let processor = 
        MailboxProcessor.Start(
            fun inbox -> async {
                while true do
                    let! msg = inbox.Receive()
                    do! x.B(msg)
                    do! OtherModule.doSomethingAsync(msg)})
    abstract member B : string -> Async<unit>
    member x.Do(t:string) = processor.Post(t)

У вас есть много экземпляров этого класса, и каждый экземпляр живет очень долго. У вас сейчас ситуация описанная выше. (Вы также хотите отследить абстрактный член, что может быть сделано защищенным трассирующим источником... который недоступен в F#. И вы хотите отследить некоторые функции модуля. Вот почему я выбрал вышеупомянутую модель распределения. Если вы это делаете любым другим способом вам будет нелегко просматривать журналы.)

3 ответа

Решение

Я не проверял это, но похоже, что это будет работать. TraceXXX методы на TraceSource принять id параметр. Как насчет использования этого в качестве "идентификатора экземпляра"? Затем вы можете написать собственный слушатель трассировки, чтобы перенаправить вывод на основе этого идентификатора. Может быть, это послужит отправной точкой:

type MultiOutputTraceListener(directory) =
  inherit TraceListener()

  let mutable output : TextWriter = null
  let writers = Dictionary()

  let setOutput (id: int) =
    lock writers <| fun () ->
      match writers.TryGetValue(id) with
      | true, w -> output <- w
      | _ ->
        let w = new StreamWriter(Path.Combine(directory, id.ToString() + ".log"))
        writers.Add(id, w)
        output <- w

  override x.Write(msg: string) = output.Write(msg)
  override x.WriteLine(msg: string) = output.WriteLine(msg)

  override x.TraceData(eventCache, source, eventType, id, data: obj) =
    setOutput id
    base.TraceData(eventCache, source, eventType, id, data)

  override x.TraceData(eventCache, source, eventType, id, data) =
    setOutput id
    base.TraceData(eventCache, source, eventType, id, data)

  override x.TraceEvent(eventCache, source, eventType, id, message) =
    setOutput id
    base.TraceEvent(eventCache, source, eventType, id, message)

  override x.TraceEvent(eventCache, source, eventType, id, format, args) =
    setOutput id
    base.TraceEvent(eventCache, source, eventType, id, format, args)

  override x.Dispose(disposing) =
    if disposing then
      for w in writers.Values do
        w.Dispose()

использование

module Tracing =
  let Source = TraceSource("MyTraceSource")

type A(id) =
  member x.M() =
    Tracing.Source.TraceEvent(TraceEventType.Verbose, id, "Entering method M()")
    ...

let a1 = A(1)
let a2 = A(2)

Ваше решение выглядит довольно интересно, но я думаю, что использование собственного рабочего процесса на основе async просто обойти объект, используемый для отслеживания, может быть излишним.

Я, вероятно, попытался бы использовать агенты F# - вы могли бы создать TracingAgent с такими методами, как Error, Warning а также Trace сообщать об отдельных видах сообщений. Когда вы инициализируете агент, вы можете указать, какой файл он должен использовать. Когда вы вызываете агента из нескольких потоков, это нормально, потому что агенты сериализуют сообщения по мере их обработки.

Итак, ваш код пользователя будет выглядеть так:

let tracer = TracingAgent("Workflow 01")

let doSomeThingInner v = async {
    tracer.Critical "CRITICAL! %s" v
    return "ToOuter" }

let testIt () = async {
    tracer.Verbose "Verbose!" 
    let! d = doSomeThingInner "ToInner"
    tracer.Warning "WARNING: %s" d }

testIt () |> Async.RunSynchronously

Таким образом, вам придется обойти tracer объекты сами по себе, но это не должно быть проблемой, потому что обычно вы используете небольшое количество глобальных трассировщиков. Если вы хотите изменить выходной файл по какой-либо причине, вы можете добавить сообщение своему агенту для этого.

Структура агента будет выглядеть примерно так:

type TracingAgent(log) = 
  let inbox = MailboxProcessor.Start(fun inbox -> async {
    while true do
      let! msg = inbox.Receive()
      // Process the message - write to a log
    })
  // Methods that are used to write to the log file
  member x.Warning fmt = 
    Printf.kprintf (fun str -> inbox.Post(Warning(str))) fmt

  // Optionally a method that changes the log file
  member x.ChangeFile(file) = 
    inbox.Post(ChangeFile(file))

Конфигурация регистрации может быть загружена из файла конфигурации - я полагаю, логическое место для этого было бы внутри TracingAgent,

После некоторого тестирования и обдумывания ответов я пришел к следующему решению:

type ITracer = 
    inherit IDisposable
    abstract member log : Diagnostics.TraceEventType ->Printf.StringFormat<'a, unit> -> 'a


type ITracer with
    member x.logVerb fmt = x.log System.Diagnostics.TraceEventType.Verbose fmt
    member x.logWarn fmt = x.log System.Diagnostics.TraceEventType.Warning fmt
    member x.logCrit fmt = x.log System.Diagnostics.TraceEventType.Critical fmt
    member x.logErr fmt =  x.log System.Diagnostics.TraceEventType.Error fmt
    member x.logInfo fmt = x.log System.Diagnostics.TraceEventType.Information fmt

type MyTraceSource(traceEntry:string,name:string) as x= 
    inherit TraceSource(traceEntry)
    do 
        let newTracers = [|
            for l in x.Listeners do
                let t = l.GetType()
                let initField =
                    t.GetField(
                        "initializeData", System.Reflection.BindingFlags.NonPublic ||| 
                                          System.Reflection.BindingFlags.Instance)
                let oldRelFilePath =
                    if initField <> null then
                         initField.GetValue(l) :?> string
                    else System.IO.Path.Combine("logs", sprintf "%s.log" l.Name)

                let newFileName =
                    if oldRelFilePath = "" then ""
                    else
                        let fileName = Path.GetFileNameWithoutExtension(oldRelFilePath)
                        let extension = Path.GetExtension(oldRelFilePath)
                        Path.Combine(
                            Path.GetDirectoryName(oldRelFilePath),
                            sprintf "%s.%s%s" fileName name extension)
                let constr = t.GetConstructor(if newFileName = "" then [| |] else [| typeof<string> |])
                if (constr = null) then 
                    failwith (sprintf "TraceListener Constructor for Type %s not found" (t.FullName))
                let listener = constr.Invoke(if newFileName = "" then [| |]  else [| newFileName |]) :?> TraceListener
                yield listener |]
        x.Listeners.Clear()
        x.Listeners.AddRange(newTracers)

type DefaultStateTracer(traceSource:TraceSource, activityName:string) = 
    let trace = traceSource
    let activity = Guid.NewGuid()
    let doInId f = 
        let oldId = Trace.CorrelationManager.ActivityId
        try
            Trace.CorrelationManager.ActivityId <- activity
            f()
        finally
            Trace.CorrelationManager.ActivityId <- oldId
    let logHelper ty (s : string) =  
        doInId 
            (fun () ->
                trace.TraceEvent(ty, 0, s)
                trace.Flush())
    do 
        doInId (fun () -> trace.TraceEvent(TraceEventType.Start, 0, activityName);)

    interface IDisposable with
        member x.Dispose() = 
            doInId (fun () -> trace.TraceEvent(TraceEventType.Stop, 0, activityName);)

    interface ITracer with 
        member x.log ty fmt = Printf.kprintf (logHelper ty) fmt  

На самом деле я нашел и решение, не зависящее от рефлексии: вы сами наследуете все важные TraceListener и выставляете данные, с которыми они инициализированы. Затем вы создаете соответствующие прослушиватели с измененными данными в конструкторе MyTraceSource.

Изменить: решение без отражения не является таким общим, как выше с отражением.

Использование это так:

let SetTracer tracer (traceAsy:AsyncTrace<_,_>) = 
    traceAsy.SetInfo tracer
    traceAsy |> convertToAsync

module OtherModule = 
    let doSomethingAsync m = asyncTrace() {
        let! (tracer:ITracer) = traceInfo()
        return()
        }

[<AbstractClass>]
type A (name:string) as x = 

    let processor = 
        let traceSource = new MyTraceSource("Namespace.A", name)
        MailboxProcessor.Start(
            fun inbox -> async {
                while true do
                    let tracer = new DefaultStateTracer(traceSource, "Doing activity Foo now") :> ITracer
                    let! msg = inbox.Receive()
                    let w = x.B(msg) |> SetTracer tracer
                    do! OtherModule.doSomethingAsync(msg) |> SetTracer tracer})
    abstract member B : string -> AsyncTrace<ITracer, unit>
    member x.Do(t:string) = processor.Post(t)

Если вы настроили в app.config "logs\Namespace.A.log", вы получите файлы типа "logs\Namespace.A.name.log".

ПРИМЕЧАНИЕ: вам все еще нужно скопировать другие свойства, которые вы можете настроить через app.config, но это должно быть легко завершить сейчас.

Если вы считаете, что это неправильный способ сделать это, пожалуйста, оставьте комментарий.

РЕДАКТИРОВАТЬ: добавил это решение для отслеживания на https://github.com/matthid/fsharpasynctrace.

Другие вопросы по тегам