Использование 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.