Declare your interfaces in a MyIf.fs and its module in MyIfModule.fs with a ([CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)])).
Place these files as high as possible. Place the module as close to and below the non-module file as possible.
If it's plausible that one would like to use point-free programming with your functions, place them in a module. (E.g. Message versus MessageModule)
Factory methods go on the types for value types. For stateful objects they go on the module, named create which may or may not return a Job of some public state type with a private/internal constructor. (E.g. PointName versus Engine)
All implementations go in a plural namespace. E.g. Logary.Metrics.Ticked has:
Another example: Logary.Target , which is a module that implements logic about the life-cycle of target instances. It's used to start/stop/pause and shutdown targets. It's singular.
Things that end-users of the library are not likely to configure should go in Logary.Internals . Examples include Logary.Internals.Globals and Logary.Internals.RuntimeInfo (which is configured with the config API instead).
The RuntimeInfo and internal Logger should be propagated to the modules and objects that you build your solution out of. This guideline is mostly about the registry's implementation and its interaction with config and services.
Generally, keep your functions to a single responsibility and compose functions instead of extending existing functions. Composition happens through currying and partial application of 'state' or 'always-this-value' values. For state it can both go through the Services abstraction (start/pause/stop/etc) or by closing over the state with a function.
If you find that your functions are getting larger than 3 lines of code, you should probably extract part of the function. By 'default' it's better to be 1% less performant but 5% more readable, after this list of priorities:
Prefer to open a namespace over fully qualifying a type.
Prefer to open fully qualified over partial.
open Logary.Internals open Logary.Internals.Supervisor
open Logary.Internals open Supervisor
A module function like MyModule.create : Conf -> Job[T] should not start the server loop. Instead, just return a cold job (that can be started multiple time) and let the composition "root", such as the Registry , perform the composition and lifetime handling.
Are you thinking of creating a new Target for Logary? It's a good idea if you can't find the right Target for your use case. It can also be useful if you have an internal metrics or log message engine in your company you wish to ship to.
When writing the Target, it's useful to keep these guidelines in mind.
RingBuffer.takeor in batches with
willChanneland throw an exception. Your target will be re-instantiated with the batch and you can now send the messages one-by-one to your target, throwing away poison messages (things that always crash).
RingBufferwill be gone, unless you send it to thewill channel.
RuntimeInfo.serviceNameas passed to your loop function.
RuntimeInfocontains a simple internal logger that you can assume always will accept your Messages. It allows you to debug and log exceptions from your target. By default it will write output to the STDOUT stream.
Alt<Promise<unit>>as returned from
logWithAck, will block forever if your target ever crashes.
Logary.Utils.Aether, which are vendored copies ofChiron and Aether. Have a look at theLogstash Target for an example.
When your Target is finished, either ping @haf on github, @henrikfeldt on twitter, or send a PR to this README with your implementation documented. I can assist in proof-reading your code, to ensure that it follows the empirical lessons learnt operating huge systems with Logary.