35#include <SimoxUtility/json/json.hpp>
42#include <RobotAPI/libraries/armem_skills/aron/Skill.aron.generated.h>
54 constexpr std::size_t SeenCapacity = 512;
57 constexpr std::chrono::milliseconds DrainTimeout{1000};
67 subscription.release();
80 return "OpossumLogger";
89 defs->optional(p.enabled,
"opossum.Enabled",
"Whether to report skill events at all.");
90 defs->optional(p.robotName,
92 "Value of the 'robot_name' field. The server sorts the logs of the "
93 "different robots by this name.");
94 defs->optional(p.host,
"opossum.Host",
"Host of the OPOSSUM summary server.");
95 defs->optional(p.port,
"opossum.Port",
"Port of the OPOSSUM summary server.");
96 defs->optional(p.path,
98 "Path the log lines are POSTed to (without leading slash).");
99 defs->optional(p.timeoutMs,
101 "Connect, read and write timeout of a single POST in milliseconds.");
102 defs->optional(p.queueSize,
104 "Maximal number of pending lines. When the server is slow or "
105 "unreachable, the oldest lines are dropped.");
106 defs->optional(p.outputFile,
107 "opossum.OutputFile",
108 "If set, every reported line is also appended to this file. Useful to "
109 "record an episode in the OPOSSUM log format. Empty = disabled.");
111 defs->optional(p.skillFilter,
112 "opossum.SkillFilter",
113 "Comma-separated skills that are not reported. These are pure body and "
114 "gaze control primitives, which say nothing at scene level.");
115 defs->optional(p.resolveObjectNames,
116 "opossum.ResolveObjectNames",
117 "Resolve object classes to their natural-language names using "
118 "PriorKnowledgeData. If false, the class name is humanized instead.");
120 defs->optional(p.memoryName,
"mem.SkillMemoryName",
"Name of the skill memory.");
121 defs->optional(p.coreSegmentName,
122 "mem.CoreSegmentName",
123 "Name of the core segment holding the skill events.");
134 if (p.resolveObjectNames)
136 lookup = spokenNames.asLookup();
138 renderer = std::make_unique<opossum::SkillEventRenderer>(
163 ARMARX_ERROR <<
"Could not use the memory '" << p.memoryName
164 <<
"'. No skill events will be reported. Reason: " << e.what();
174 ARMARX_IMPORTANT <<
"Reporting skill events from " << skillEventID <<
" as '" << p.robotName
175 <<
"' to http://" << p.host <<
":" << p.port <<
"/" << p.path <<
".";
181 subscription.release();
194 const std::string
id = snapshotID.
str();
196 std::scoped_lock lock(seenMutex);
197 if (not seen.insert(
id).second)
201 seenOrder.push_back(
id);
202 if (seenOrder.size() > SeenCapacity)
204 seen.erase(seenOrder.front());
205 seenOrder.pop_front();
210 std::map<armem::MemoryID, armem::wm::EntityInstance>
211 OpossumLogger::resolve(
const std::vector<armem::MemoryID>& snapshotIDs)
216 armem::client::QueryResult result = skillMemoryReader.queryMemoryIDs(snapshotIDs);
224 std::map<armem::MemoryID, armem::wm::EntityInstance> instances;
225 for (
const armem::MemoryID& snapshotID : snapshotIDs)
229 instances.emplace(snapshotID, result.
memory.
getSnapshot(snapshotID).getInstance(0));
231 catch (
const armem::error::ArMemError& e)
236 <<
" is not in the memory (anymore): " << e.what();
244 const std::vector<armem::MemoryID>& updatedSnapshotIDs)
246 std::vector<armem::MemoryID> newSnapshotIDs;
247 for (
const armem::MemoryID& snapshotID : updatedSnapshotIDs)
251 if (renderer->isNoiseSkill(snapshotID.
entityName))
257 if (isNew(snapshotID))
259 newSnapshotIDs.push_back(snapshotID);
262 if (newSnapshotIDs.empty())
268 std::map<armem::MemoryID, armem::wm::EntityInstance> instances;
271 instances = resolve(newSnapshotIDs);
273 catch (
const std::exception& e)
276 <<
" skill event snapshot(s): " << e.what();
280 std::vector<std::pair<armarx::core::time::DateTime, std::string>> lines;
281 for (
const auto& [snapshotID, instance] : instances)
283 if (instance.data() ==
nullptr)
291 skills::arondto::SkillStatusUpdate::FromAron(instance.data());
293 opossum::SkillEvent event;
294 event.skillName =
update.skillId.skillName;
295 event.status =
update.status;
296 event.executorName =
update.executorName;
302 namespace converter = armarx::aron::data::converter;
306 converter::AronNlohmannJSONConverter::ConvertToNlohmannJSON(
311 event.result = converter::AronNlohmannJSONConverter::ConvertToNlohmannJSON(
315 if (
const std::optional<std::string> line = renderer->render(event))
317 lines.emplace_back(event.timestamp, *line);
320 catch (
const std::exception& e)
323 << snapshotID <<
": " << e.what();
329 std::sort(lines.begin(),
331 [](
const auto& lhs,
const auto& rhs)
332 { return lhs.first.toMicroSecondsSinceEpoch() < rhs.first.toMicroSecondsSinceEpoch(); });
333 for (
auto& [time, line] : lines)
335 enqueue(std::move(line));
340 OpossumLogger::enqueue(std::string line)
344 std::size_t dropped = 0;
346 std::scoped_lock lock(queueMutex);
347 queue.push_back(std::move(line));
348 while (queue.size() >
static_cast<std::size_t
>(std::max(1, p.queueSize)))
351 dropped = ++droppedLines;
354 queueCondition.notify_one();
359 << p.queueSize <<
" pending log lines (" << dropped
360 <<
" in total). The OPOSSUM server is not keeping up.";
365 OpossumLogger::startSender()
367 if (running.exchange(
true))
371 sender = std::thread(&OpossumLogger::sendLoop,
this);
375 OpossumLogger::stopSender()
377 if (not running.exchange(
false))
381 queueCondition.notify_all();
382 if (sender.joinable())
389 OpossumLogger::sendLoop()
398 sigemptyset(&blocked);
399 sigaddset(&blocked, SIGPIPE);
400 pthread_sigmask(SIG_BLOCK, &blocked,
nullptr);
402 httplib::Client client(p.host, p.port);
403 client.set_connection_timeout(std::chrono::milliseconds(p.timeoutMs));
404 client.set_read_timeout(std::chrono::milliseconds(p.timeoutMs));
405 client.set_write_timeout(std::chrono::milliseconds(p.timeoutMs));
408 if (not p.outputFile.empty())
410 file.open(p.outputFile, std::ios::out | std::ios::app);
411 if (not file.is_open())
414 <<
"' for writing. Log lines will only be posted.";
418 const std::string path =
"/" + p.path;
419 bool reachable =
true;
422 std::optional<std::chrono::steady_clock::time_point> drainUntil;
428 std::unique_lock lock(queueMutex);
429 queueCondition.wait(lock,
430 [
this] {
return not running.load() or not queue.empty(); });
436 if (not running.load())
438 if (not drainUntil.has_value())
440 drainUntil = std::chrono::steady_clock::now() + DrainTimeout;
442 if (std::chrono::steady_clock::now() > *drainUntil)
445 <<
" pending log line(s) on shutdown.";
449 line = std::move(queue.front());
455 file << line <<
"\n";
459 const nlohmann::json message{{
"robot_name", p.robotName}, {
"message", line}};
460 const std::string payload = message.dump();
464 ARMARX_VERBOSE <<
"Publishing to http://" << p.host <<
":" << p.port << path
465 <<
" (" << payload.size() <<
" bytes): " << payload;
472 const httplib::Result result = client.Post(path, payload,
"application/json");
476 posted =
static_cast<bool>(result);
477 if (posted and (result->
status < 200 or result->
status >= 300))
485 << p.host <<
":" << p.port << path <<
" answered "
487 <<
". The log line was discarded. If this is a 404, check "
488 <<
"opossum.Path against the path the server serves.";
493 <<
"), answer: " << result->
body;
496 catch (
const std::exception& e)
499 << p.host <<
":" << p.port << path <<
": " << e.what();
502 if (posted != reachable)
507 ARMARX_IMPORTANT <<
"The OPOSSUM server at http://" << p.host <<
":" << p.port
508 << path <<
" is reachable again.";
512 ARMARX_WARNING <<
"The OPOSSUM server at http://" << p.host <<
":" << p.port
513 << path <<
" is not reachable. Log lines are discarded until "
514 <<
"it comes back. This is not an error on the robot's side.";
#define ARMARX_REGISTER_COMPONENT_EXECUTABLE(ComponentT, applicationName)
Default component property definition container.
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
SpamFilterDataPtr deactivateSpam(float deactivationDurationSec=10.0f, const std::string &identifier="", bool deactivate=true) const
disables the logging for the current line for the given amount of seconds.
void onInitComponent() override
Pure virtual hook for the subclass.
~OpossumLogger() override
void onDisconnectComponent() override
Hook for subclass.
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
void onConnectComponent() override
Pure virtual hook for the subclass.
static std::string GetDefaultName()
void onExitComponent() override
Hook for subclass.
std::string getDefaultName() const override
Retrieve default name of component.
std::string str(bool escapeDelimiters=true) const
Get a string representation of this memory ID.
Reader useReader(const MemoryID &memoryID)
Use a memory server and get a reader for it.
MemoryNameSystem & memoryNameSystem()
SubscriptionHandle subscribe(const MemoryID &subscriptionID, Callback Callback)
Base class for all exceptions thrown by the armem library.
static std::string DefaultNoiseSkillsString()
DefaultNoiseSkills() as a comma-separated string (for property defaults).
static std::set< std::string > ParseSkillList(const std::string &commaSeparated)
Split a comma-separated list into a set, trimming whitespace.
#define ARMARX_INFO
The normal logging level.
#define ARMARX_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
#define ARMARX_VERBOSE
The logging level for verbose information.
bool update(mongocxx::collection &coll, const nlohmann::json &query, const nlohmann::json &update)
std::function< std::optional< std::string >( const std::string &dataset, const std::string &className)> SpokenNameLookup
Resolves an object class to its natural-language ("spoken") name.
This file offers overloads of toIce() and fromIce() functions for STL container types.
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
auto & getSnapshot(const MemoryID &snapshotID)
Retrieve an entity snapshot.
wm::Memory memory
The slice of the memory that matched the query.