Go Structured Logging with the slog Package (Golang)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this video we're going to explore some logging options in the go programming language and in particular we're going to look at a package called s log and slog was introduced very recently in Go version 1.21 and this is a package for structured logging in go now structured logging is useful because it outputs logs in a format that allows them to be searched and allows you to find the particular pieces of information that you need to find and the slog package offers functionality and goal for things like log levels content textual logging and different Handler formats for example Json handlers and it also provides tools to build child loggers and also redact sensitive information we're going to dive in in this video and see examples of all of these so let's get started now this is the package here it's called esog and there'll be links to these packages below the video what we're going to do to start with is look at the standard log package that was built into to go since the beginning of the programming language and if we look at the overview the package log it implements a very simple logging package it defines a type called logger with methods for formatting output and this package provides functions like print F and print line as well as similar functions called fatal and panic that do slightly different things and exit the program when they are called now let's start with a look at the log package I'm going to open VSS code where I have a very simple go Application it consists of one file called main.go and we've defined a package called men here what I'm going to do is add a statement to log. print line so we can see what kind of output this generates and let's say we've submitted an order and we want to Output that statement to the terminal when this particular program is run now I have a terminal on the right hand side and I'm going to run the G run main. go command and that's going to Output that log in the format that is generated by default by the log package and you can see that format here it consists of the time at which this log was generated as well as the message that was passed into the print line function here so that's the very simple use of the log package what we can do is customize the default output slightly now what I'm going to do in order to do that is create a variable called logger and we're going to call a method from the log package and that's the default method now what does the default method do let me get access to the intellisense here default Returns the standard logger that's used by the package level output functions and once we get access to that logger we can call a method on that called set output and what this can do is it takes an i. writer instance and it's going to send the output to that writer now the io. writer is actually an interface and there's many implementations of that interface in go and one of these just a very simple one is the os. standard error output so what we're doing in this program is we're getting access to the default logger and we're setting the output to standard error and this can take any writer for example it can take a file and you could then send your log output to that file so what we're going to do is just call a method called print line and again I'm going to pass that string order submitted and on the right hand side we'll run this application and we're going to see that we get that output and that's coming to standard error and it's the same output with obviously a different time stamp and there's one more function to show from the log package and that's the log. new function and what this function does is it allows you to create a new logger and it takes three parameters the first one being the output which is an instance of io. writer as well as a prefix which we'll see in a second and some Flags can be sent as well what we're going to do now is we're going to implement this function and we need to pass a writer again I'm going to pass standard error and let me move this to a new line so we can see it more clearly and I'm going to remove the previous statement as well as that we need a prefix so let's say this is for an orders application we can prefix the output for this logger with this prefix here and then finally we can pass some Flags now these are constants that are defined in the log package for example one of them is log do l Date and we can separate the flags with this pipe operator and we can pass a bunch of other ones in there and if we save that after adding those flags and run the application on the right hand side you can see we get the same output as before except that now we have a prefix of orders which is the prefix we specified as the second parameter here and we can add extra Flags here for example I could add a call or rather a reference to log. L short file and what that's going to do is actually add the file in shorthand format to this output so you can see that we get main. go and we also get line 14 here and you can see that a little bit better now and line 14 is this one here the logger do print line function that generates that output so this is all working fine we have a log package in go so why do we need structured logging now there are some problems with the log package as we see it here we don't have the concept of levels built into this package we're also unable to easily generate structured Json output or log format output in key value pairs what we actually get as output here is just a bit of text it's a string containing the flags that we set and the message that we passed in but we can't use any kind of structured key value lookups here because it's just a dump of text essentially and finally we also have limited configuration available in this log package so in order to customize the log output we can now use this new package called SL log so what I'm going to do is remove all of this code that we have here and I'm going to save this file and that will remove the Imports at the top now you need to have Go version 1.21 installed if you run the Go version command you'll get the particular version that you have and you can see that I'm on 1. 121.4 and if you have that what you can do is call functions from the slog package for example SL log. debug and we can pass messages in there for example debug message and I'm going to define a couple of extra methods here to show that we have the concept of log levels built into slog so we have SL log. debug slog doino and let's again use order submitted as a message and I'm going to copy that down below and we can also use SL log. error this is another level function and I'm going to change the message here to error occurred and I'm now going to run this program and so we can see the output better I'm going to close this terminal and I'm going to open a new terminal and bring that onto the screen now I'm in the same directory as that main. go file as you can see here we can run that again with the G run command and we're going to see the output from these log messages and you can see we get the time of the logging as before as well as the log level here info and error and the message that was passed to those methods now one thing you might notice is that we don't get the message for the SL log. debug call and that's because by default the default log level in the slog package is set to info and any message with a lower severity than info will not be shown by default what I'm going to do is Define a variable called logger as we did before but this time what I'm going to do is call the SL log. new function and this creates a new logger with the given non-nil Handler and that takes an EXA uh an instance sorry of the SL log. Handler this is another interface that's built into the S log package and what we can provide to that is one of the two handlers that are built into the S Lo package I'm going to use the new text Handler and what this does is it creates a text Handler that writes to the writer that we provide as a parameter and we can also specify some options as well and this Handler is going to Output our logs in key value pairs as we're going to see in a second so let's pass an instance of the io. writer interface and that's os. standard error and right now I'm going to pass nil for these options we're not going to specify any custom options at the moment so we now have a logger instance here and in order to use that logger by default the SL log package has a method called set default and we can pass that logger instance into that method and it's going to make it the default logger as it says here once we've done that anytime we call these methods like debug info and error it's going to use that logger that we passed in here to the set default function and that is is going to then output our logs using this new text Handler so let's see another example of that if we go back to the terminal here and run this command what we're going to get is a different format and you can see we have keys for example time and values here for each of the keys so the keys are time level and message by default so we can see the time at which the log was generated we can see the level and we can also see the message and because these have keys that makes it a lot easier to do lookups for for these values and extract the information that you need and if you're wondering about this output format I'm going to reference this page here and this is the name given to that format and this has been popularized by Heroku and as it says each line consists of a single level of key value pairs that are densely packed together so that's the format for this particular Handler the new text Handler but we can add another Handler and the slog package has two handlers built in the other one is the new Json Handler and as the name implies that's going to Output these logs in Json format when we rerun this program you can see we now get that output so we're now getting Json output for our logs and again if you take these outputs and you look up each record by its timestamp or the level you can then extract the data that you might need in order to find things that are going on in your application and also to build things like dashboards and charts around that data and that is of course where centralizing your logs on a platform like better stack is going to payoff dividends it allows you to analyze those things much more clearly and we are aided in the case of go by the use of this slog package so two different output formats here but they are both structured formats with key value pairs let's now move on what I'm going to do is show how we can actually get access to this SL log. debug output and in order to do that we can pass some options to the Handler that we're passing into the SL log. new function so rather than passing nil we're going to pass a second argument here which has to be be an instance of or rather a pointer to an S log. Handler options so what I'm going to do is I'm going to Define this variable in a second but it's going to be called Handler options and I'm going to copy the name of that and on the line above this logger we're going to Define that and that's going to be a pointer to these Handler options so slog do Handler options and we can pass in an option here called level and we're going to set that equal to the slog do level debug so these Handler options are setting a level of debug and when we pass that into our Json Handler it's going to tell the logger that we have that we want to Output messages of debug level and above so we can save that and then go back to the terminal and I'm going to rerun this command and I'm getting this error here and that's because I didn't close the options or rather I didn't add a comma at the end here so what we can do is rerun this again and we're going to get the output but this time we get the output for the debug message as well now we can add another option to these Handler options for example let's add one called add source and set that to true if we go back and run this again what we're going to see is we get an extra key in the output called source and that links to some particular information about where the log was generated and it was generated in the main function in this particular file so that's another option you can pass to these Handler options I'm actually going to remove that we're going to keep the level debug for now but we can remove that extra option and let's now move on what we're going to see is how we can log out our own attributes so custom key value pairs that we want to be in the log output and of course this is very important because the default logs they are giving us some useful information for example let me clean this up and rerun this if we look at this we get the level which we need to know we also get the message which is very important and the Tim stamp but we might want extra key value pairs giving more contextual information about the log that's being generated for example we might need a trace ID a request ID or user ID or some kind of information like that to try and narrow down which particular user or request or methods are actually generating this output so we can add our own custom key value pairs to the output that we are generating from these SL log methods and we can do that just by passing extra keys and values to the methods actually for now I'm going to remove the SL log. debug let's use slog doino and I'm going to pass extra key value Pairs and this is done in a couple of different ways we're going to see this example now what we can do is we can pass a key and we can call that anything we want let's call it user and we're going to give this a value of John do so what we're telling the slog doino method here is that we want to pass this extra key called user and we're passing a value here of John do for that key and of course in the real system this value is probably going to be some kind of dynamic value for the user but let's save this and we're going to go back to the terminal here I'm going to clear this out and we're going to rerun the script and we're going to see this output that's generated we can see we get the extra key for our context here our user and the value of John do and we can pass as many key value pairs as we want to these methods such as info and error so for example if I also wanted to log out the user's ID I could add another key called ID and you're seeing the error here that we're getting because we don't have an even number of arguments after the message you need to make sure every key has a value if you're going to use this structure and this has caused a bit of debate in the go community believe but anyway we do need to add a value here so let's use a value this time I'm going to use a value from the random module let's call a function called Inn and the NN function here is going to return an integer between zero and the number that we pass in so let's pass in 50 and that's going to give us back a random integer between 0 and 50 if we go back to our terminal and run this again you can see we now get this ID at the end here and it's set to Value 49 if we rerun it again we get 39 so we get a different ID each time we run the program and what we can do if you don't like this format where you have a key then a value then another key then another value is you can consolidate these into SL log attributes so an attribute is just a key value pair what I'm going to do is start with the user what we can do is Define an slog do string attribute and you can see that takes a key and a value and it returns one of these attributes so the key was user and the value is going to be John do and what I'm going to do is move these to a new line so let me clean this up a little bit we're going to move these to new lines so we can see it clearly and the second SL log attribute that we want to add here is for the ID so let's remove that and this time we're going to use slog do int and the key was the ID and the value again let's use the random package and we're going to use the in in function and pass 50 now if we save this and go back to our terminal I'm going to clear this out and run the application again and again we've got a missing comma so that's my mistake let's add that to the slog doino call and go back and rerun this again we're going to get the output now and it contains the ID and the username as before so this is a different way of adding these attributes rather than a key and a value and then another key and another value and so on you can actually consolidate them into slog attributes and pass them into the function and whichever one you prefer that's just a matter of personal preference you can use whichever one you like best with this method you get the benefit of strong typing but other than that it's pretty much the same thing so we've seen a few things so far we've seen how to define different handlers including the Json Handler and the text Handler that's built into the SL log package we've seen how to override the default logger and set it to a custom one that we've created and we've also seen how we can pass in some Handler options when we create these handlers in order to customize things like the log level and of course we've just seen how to pass custom arguments custom key values to our SL log level methods let's now move on what we're going to do is we're going to see how to use groups in the slog package in order to group together contextual attributes so let's say for this example that we want to log out data on users but we also want to log out some request data for example whether it's a post or a get request and we want to very commonly add this data to our logs now what I'm going to do to start with is I'm going to remove these attributes that we passed to SL loginfo and what we're going to do above that is create an SL log group and we're actually going to create two of these I'm going to call the first one the user group and that's going to be an slog do group call and you can see that the first argument to this is a key and that's the name that we give to this group I'm going to give it a name of users after we give the group a name we can pass a variable number of arguments here and this takes the same format that we saw before where we have a key then a value then optionally another key and another value and these are grouped together into this SL log group so let's see an example of that now again then we have a user ID which is going to be equal to the rand. Inn function and we'll pass 50 into that again and let's also pass the username of John do into this function so I've cleaned this up a bit we have the name of the group on the first line and then we have each key value that we want to add to the group on each line below so the ID and the username these make up the actual key values that we want to add to this group and then we can pass the group to our slog level methods so let's pass the user group to slog doino and I'm also going to create a second group here and we'll call that request group and we're going to make that equal to another slog group we'll give this one a name of request and for this group I'm only going to add one single key and obviously a value for that so let's just say for this imaginary request the method is a get request so let's copy the name of this group and add it to the call to slog doino so we're actually passing two groups to the slog doino method and if we scroll up a little bit we have a Json Handler let's see how the Json Handler outputs the data for these groups I'm going to run the application and we get this output here now what I've done is I've copied that and I've pasted it into vs code and formatted it so we can see this better what we have are the typical Keys here for the time the level and the message but we've added the groups for the users and the request to this application and I'm going to split screen this so we can compare it to the code what we have is a group the user group here with the key of users and that's the key that's added to the output Json and with Json the user group here is represented as a nested object and each key in value corresponds to what we have in the group itself and that is exactly the same for the request group we have a group with the name or the key of request and then we have a single key in value in that group and that's for the method and that's added as that nested object and we're going to see how this output changes when we use the other Handler that's built into the s log package and that's the new text Handler if we save that and go back to the terminal I'm going to run this again and remember the text Handler outputed that key equals value format you can see the nested data here at the right hand side of this output and we have the key of the group and then a DOT syntax and then the name of the actual nested key within the group in this case the ID and also one for the username and we have the same for the request group here that group has the key name request and then we have the method attribute within that group so that's represented in this key value output with the neste data separated using this dot notation so that is an example of how we can use these groups in the slog package we Define an slog do group and then we can add any number of keys and values to that group and we can then pass the groups into our log level methods such as info and error in order for all of the keys and values in that group to be present in the output let's now move on we're going to look at the concept of child loggers now often when you are logging data you want to log the same keys and values many times and at different places in your application there are different ways of adding those keys and values you could add them to every single call of methods like slog doino or SL log. error but that is not the most maintainable approach because anytime you wanted to change any data that's present in those logs you would need to add new keys and values or remove keys and values from every single instance of these calls where you've added those particular attributes now there is a better way to do this and go what we can do with the S log package is we can define a child logger and consolidate a set of attributes in that child logger so basically you can add the keys and values you want to repeatedly appear in your output in a single place and then if you need to change things later on you simply change that child logger definition and that will then apply those changes to anywhere that you're using that particular child logger so let's see an example of that now we can use a method that's defined on the logger object and that's the width method and this will return a new logger that includes the given arguments converted to attributes and then these attributes will be added to every single output for that logger so let's go back to VSS code here and what I'm going to do for this example is I'm going to remove the user group we don't need that anymore we'll keep the request group around and we're going to create an SL log child logger for the data that we have in this request so I'm going to save the file we have this group here called request group what I'm going to do is I'm going to call that with method that's defined on our logger so we're going to use the logger here and we're going to say logger dowith and that will as it said in the docs return a new SL log logger object and in order to add all of the keys and values from a group to the logger by default we can just pass the name of the group that we've already defined into this method here now as the doc said this will return a new logger so what we're going to do is create a variable called request logger and whenever we use this request logger in our program it's going to include all of the attributes from the group that we've passed into this with method by default so what we can do is we can change these methods here rather than slog dotinfo we can use a request logger that we've created and we can do that for the error as well and I can remove this group that we're passing into the method because that's now going to be done through this child logger that we're using to call these methods so let's now save this file and we're going to go back to the terminal here and I'm going to clear this and run go run main.go and you can see in the output here that we get the request method added to both of the outputs that we have in this application so if we go back to VSS code we have a call to do info and error on the logger but because we've created this child logger that has the request attributes added to it every time we call a log method using this logger it's going to add the request to the output as you can see here and this will work for any number of attributes in the request so if I added another one for example let's say just add one called content type here and we set that equal to application SL Json we've added another key and value to the request group and we're passing that into the with method which will create this child logger so when we run this application again we're going to get that extra request value for the content type added to the logs so this method of creating a child blogger is a good way to consolidate keys and values that you want to add to multiple places in your application you can simply create a child instance of the logger and then call the normal methods and then that will by default add those keys and values to all of your log output and of course as well as passing a group you can actually pass keys and values into the width method so if I cut these values out of the group and we pass them into loger dowith this time as well as the group we're passing content type and application Jason if we save this and go back to the terminal here I'm going to execute this one more time and we're going to see that we get almost the same output but because this is no longer part of a group we don't get the prefix with the group's name but the key Point again is that we can add any number of keys and values including the keys and values from a group into a child logger which will then add those keys and values to all of your outputs let's now move on to another example we're going to see how to log out to files in a application using the S log package so I'm going to scroll up here and we're going to look at this Handler that we have and to this Handler we passed the io. writer instance and in this case it was standard error that we passed in now we can pass anything we want into this function that implements the io. writer interface and that basically means anything that defines a right method can be passed in as the first argument to our Handler here so let's start by working through an example what I'm going to do in the terminal is I'm going to Define an environment variable so let's clear out what we have and I'm going to reference inv and then we'll Define a variable called log file and I'm going to set that equal to a variable or a value sorry called app. log and what this does is it sets an environment variable for the duration of this Powershell session in this case and if you want to do the same on a Unix system or on a Mac you can use this command it's called export and then you can Define your variable log file and set it to the value that you want so so once we've defined that environment variable what we're going to do is we're going to read it into our go application so let's open up the file main. go and we're going to read it in at the top of the main function so let's set a variable called log file and we're going to set that equal to the os. get EnV function and get in will retrieve the value of an environment variable named by the key that we pass in and the key that we used was log file and I need to make sure as well that I'm assigning that value correctly using that syntax here so so what we're doing is we are reading the log file from the environment which is a common practice and what we're going to do is open this file in the application and we're going to use that file as the io. writer that we're passing into the Handler so I'm going to open a file in this application using the os. open file function and this takes three parameters that we're going to use first of all the name of the file and we're going to pass that log file that we read in from the environment as that parameter and we then need to pass in a flag to this method so I'm going to pass in a couple of these we're going to use the os. create flag so that's the first flag let's use another one now and we're going to tell the open file function that we want to write only to this file using this flag here it's called Write only and the final thing we want to say because this is a log file when we actually write things to the file we don't want to overwrite what's already there so what we're going to do is we're going to pass the O appended flag so that's the three that I want to pass in and the final parameter is the permission and that is going to be 0666 and this mode allows the file to be read and also written to now the open file method returns two things it returns a pointer to a file and also potentially an error so we need to get those at the start of this statement so let's create a variable called file and we'll also get the error here when we call west. open file so what this is doing is it's going to open that log file if it doesn't already already exist it's going to create it using this flag and it's also specifying that we only want to write to the file with this flag and also that we want to append any output to the file now when we potentially get that error back we need to check if the error is not equal to nil and if it turns out we've got an error what we can just do in this case is panic and pass the error through to the Panic function and that's going to exit the application if there was any problems opening that file otherwise we can move on and what we always need to do when we open a file is defair the file. close method and when you defer a statement and go it means that when the function Returns the statement here will be executed and in this case it's going to close the file and that's going to prevent any potential memory leaks so this gives us a file object and this is an implementation of io. writer so we can actually pass it into our Handler here instead of os. standard error let's pass in the file itself now once we've done that I'm going to bring up the terminal and I'm going to run this command go run main.go and this time you see that we don't get any output on the terminal if we go back to vs code and open up the file browser on the left hand side we now have this file called app. loock and that's the name that we gave to the file in the environment variable and we're getting this output here in the key value format and we get all of the keys and values output to this file so now we've changed the structure of our output we're no longer logging to the terminal to standard out or standard error this time we're logging out to a file and we do that by passing the file into our Handler and this will work for the Json Handler as well so if we change the name of the Handler and run this again if we go back to the log file this time we get the logs coming out in Json format so it's very easy to change the way this works to log out to a file and you may actually want to log out to multiple destinations let's see an example of where we can write not only to the file but also to standard error and to do that we're going to use a utility from the io module and it's the multiwriter this will create a writer that duplicates its rights to all of the provided writers similar to the Unix T command if you're familiar with that and each write is written one at a time to the writer so it's very easy to implement this what we can do is go back to vs code and just after we defer closing the file we're going to create a variable called W and we're going to set that to io. multiwriter and let's look at the signature for this this takes a VAR adic number of writers so we can pass as many writers in as parameters as we want so let's start with os. standard error and then we can also pass the file that we created as the second writer so we now have W let's pass that instead of the file to the Json Handler and we can then save this go file and go back to the terminal this time we expect to see the Json on the terminal and you can see we get that back here but if we go back to the program and go to app. log that Json is also being sent to this file as well so we're now writing the output of our logs to multiple destinations very easy to do that by just implementing a multi-writer and passing every destination that we need as parameters to that multiwriter now the last thing I want to do in this video is show how we might want to redact sensitive data from our logs and sensitive data could mean credit card information it could mean private user information like passwords and email addresses and addresses themselves you don't want to Output stuff like that in your logs where it could potentially be read by people that shouldn't be reading that information so you want to redact that information we can do that by implementing an interface called log Val in go so let's go to the documentation and look at this type it's called log value and this is any Go Value that can convert itself into a value for logging so the idea is we take a value and go that we want to log out but we want to perform some kind of transformation of that value so that it's safe to log in our output and this interface log value will allow you to specify how your custom types should be logged we're going to see an example of that now by going back to vs code and just above the main function I'm going to define a type that we're going to call user and this is going to be a struct and it's going to contain a few Fields let's start with the ID which should be an integer and in Json we want to Output that with the key name of lowercase ID we also want this user to have a username and that's going to be a string and again in Json we want that to be equal to a key called username and the final field is going to be the password and that's going to be a string as well and in Json we want that to be the password now you could omit this from the output ad Json just by converting the member to lowercase but let's assume that we don't have that we're going to use the log value interface in order to change the output for this particular type the user type so what we're going to do is we're going to go down to the very bottom here and just after we Define this request logger what I'm going to do is create a variable called user and this is going to be a pointer to a user object so we're going to pass three values the users ID which is going to be another instance of a random integer between 0 and 50 we also have the username let's just pass John do and the password for this user is just going to be secret and I'm going to remove the call to the error method and we're just going to pass the user in to the request logger doino method now that's the value of the user we also need to pass the key which we're going to pass here as user so basically the key is user and the value is the value of this struct so we're going to see that represented in the output let's go back to the terminal and run the application and we get this output again I've pasted that into vs code so we can see it better by adding a key called user that points to that structure you can see we get a nested object in the Json data with the ID the username and the password now of course the problem here is that we have a raw string password appearing in our log output that is not desirable under no circumstances do you want to do this so what we're going to do is we're going to implement that log value interface in order to Define our output representation for a user now it's very easy to do this if we go back to main. go I'm going to go above this main function and just below where we Define the user struct and I'm going to Define what's called a pointer receiver method and let's see exactly how to do that just now we Define a function but before we actually Define the name of that function we are going to pass a pointer to the user struct and then in order to implement the log valer interface we Define the method called log value and that's not going to take any parameters but it's going to return an SL log. value so this syntax is defining a function that will operate on an instance of our user structure and in order to customize the log output for the user we just need to Define this function and use that pointer to the user object what we return from this function is what we actually want to be output when we log out an instance of the structure so let's log out the SL log. int value so we're going to return an integer when we log out this user and we just want to log out the user's ID so we have access to the strrip that we're working with through the variable U that we have in this pointer receiver here and that has the field name of ID and that's what what we're referencing and returning from this log value function so that's the only change we actually need to make let's go back to the terminal and what we hope to see is that when we log out the user rather than getting this entire object of data we are now going to get just the user's ID and this is the output we get again I'm going to copy that to vs code so let's go back to VSS code and you can see this time we only get the user ID and the output so if you need to customize the output for a particular type that you have or if you need to redact some data you can implement the log valer interface on your particular type and then return whatever value you want to come out in the logs for that particular type and we could do something more complicated here so for example we could use the slog do string value function and then use the format. S printf and the S printf method allows us to pass some kind of format string here so we could pass the string here user ID and then use the percentage D and that will give us the value for the user ID and then we could pass more information in this string for example we could pass for user and then pass the percentage s in here and what we're going to reference is the user's ID and also the username here and these are both fields on the structure and we are referencing them in the S print F function and this is telling the slog package that when we encounter this type this is the particular format of the output we want to generate so let's test this out and we're going to run the program and you can see the output we get here for the user key user ID 40 for user John do so that has changed again the output format just by implementing the log valer interface so that's all for this video we've showed numerous features of the SL log package of course there's much more you can do with s log for example you could send your logs to a service like better stack in order to better Aggregate and analyze those logs and search FR logs for useful information you could also look at how to work with context when you're using these loggers and also you can look at third party handlers rather than defining or rather than implementing the two handlers that we have in the S Lo package which is the new Json Handler and the new text Handler you can also integrate with third party loggers and there are many options for that we'll link an article in the description of this video that shows you a comparison of many different log packages in go if you're interested in more go content or more loging content in the go programming language let us know in the comments if you have any thoughts please share them in the comments as well thanks again for watching if you've enjoyed this content please consider subscribing to the better stack YouTube channel thanks again and we'll see you in the next video
Info
Channel: Better Stack
Views: 3,872
Rating: undefined out of 5
Keywords:
Id: ptoKy-COIlE
Channel Id: undefined
Length: 37min 22sec (2242 seconds)
Published: Sat Dec 02 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.