Traffic Control Development Guide¶
Traffic Control interacts with Traffic Server through the JSONRPC 2.0 endpoint. All interaction is done by following the JSONRPC 2.0 protocol.
Overall structure¶
The whole point is to separate the command handling from the printing part.
Printing should be done by an appropriate Printer implementation, this should support several kinds of printing formats.
For now, everything is printing in the standard output, but a particular printer can be implemented in such way that the output could be sent to a different destination.
JSONRPC requests have a base class that hides some of the basic and common parts, like
id
, andversion
. When deriving from this class, the only thing that needs to be override is themethod
Important
CtrlCommand will invoke _invoked_func
when executed, this should be set by the derived class
The whole design is that the command will execute the
_invoked_func
once invoked. This function ptr should be set by the appropriated derived class based on the passed parameters. The derived class have the option to override the execute() which is avirtual
function and does something else. CheckRecordCommand
as an example.
Command implementation¶
Add the right command to the
ArgParser
object inside thetraffic_ctl.cc
.If needed, define a new
Command
derived class inside theCtrlCommands
file. if it’s not an new command level, and it’s a subcommand, then you should check the existing command to decide where to place it.Implement the member function that will be dealing with the particular command, ie: (config_status())
If a new JsonRPC Message needs to be done, then implement it by deriving from
shared::rpc::ClientRequest
if a method is needed, or fromshared::rpc::ClientRequestNotification
if it’s a notification. More info can be found here Requests and Design. This can be done inside theRPCRequest.h
file.
Note
Make sure you override the
std::string get_method() const
member function with the appropriate api method name.If needed define a new
Printer
derived class inside theCtrlPrinter
file.If pretty printing format will be supported, then make sure you read the
_format
member you get from theBasePrinter
class.
If it’s a new command level (like config, metric, etc), make sure you update the
Command
creation inside thetraffic_ctl.cc
file.
Implementation Example¶
Let’s define a new command for a new specific API with name == admin_new_command_1
with the following json structure:
$ traffic_ctl new-command new-subcommand1
Update
traffic_ctl.cc
. I will ignore the details as they are trivial.Define a new Request.
So based on the above json, we can model our request as:
// RPCRequests.h struct NewCommandJsonRPCRequest : shared::rpc::ClientRequest { using super = shared::rpc::ClientRequest; struct Params { std::string named_var_1; }; NewCommandJsonRPCRequest(Params p) { super::params = p; // This will invoke the built-in conversion mechanism in the yamlcpp library. } // Important to override this function, as this is the only way that the "method" field will be set. std::string get_method() const { return "admin_new_command_1"; } };
Implement the yamlcpp convert function, Yaml-cpp has a built-in conversion mechanism. You can refer to YAML for more info.
// yaml_codecs.h template <> struct convert<NewCommandJsonRPCRequest::Params> { static Node encode(NewCommandJsonRPCRequest::Params const ¶ms) { Node node; node["named_var_1"] = params.named_var_1; return node; } };
Define a new command. For the sake of simplicity I’ll only implement it in the
.h
files.// CtrlCommands.h & CtrlCommands.cc struct NewCommand : public CtrlCommand { NewCommand(ts::Arguments args): CtrlCommand(args) { // we are interested in the format. auto fmt = parse_format(_arguments); if (args.get("new-subcommand1") { // we need to create the right printer. _printer = std::make_sharec<MyNewSubcommandPrinter>(fmt); // we need to set the _invoked_func that will be called when execute() is called. _invoked_func = [&]() { handle_new_subcommand1(); }; } // if more subcommands are needed, then add them here. } private: void handle_new_subcommand1() { NewCommandJsonRPCRequest req{}; // fill the req if needed. auto response = invoke_rpc(req); _printer->write_output(response); } };
Define a new printer to deal with this command. We will assume that the printing will be different for every subcommand. so we will create our own one.
class MyNewSubcommandPrinter : public BasePrinter { void write_output(YAML::Node const &result) override { // result will contain what's coming back from the server. } };
In case that the format type is important, then we should allow it by accepting the format being passed in the constructor. And let it set the base one as well.
MyNewSubcommandPrinter(BasePrinter::Format fmt) : BasePrinter(fmt) {}
The way you print and the destination of the message is up to the developer’s needs, either a terminal or some other place. If the response from the server is a complex object, you can always model the response with your own type and use the built-in yamlcpp mechanism to decode the
YAML::Node
.write_output(YAML::Node const &result)
will only have the result defined in the protocol, check Result for more detail. So something like this can be easily achieved:void GetHostStatusPrinter::write_output(YAML::Node const &result) { auto response = result.as<NewCommandJsonRPCRResponse>(); // will invoke the yamlcpp decode. // you can now deal with the Record object and not with the yaml node. }
Notes¶
There is code that was written in this way by design, RecordPrinter
and RecordRequest
are meant to be use by any command
that needs to query and print records without any major hassle.