gcanti / logging-ts

Composable loggers for TypeScript
https://gcanti.github.io/logging-ts/
MIT License
98 stars 8 forks source link

Examples of composing loggers #20

Closed waynevanson closed 4 years ago

waynevanson commented 4 years ago

I'm thinking about how to compose these loggers with other functions.

Let's say we have the following scenario:

We see the pattern, each action has a logger.

What would you try to do to make it happen? I don't need code, just the name of some tools and techniques to look through.

The current though i had was to run these in parallel.

type LogParallel1 = [TaskEither<NodeJS.ErrnoException, A>, IO<void>]

We can see that these could all be lifted into TaskEither, but not sure what a good signature should be.

I'll be happy to write an example for the docs once I figure it out.

gcanti commented 4 years ago

I'd go for a combinator like

import * as TE from 'fp-ts/lib/TaskEither'
import * as L from 'logging-ts/lib/IO'

export const withLogger = (logger: L.LoggerIO<string>) => <A>(
  message: (a: A) => string
): (<E>(ma: TE.TaskEither<E, A>) => TE.TaskEither<E, A>) => TE.chainFirst((a) => TE.fromIO(logger(message(a))))

Example

declare const read: (path: string) => TE.TaskEither<Error, string>
declare const parse: (content: string) => TE.TaskEither<Error, string>
declare const reverse: (content: string) => TE.TaskEither<Error, string>
declare const write: (path: string) => (content: string) => TE.TaskEither<Error, void>

import { pipe, flow } from 'fp-ts/lib/function'

export const program = pipe(read('in'), TE.chain(parse), TE.chain(reverse), TE.chain(write('out')))

import * as C from 'fp-ts/lib/Console'

const log = withLogger(C.log)

export const programWithLogging = pipe(
  read('in'),
  log(() => 'file accessed!'),
  TE.chain(parse),
  log(() => 'file contents parsed!'),
  TE.chain(reverse),
  log(() => 'contents reversed'),
  TE.chain(write('out')),
  log(() => 'file has been saved!')
)

alternatively, with the same harness, you can enrich the base operations

const readWithLog = flow(
  read,
  log(() => 'file accessed!')
)
const parseWithLog = flow(
  parse,
  log(() => 'file contents parsed!')
)
// etc...
waynevanson commented 4 years ago

Wow, all the little details in this implementation are genius!

I think a combinator to create the combinator you suggested would be a cool idea, so any HKT that is above IO can be lift the logger to it. Not sure if it's possible or not, but I'll check.

Thank you so much.

waynevanson commented 4 years ago

Here's something that is compatible with the MonadIO2 monad:

import { MonadIO2 } from "fp-ts/lib/MonadIO";
import { URIS2, Kind2 } from "fp-ts/lib/HKT";

import { ioEither as IOE } from "fp-ts";
import * as L from "logging-ts/es6/IO";

const chainFirst = <F extends URIS2>(I: MonadIO2<F>) => <E, A, B>(
  ma: Kind2<F, E, A>,
  f: (a: A) => Kind2<F, E, B>
) => I.chain(ma, (a) => I.map(f(a), () => a));

export const withLogger = <F extends URIS2>(I: MonadIO2<F>) => <E, A>(
  logger: L.LoggerIO<string>
) => <A>(message: (a: A) => string) => (ma: Kind2<F, E, A>) =>
  chainFirst(I)(ma, (a) => I.fromIO(logger(message(a))));

const teLog = withLogger(TE.taskEither)(C.log);
const ioeLog = withLogger(IOE.ioEither)(C.log);

I wouldn't mind seeing something like this added to the library, but with all the MonadIO[x] overloads.

gcanti commented 4 years ago

I wouldn't mind seeing something like this added to the library, but with all the MonadIO[x] overloads.

Would you like to send a PR?

waynevanson commented 4 years ago

I'd love to. Does the implementation stay the same and we just add function overloads?

gcanti commented 4 years ago

Does the implementation stay the same and we just add function overloads?

Yes, though I would

export function withLogger<M>(
  M: MonadIO<M>
): <B>(logger: L.LoggerIO<B>) => <A>(message: (a: A) => B) => (ma: HKT<M, A>) => HKT<M, A> {
  return (logger) => (message) => (ma) => M.chain(ma, (a) => M.map(M.fromIO(logger(message(a))), () => a))
}
matthewpflueger commented 4 years ago

I've been wanting to integrate logger-ts into my toolbelt but haven't been able to get my head wrapped around using it especially with third-party loggers like pino. This looks like it could help!

One other thing I'm having trouble with is how to log the Error (the M in HKT<M, A>)? Maybe this is obvious but any pointers would be great!

waynevanson commented 4 years ago

@matthewpflueger Do you mean the E in Either<E,A>? If not, I don't understand what you mean and need some clarification.

You could use the described withLogger function with Task instead of TaskEither, or IO instead of IOEither. This way you can have a logger like ~LoggerIO<E, A>~ LoggerIO<Either<E, A>> and you can handle the left and right within the logger.

matthewpflueger commented 4 years ago

Yeah, I mean the E in Either<E, A>. I'm a little confused - how would you log the error (the E)?

waynevanson commented 4 years ago

Let's assume we're in TaskEither<E, A>.

@matthewpflueger

This code should be all you need.

import { URIS3, Kind3, URIS2, Kind2, URIS, Kind, HKT } from "fp-ts/lib/HKT";
import {
  MonadIO3,
  MonadIO2C,
  MonadIO2,
  MonadIO1,
  MonadIO,
} from "fp-ts/lib/MonadIO";
import { LoggerIO } from "logging-ts/lib/IO";
import { task as T, either as E, console as C } from "fp-ts";

/**
 * @category Combinator
 *
 * @since 0.3.4
 *
 * @example
 * import { pipe } from 'fp-ts/lib/pipeable'
 * import * as IO from 'fp-ts/lib/IO'
 * import * as C from 'fp-ts/lib/Console'
 * import { withLogger } from 'logging-ts/lib/IO'
 * import { equal } from 'assert'
 *
 * const log = withLogger(IO.io)(C.log)
 *
 * const result = pipe(
 *   IO.of(3),
 *   log(n => `lifted "${n}" to the IO monad`), // n === 3
 *   IO.map(n => n * n),
 *   log(n => `squared the value, which is "${n}"`), // n === 9
 * )
 *
 * equal(result(), 9)
 */
export function withLogger<M extends URIS3>(
  M: MonadIO3<M>
): <B>(
  logger: LoggerIO<B>
) => <A>(
  message: (a: A) => B
) => <R, E>(ma: Kind3<M, R, E, A>) => Kind3<M, R, E, A>;
export function withLogger<M extends URIS2, E>(
  M: MonadIO2C<M, E>
): <B>(
  logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: Kind2<M, E, A>) => Kind2<M, E, A>;
export function withLogger<M extends URIS2>(
  M: MonadIO2<M>
): <B>(
  logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => <E>(ma: Kind2<M, E, A>) => Kind2<M, E, A>;
export function withLogger<M extends URIS>(
  M: MonadIO1<M>
): <B>(
  logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: Kind<M, A>) => Kind<M, A>;
export function withLogger<M>(
  M: MonadIO<M>
): <B>(
  logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: HKT<M, A>) => HKT<M, A>;
export function withLogger<M>(
  M: MonadIO<M>
): <B>(
  logger: LoggerIO<B>
) => <A>(message: (a: A) => B) => (ma: HKT<M, A>) => HKT<M, A> {
  return (logger) => (message) => (ma) =>
    M.chain(ma, (a) => M.map(M.fromIO(logger(message(a))), () => a));
}

export const logEA = withLogger(T.taskSeq)(E.fold(C.error, C.log));

Now you can do it like:

pipe(
  TE.right("value"),
  TE.map((a) => a),
  logEA(
    E.bimap(
      (e) => `Error! value is "${e}".`,
      (a) => `Success! The value is "${a}"`
    )
  ),
  TE.map((a) => a)
);