Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging Prism's decisions. #323

Merged
merged 117 commits into from Jun 18, 2019
Merged

Logging Prism's decisions. #323

merged 117 commits into from Jun 18, 2019

Conversation

XVincentX
Copy link
Contributor

@XVincentX XVincentX commented May 27, 2019

All right, be prepared.

TL; DR

  1. Prism can now log stuff from the negotiation.
  2. The negotiation and logging process can't make Prism crash anymore for any reason.

Long version

The following PR, in theory, implements logging for the negotiator. Practically, though, this PR is laying down the foundation for the logging for the future hosted version as well and hopefully start moving Prism's code in a different direction.

There were some main principles I've been keeping in mind while writing the code for this stuff:

  1. Prism should not process/format/react to the logs. Somebody else should do it; doing stuff on logs is usually slow and Prism should instead answer all the requests as fast as possible.
  2. The negotiation process in Prism is currently composed by numerous and nested function calls and last thing I wanted to do is to carry over this logger parameter in all the function and having to deal with it everywhere
  3. The logging cannot be a singleton defined somewhere — because the logging instance will be provided externally (Prism Http Server, in this case)
  4. The logging process and the negotiation process should never make Prism crash. In any circumstance. I repeat, in ANY circumstances.

Let's now see how I've tamed them down.

  1. Use Pino. If you go on their website the write that "it's the fastest logger on the world" but you can ignore that, that was not the reason why I went with it. The main points were:
    • It's included with fastify — and so we're not introducing a new dependency. We've been shipping it since forever.
    • Pino does not include any way to process/alert/react to logs; it simply outputs the log statements as JSON on the stdout (or any stream you provide to it). It means that all the processing must be off process and I think this is great. It means that, by design, there's no way somebody can make messes with logs and do any action that's logging dependant. You can't break what's not even included.

So where's the log processing happening in our case? In the CLI. You can see from the code that, whenever the production environment is detected (or the -m flag is passed) the CLI will effectively use the cluster module to fork the process, run Prism Server and funnel its stdout logs back to the CLI, where Signale will pretty print them. When Prism will be hosted somewhere, we'll likely do the same.

In case you're running the things locally to write code and test the stuff, you'll see that the fork is avoided and all is done in process. This is done fundamentally because it's going to be way easier to debug stuff in a single process. This is exactly the same that Jest does with the --runInBand flag.

image

You can see from the image what when the -m flag is specified, you'll find an additional process with node running — this is exactly the forking happening.

Note: if NODE_ENV===production the forking is enabled by default, so that if you use the CLI in a Docker Environment you get what the production behaviour.

  1. Inject the logger through currying. Thanks to the partial application I can basically write a function that takes the regular arguments which returns a function needing an additional dependency to run.
    const negotiate = (arg1, arg2, arg3) => logger => { // real body }. For this, I've employed fp-ts's reader implementation, that allows me to do exactly that: write code that has regular arguments and returns something that will need a logger to be executed. By leveraging chain and map method, intermediate functions can easily introspect and modify the result in the middle without having to worry about the logging presence. It'll be put as last step. Moreover, if one day we will want to make the Router start outputting logs as well — it's really easy. All we need to do is make the router spit up a Reader and chain it with the following one in the mocker. All good, profit.

  2. See point 2. With Currying I can pretend the inner functions will eventually have a logger, it's resolution is happening in a completely different npm package; this is important to understand why I could not simply have a shared instance to require from somewhere.

  3. This happened as an effect of the reader. Since the logger in injected at the end, the traditional try catch in the middle of the code to decorate the errors does not work anymore. I could have workaround this but I decided instead to change the approach and make sure the negotiation and the logging process never throws exceptions. Therefore the code has been modified to use the Either implementation that's included in fp-ts, which is not that hard to understand. If you check out the NegotiatorHelper.ts you can see there are not that many changes and more importantly nested exceptions are now been flattened — which is good. Clearly there are 1-2 things that aren't that cool but I'm on all of them. In particular:


Outstanding points

  • Refactor mock.ts to look a little bit more human
  • Resolve the null-ness of logger component
  • Understand whether we're logging enough or too much
  • CLI Readme Update
  • Deduplicate the testing helpers
  • General review and cleanup

SO-231

@XVincentX XVincentX force-pushed the feat/logs branch 2 times, most recently from b1132c8 to ef47888 Compare May 27, 2019 14:20
@XVincentX XVincentX marked this pull request as ready for review May 27, 2019 16:13
@XVincentX XVincentX force-pushed the feat/logs branch 3 times, most recently from 31cf3b3 to a8077dd Compare May 28, 2019 11:43
@XVincentX XVincentX changed the title Logging Prism Logging Prism's decisions. May 28, 2019
@@ -0,0 +1,33 @@
import * as pino from 'pino';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General inquiry: does pino work in browser context? Remember that one of the Prism's requirements is to be able to work both in nodejs and browser (e.g. we want to use it in request maker).

It caught my attention because I wasn't sure if core is actually a good place to store this stuff in. It looks that pino is mostly CLI specific (?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Yes! http://getpino.io/#/docs/browser

  2. Not entirely correct: the CLI is the place where the logs get captured from the stream and processed/visualised, but its usage spread in the whole application. Moreover, Pino implements the log4j interface so it should be, if needed, easily replaceable with anything we like in the future (Winston, Buyan). Does that clarify?

packages/http/package.json Show resolved Hide resolved
philsturgeon
philsturgeon previously approved these changes Jun 18, 2019
Copy link
Contributor

@chris-miaskowski chris-miaskowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a first (but not final) look. It really looks good but there are two new libraries I'm completely unfamiliar with and need to take another closer look at it.

See if any of the comments I made make sense.

packages/core/src/factory.ts Show resolved Hide resolved
},
httpContent,
)
.map(contentNegotiationResult => ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooooh! It's the Either's map! I got so confused by this. I thought you somehow ended up mapping arrays here... Very confusing. Not sure what to suggest here. Is there any function other than map that we could use?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm.. why would mapping on Either be confusing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand @chris-miaskowski's confusion. I had the same when I started to look into this in November.

Thing is the map we use on daily basis on an arrays and objects is a specialisation of a more general map that is basically the introspection in an "effect".

In case of an Array, the effect is the "repetition" of values
In case of an Object, the effect is the presence of multiple properties
On an Either though, the effect is that "something went correctly".

Array and Either are effectively wrapping/representing an effect.

packages/http/src/mocker/negotiator/NegotiatorHelpers.ts Outdated Show resolved Hide resolved
NOT_ACCEPTABLE,
`Could not find any content that satisfies ${mediaTypes.join(',')}`,
);
mediaType: 'text/plain',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that fix SO-265?
If it does make sure you have those tests in 631236f

See that WIP branch https://github.com/stoplightio/prism/tree/bug/so-265/inconsistent-content-negotiation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah now I realise the reason of that PR — I can merge that branch and see how it goes when we settle down on the code here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@XVincentX do you still have that on your radar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have, I'm a little bit overwhelmed but I'll get it done.

throw new Error('Requested status code is not defined in the schema.');

return result;
}).chain(responseByForcedStatusCode => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of using chain vs if?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if and chain do two different things, they cannot be interchanged.

To keep it short, chain is just like a flatMap

[1,2,3,4].map(t=>t*2) = [2,4,6,8]
[1,2,3].flatMap(t=>[t, t * 2, t * 3]) = [1, 2, 3, 2, 4, 6, 3, 6, 9]


In this context in particular, chain unwraps the effect for you so if you have an Either — you can concatenate it with another Either

const operationThatMightFail = getDataFromInternet();

operationThatMightFail
  .map(operation => operation.result)
  .chain(result => postInternetDataSomewhere(result))

In this case you can see:

  1. You receive an Either from a function that gets data from the internet or fails.
  2. In case it passes, it will call the map function that will effectively transform the result
  3. Pass the data to chain; a chain function can return another Either that will be "executed"/"unwrapped" for you automatically, flattening the final Either you have to deal with.

Does that help?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never liked chain / bind names for it. flatMap is so obvious a name! :)

return helpers
.negotiateOptionsBySpecificResponse(httpOperation, desiredOptions, responseByForcedStatusCode)
.chain(either =>
either.fold(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the goals was to improve readability but I can't tell this did the trick.
We're a increasing the cognitive complexity. Contributing to Prism is already hard (TypeScript, monorepo, workspaces/build and now we're adding functional programming on top of it - I have the feeling we're narrowing potential contributors to 0.00001% of developers ;) )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is nothing to do with this comment other than maybe not using chains?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chain is a fundamental piece — I'll try to explain it in another of the comment you did.

You have a point though, this is indeed a case-limit that will be addressed soon by the library itself by providing a more specific interface to handle this use case. So you're not wrong here!

packages/cli/README.md Show resolved Hide resolved
Co-Authored-By: Chris Miaskowski <chris@11sigma.com>
README.md Outdated Show resolved Hide resolved
packages/cli/src/commands/mock.ts Outdated Show resolved Hide resolved
if (dynamic) {
signale.star('Dynamic example generation enabled.');
}
if (cluster.isMaster) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change has so many new terms in it. cluster, stream, multipurpose, pino, master, signale, there is so much not mocking command leaking into the mocking command. I fear this will confuse the heck out of anyone who works on it.

Could you maybe make a stiry with sone notes how to refactor this? It's not just about reuse for a 2nd command, its a problem for understanding this one command.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah ok, this is definitely confusing; let me try to come up with a better way to reorganise the thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philsturgeon I've teared out stuff as much as possible here. 72336ba

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still a monster, but it should be very well enclosed in two nice functions.

}

function pipeOutputToSignale(stream: Readable) {
function constructPrefix(logLine: LogDescriptor): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm.. if I'm not mistaken, we can just move out constructPrefix from pipeOutputToSignale (?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'll probably refactor this command and offload most of the shit in the createServer file and go from there, hopefully that's going to help a lot in this command specifically. Stay tuned for updates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@XVincentX, I've seen that pipeOutputToSignale is now in another file. But can we also move constructPrefix out of pipeOutputToSignale? I don't see a need for constructPrefix to be defined inside pipeOutputToSignale (but maybe there is (?)). And also, how about pipeOutputToSignale -> pipeOutputToSignal ?

timestamp: false,
};

if (destination) return pino(options, destination);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, I had to... maybe a ternary ;) ?

payloadGenerator = generate;
}
return withLogger(logger => {
// setting default values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be much work to make this a separate function? Just like negotiateResponse ?

@XVincentX XVincentX merged commit 62c3f45 into master Jun 18, 2019
@XVincentX XVincentX deleted the feat/logs branch June 18, 2019 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants