Canistergeek-IC-Motoko is the open-source tool for Internet Computer to track your project canisters cycles and memory status and collect log messages.
Canistergeek-IC-Motoko can be integrated into your canisters as motoko library which exposes following components:
Monitor
- public class that collects the data for specific canister by 5 minutes intervals.Logger
- public class that collects log messages for specific canister.
Canistergeek-IC-Motoko should be used together with Canistergeek-IC-JS - Javascript library that fetches the data from canisters, performs all necessary calculations and displays it on a webpage
Monitor
- stored data for cycles and memory consumes ~6.5Mb per year per canister (assuming data points every 5 minutes).Logger
- depends on the length of messages and their number. (canister with e.g. 10000 messages with 4096 characters each consumes 120Mb after upgrade).
New static method Canistergeek.getInformation
added to reduce information from monitor and logger.
Starting from version 0.0.6
Monitor
has new API methods:
updateInformation
method will replacecollectMetrics
methodgetInformation
method will replacegetMetrics
method
New methods provide an opportunity to evolve API in the future.
Legacy methods (collectMetrics
and getMetrics
) still available.
Data can be collected in two ways: automatically and manually
- Manually by calling
updateCanistergeekInformation
public method of your canister. - Automatically by calling
canistergeekMonitor.updateInformation
orcanistergeekMonitor.collectMetrics
in "update" methods in your canister to guarantee desired "Collect metrics" frequency.
In some cases you may want to collect metrics in every "update" method to get the full picture in realtime and see how "update" methods influence canister price and capacity.
Monitor collects the number of canister update calls
Monitor collects how many cycles left at particular time using ExperimentalCycles.balance()
.
Monitor collects how many memory bytes the canister consumes at particular time using Prim.rts_memory_size()
.
Monitor collects how many heap memory bytes the canister consumes at particular time using Prim.rts_heap_size()
.
Log messages can be collected by calling canistergeekLogger.logMessage(msg: Text)
method in "update" methods in your canister.
Logger collects time/message pairs with a maximum message length of 4096 characters.
Default number of messages (10000) can be overridden with corresponding method in realtime.
Vessel is a package manager for Motoko. Learn more.
- Add canistergeek to your
package-set.dhall
:
let
additions =
[{ name = "canistergeek"
, repo = "https://github.com/usergeek/canistergeek-ic-motoko"
, version = "v0.0.8"
, dependencies = ["base"] : List Text
}] : List Package
- Add canistergeek to your
vessel.dhall
:
dependencies = [ ..., "canistergeek" ],
- Import library in your canister
import Canistergeek "mo:canistergeek/canistergeek";
- Copy
src
folder from this repository into your project, renaming it tocanistergeek
. - Import library in your canister
import Canistergeek "../canistergeek/canistergeek";
Please perform the following steps
Initialize Canistergeek Monitor and(or) Logger as a private constant(s).
actor {
private let canistergeekMonitor = Canistergeek.Monitor();
private let canistergeekLogger = Canistergeek.Logger();
}
Add stable variable(s) and implement pre/post upgrade hooks. This step is necessary to save collected data between canister upgrades.
actor {
stable var _canistergeekMonitorUD: ? Canistergeek.UpgradeData = null;
stable var _canistergeekLoggerUD: ? Canistergeek.LoggerUpgradeData = null;
system func preupgrade() {
_canistergeekMonitorUD := ? canistergeekMonitor.preupgrade();
_canistergeekLoggerUD := ? canistergeekLogger.preupgrade();
};
system func postupgrade() {
canistergeekMonitor.postupgrade(_canistergeekMonitorUD);
_canistergeekMonitorUD := null;
canistergeekLogger.postupgrade(_canistergeekLoggerUD);
_canistergeekLoggerUD := null;
//Optional: override default number of log messages to your value
canistergeekLogger.setMaxMessagesCount(3000);
};
}
Implement public methods in the canister in order to query collected data and optionally force collecting the data
actor {
// CANISTER MONITORING
/**
* Returns canister information based on passed parameters.
* Called from browser.
*/
public query ({caller}) func getCanistergeekInformation(request: Canistergeek.GetInformationRequest): async Canistergeek.GetInformationResponse {
validateCaller(caller);
Canistergeek.getInformation(?canistergeekMonitor, ?canistergeekLogger, request);
};
/**
* Updates canister information based on passed parameters at current time.
* Called from browser or any canister "update" method.
*/
public shared ({caller}) func updateCanistergeekInformation(request: Canistergeek.UpdateInformationRequest): async () {
validateCaller(caller);
canistergeekMonitor.updateInformation(request);
};
private func validateCaller(principal: Principal) : () {
//limit access here!
};
}
Call canistergeekMonitor.collectMetrics()
(it is a shortcut for generic method canistergeekMonitor.updateInformation({metrics = ?#normal});
) method in all "update" methods in your canister in order to automatically collect all data.
Call canistergeekLogger.logMessage()
method where you want to log a message.
actor {
public shared ({caller}) func doThis(): async () {
canistergeekMonitor.collectMetrics();
canistergeekLogger.logMessage("doThis");
// rest part of the your method...
};
public shared ({caller}) func doThat(): async () {
canistergeekMonitor.collectMetrics();
canistergeekLogger.logMessage("doThat");
// rest part of the your method...
};
}
π΄π΄π΄ We highly recommend limiting access by checking caller principal π΄π΄π΄
actor {
private let adminPrincipal: Text = "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxx";
private func validateCaller(principal: Principal) : () {
//data is available only for specific principal
if (not (Principal.toText(principal) == adminPrincipal)) {
Prelude.unreachable();
}
};
}
import Time "mo:base/Time";
import Canistergeek "../canistergeek/canistergeek";
actor {
stable var _canistergeekMonitorUD: ? Canistergeek.UpgradeData = null;
private let canistergeekMonitor = Canistergeek.Monitor();
stable var _canistergeekLoggerUD: ? Canistergeek.LoggerUpgradeData = null;
private let canistergeekLogger = Canistergeek.Logger();
private let adminPrincipal: Text = "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx-xxx";
system func preupgrade() {
_canistergeekMonitorUD := ? canistergeekMonitor.preupgrade();
_canistergeekLoggerUD := ? canistergeekLogger.preupgrade();
};
system func postupgrade() {
canistergeekMonitor.postupgrade(_canistergeekMonitorUD);
_canistergeekMonitorUD := null;
canistergeekLogger.postupgrade(_canistergeekLoggerUD);
_canistergeekLoggerUD := null;
canistergeekLogger.setMaxMessagesCount(3000);
canistergeekLogger.logMessage("postupgrade");
};
public query ({caller}) func getCanistergeekInformation(request: Canistergeek.GetInformationRequest): async Canistergeek.GetInformationResponse {
validateCaller(caller);
Canistergeek.getInformation(?canistergeekMonitor, ?canistergeekLogger, request);
};
public shared ({caller}) func updateCanistergeekInformation(request: Canistergeek.UpdateInformationRequest): async () {
validateCaller(caller);
canistergeekMonitor.updateInformation(request);
};
private func validateCaller(principal: Principal) : () {
//data is available only for specific principal
if (not (Principal.toText(principal) == adminPrincipal)) {
Prelude.unreachable();
}
};
public shared ({caller}) func doThis(): async () {
canistergeekMonitor.collectMetrics();
canistergeekLogger.logMessage("doThis");
// rest part of the your method...
};
public shared ({caller}) func doThat(): async () {
canistergeekMonitor.collectMetrics();
canistergeekLogger.logMessage("doThat");
// rest part of the your method...
};
public query ({caller}) func getThis(): async Text {
"this";
};
public query ({caller}) func getThat(): async Text {
"that";
};
}