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
Conversation
b1132c8
to
ef47888
Compare
31cf3b3
to
a8077dd
Compare
| @@ -0,0 +1,33 @@ | |||
| import * as pino from 'pino'; | |||
There was a problem hiding this comment.
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 (?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
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?
There was a problem hiding this 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.
| }, | ||
| httpContent, | ||
| ) | ||
| .map(contentNegotiationResult => ({ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| NOT_ACCEPTABLE, | ||
| `Could not find any content that satisfies ${mediaTypes.join(',')}`, | ||
| ); | ||
| mediaType: 'text/plain', |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- You receive an
Eitherfrom a function that gets data from the internet or fails. - In case it passes, it will call the
mapfunction that will effectively transform the result - Pass the data to chain; a chain function can return another
Eitherthat will be "executed"/"unwrapped" for you automatically, flattening the final Either you have to deal with.
Does that help?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 ;) )
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
Co-Authored-By: Chris Miaskowski <chris@11sigma.com>
packages/cli/src/commands/mock.ts
Outdated
| if (dynamic) { | ||
| signale.star('Dynamic example generation enabled.'); | ||
| } | ||
| if (cluster.isMaster) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
packages/cli/src/commands/mock.ts
Outdated
| } | ||
|
|
||
| function pipeOutputToSignale(stream: Readable) { | ||
| function constructPrefix(logLine: LogDescriptor): string { |
There was a problem hiding this comment.
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 (?)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 ?
All right, be prepared.
TL; DR
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:
loggerparameter in all the function and having to deal with it everywhereLet's now see how I've tamed them down.
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
-mflag 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
--runInBandflag.You can see from the image what when the
-mflag is specified, you'll find an additional process with node running — this is exactly the forking happening.Note: if
NODE_ENV===productionthe forking is enabled by default, so that if you use the CLI in a Docker Environment you get what the production behaviour.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 employedfp-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 leveragingchainandmapmethod, 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.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.
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
Eitherimplementation that's included infp-ts, which is not that hard to understand. If you check out theNegotiatorHelper.tsyou 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:ReaderEithercould let me remove some nested maps. I'm quite sure @gcanti will make it happen soonOutstanding points
mock.tsto look a little bit more humanloggercomponentSO-231