blob: bdb7e8e05637bb6807fe563c1bda1f3c76afc53b [file] [log] [blame]
/** @file
*
* Traffic Manager custom metrics.
*
* @section license License
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <cmath>
#include "ts/ink_config.h"
#include "ts/ink_memory.h"
#include "ts/Ptr.h"
#include "ts/Vec.h"
#include "ts/I_Layout.h"
#include "bindings/bindings.h"
#include "bindings/metrics.h"
#include "I_RecCore.h"
#include "MgmtDefs.h"
#include "MgmtUtils.h"
#include "WebOverview.h"
#include "metrics.h"
struct Evaluator {
Evaluator() : rec_name(nullptr), data_type(RECD_NULL), ref(LUA_NOREF) {}
~Evaluator() { ink_release_assert(this->ref == LUA_NOREF); }
Evaluator(const Evaluator &) = delete;
Evaluator &operator=(const Evaluator &) = delete;
bool
bind(lua_State *L, const char *metric, const char *expression)
{
if (RecGetRecordDataType(metric, &this->data_type) != REC_ERR_OKAY) {
return false;
}
this->rec_name = ats_strdup(metric);
switch (luaL_loadstring(L, expression)) {
case LUA_ERRSYNTAX:
case LUA_ERRMEM:
Debug("lua", "loadstring failed for %s", metric);
luaL_error(L, "invalid expression for %s", metric);
return false;
case 0:
break; // success
}
// The loaded chunk is now on the top of the stack. Stuff it into the registry
// so we can evaluate it later.
this->ref = luaL_ref(L, LUA_REGISTRYINDEX);
return true;
}
void
unbind(lua_State *L)
{
if (this->ref != LUA_NOREF) {
luaL_unref(L, LUA_REGISTRYINDEX, this->ref);
}
ats_free(this->rec_name);
this->ref = LUA_NOREF;
this->rec_name = nullptr;
this->data_type = RECD_NULL;
}
void
eval(lua_State *L) const
{
// Push the stashed expression chunk onto the stack.
lua_rawgeti(L, LUA_REGISTRYINDEX, this->ref);
// Evaluate it. Note that we don't emit a warning for
// evaluation errors. This is because not all metrics (eg.
// cache metrics) are available early in startup so we don't
// want to log spurious warning. Unfortunately it means that
// to check your config for errors you need to enable
// diagnostic tags.
lua_pushstring(L, this->rec_name);
if (lua_pcall(L, 1 /* nargs */, 1 /* nresults */, 0) != 0) {
Debug("lua", "failed to evaluate %s: %s", this->rec_name, lua_tostring(L, -1));
lua_pop(L, 1);
return;
}
// If we got a return value, set it on the record. Records can return nil to
// indicate they don't want to be set on this round.
if (!lua_isnil(L, -1)) {
RecData rec_value;
switch (this->data_type) {
case RECD_INT:
rec_value.rec_int = lua_tointeger(L, -1);
break;
case RECD_COUNTER:
rec_value.rec_counter = lua_tointeger(L, -1);
break;
case RECD_FLOAT:
// Lua will eval 0/0 to NaN rather than 0.
rec_value.rec_float = lua_tonumber(L, -1);
if (std::isnan(rec_value.rec_float)) {
rec_value.rec_float = 0.0;
}
break;
default:
goto done;
}
RecSetRecord(RECT_NULL, this->rec_name, this->data_type, &rec_value, nullptr, REC_SOURCE_EXPLICIT);
}
done:
// Pop the return value.
lua_pop(L, 1);
}
private:
char *rec_name;
RecDataT data_type;
int ref;
};
struct EvaluatorList {
EvaluatorList() : update(true), passes(0) {}
~EvaluatorList()
{
forv_Vec (Evaluator, e, this->evaluators) {
delete e;
}
}
EvaluatorList(const EvaluatorList &) = delete;
EvaluatorList &operator=(const EvaluatorList &) = delete;
void
push_back(Evaluator *e)
{
evaluators.push_back(e);
}
void
unbind(lua_State *L) const
{
forv_Vec (Evaluator, e, this->evaluators) {
e->unbind(L);
}
}
void
evaluate(lua_State *L) const
{
ink_hrtime start = ink_get_hrtime_internal();
ink_hrtime elapsed;
forv_Vec (Evaluator, e, this->evaluators) {
e->eval(L);
}
elapsed = ink_hrtime_diff(ink_get_hrtime_internal(), start);
Debug("lua", "evaluated %u metrics in %fmsec", evaluators.length(), ink_hrtime_to_usec(elapsed) / 1000.0);
}
bool update;
int64_t passes;
Vec<Evaluator *> evaluators;
};
static int
update_metrics_namespace(lua_State *L)
{
lua_Integer count;
lua_metrics_install(L);
count = lua_tointeger(L, 1);
lua_pop(L, 1);
return count;
}
static int64_t
timestamp_now_msec()
{
ink_hrtime now = ink_get_hrtime_internal();
return ink_hrtime_to_msec(now);
}
static int
metrics_register_evaluator(lua_State *L)
{
const char *metric;
const char *chunk;
Evaluator *eval;
EvaluatorList *evaluators;
BindingInstance *binding;
// The metric name is the first upvalue (from the record creation closure).
metric = lua_tostring(L, lua_upvalueindex(1));
// The evaluation chunk is the (only) argument.
chunk = lua_tostring(L, -1);
binding = BindingInstance::self(L);
evaluators = (EvaluatorList *)binding->retrieve_ptr("evaluators");
ink_release_assert(evaluators != nullptr);
eval = new Evaluator();
eval->bind(L, metric, chunk);
evaluators->push_back(eval);
return 0;
}
static int
metrics_create_record(lua_State *L, RecDataT data_type)
{
const char *name;
RecT rec_type = RECT_NULL;
int error = REC_ERR_FAIL;
BindingInstance::typecheck(L, "record.create", LUA_TSTRING, LUA_TNONE);
// Get the name of the record to create.
name = lua_tostring(L, -1);
if (strncmp(name, "proxy.process.", sizeof("proxy.process.") - 1) == 0) {
rec_type = RECT_PROCESS;
} else if (strncmp(name, "proxy.node.", sizeof("proxy.node.") - 1) == 0) {
rec_type = RECT_NODE;
} else if (strncmp(name, "proxy.cluster.", sizeof("proxy.cluster.") - 1) == 0) {
rec_type = RECT_CLUSTER;
}
// You have to follow the naming convention.
if (rec_type == RECT_NULL) {
luaL_error(L, "invalid metric name '%s'", name);
}
switch (data_type) {
case RECD_INT:
error = RecRegisterStatInt(rec_type, name, 0, RECP_NON_PERSISTENT);
break;
case RECD_FLOAT:
error = RecRegisterStatFloat(rec_type, name, 0, RECP_NON_PERSISTENT);
break;
case RECD_COUNTER:
error = RecRegisterStatCounter(rec_type, name, 0, RECP_NON_PERSISTENT);
break;
default:
break;
}
if (error != REC_ERR_OKAY) {
luaL_error(L, "failed to register metric '%s'", name);
}
// Push a copy of the metric name onto the stack.
lua_pushvalue(L, -1);
// Push the Evaluator as a closure with the metric name as an upvalue.
lua_pushcclosure(L, metrics_register_evaluator, 1);
Debug("lua", "registered %s as record type %d", name, rec_type);
return 1;
}
static int
metrics_create_integer(lua_State *L)
{
return metrics_create_record(L, RECD_INT);
}
static int
metrics_create_counter(lua_State *L)
{
return metrics_create_record(L, RECD_COUNTER);
}
static int
metrics_create_float(lua_State *L)
{
return metrics_create_record(L, RECD_FLOAT);
}
static int
metrics_cluster_sum(lua_State *L)
{
const char *rec_name;
RecDataT data_type;
RecData rec_data;
// Get the name of the record to sum.
rec_name = lua_tostring(L, -1);
// XXX Check whether we have a cached value for this somewhere ...
// If not, get the record data type.
if (RecGetRecordDataType(rec_name, &data_type) == REC_ERR_FAIL) {
luaL_error(L, "unknown metric name '%s'", rec_name);
}
// Sum the cluster value.
if (!overviewGenerator->varClusterDataFromName(data_type, rec_name, &rec_data)) {
RecDataZero(data_type, &rec_data);
// If we can't get any cluster data, return nil. This will generally cause the
// evaluator to fail, which is handled by logging and ignoring the failure.
lua_pushnil(L);
return 1;
}
switch (data_type) {
case RECD_INT: /* fallthru */
case RECD_COUNTER:
lua_pushinteger(L, rec_data.rec_int);
break;
case RECD_FLOAT:
lua_pushnumber(L, rec_data.rec_float);
break;
case RECD_STRING:
lua_pushlstring(L, rec_data.rec_string, strlen(rec_data.rec_string));
break;
default:
lua_pushnil(L);
}
// Return 1 value on the stack.
return 1;
}
bool
metrics_binding_initialize(BindingInstance &binding)
{
if (!binding.construct()) {
mgmt_fatal(0, "failed to initialize Lua runtime\n");
}
// Register the metrics userdata type.
lua_metrics_register(binding.lua);
update_metrics_namespace(binding.lua);
// Register our own API.
binding.bind_function("integer", metrics_create_integer);
binding.bind_function("counter", metrics_create_counter);
binding.bind_function("float", metrics_create_float);
binding.bind_function("metrics.cluster.sum", metrics_cluster_sum);
binding.bind_constant("metrics.now.msec", timestamp_now_msec());
binding.bind_constant("metrics.update.pass", lua_Integer(0));
// Stash a backpointer to the evaluators.
binding.attach_ptr("evaluators", new EvaluatorList());
return true;
}
void
metrics_binding_destroy(BindingInstance &binding)
{
EvaluatorList *evaluators;
evaluators = (EvaluatorList *)binding.retrieve_ptr("evaluators");
binding.attach_ptr("evaluators", nullptr);
evaluators->unbind(binding.lua);
delete evaluators;
}
bool
metrics_binding_configure(BindingInstance &binding)
{
ats_scoped_str sysconfdir(RecConfigReadConfigDir());
ats_scoped_str config(Layout::get()->relative_to(sysconfdir.get(), "metrics.config"));
return binding.require(config.get());
}
void
metrics_binding_evaluate(BindingInstance &binding)
{
EvaluatorList *evaluators;
evaluators = (EvaluatorList *)binding.retrieve_ptr("evaluators");
ink_release_assert(evaluators != nullptr);
// Keep updating the namespace until it settles (ie. we make 0 updates).
if (evaluators->update) {
evaluators->update = update_metrics_namespace(binding.lua) ? true : false;
}
binding.bind_constant("metrics.now.msec", timestamp_now_msec());
binding.bind_constant("metrics.update.pass", ++evaluators->passes);
evaluators->evaluate(binding.lua);
// Periodically refresh the namespace to catch newly added metrics.
if (evaluators->passes % 10 == 0) {
evaluators->update = true;
}
}