Repository navigation
Monitor Alerts
Notification channels, log projects, and alarms for
danny.vn/vngcloud/monitor. See Monitor for checks,
locations, and pausing and resuming them.
This page assumes cfg, ctx, and client := monitor.New(cfg) from
Monitor's Setup.
A notification channel is what GreenNode's own API calls a "notification";
the SDK says "channel" so the name does not clash with a check's
Notifications field. ListChannels and GetChannel return every channel
type the console offers, including Email, Slack, SMS, Telegram, and
Webhook. CreateChannel, UpdateChannel, and DeleteChannel work with
any of the five; Webhook needs no OTP, and every other type needs one
from SendChannelOTP, covered next, before a create or an Address
update.
types, err := client.ListChannelTypes(ctx, nil)
if err != nil {
log.Fatal(err)
}
for _, t := range types.Items {
log.Printf("%s: %s", t.ID, t.Name)
}
channels, err := client.ListChannels(ctx, &monitor.ListChannelsInput{Type: monitor.ChannelTypeWebhook})
if err != nil {
log.Fatal(err)
}
for _, ch := range channels.Items {
log.Printf("%s: %s (%s)", ch.ID, ch.Name, ch.Type)
}ListChannelTypesInput has no fields; a nil Input is valid, and the API
returns every type in one response with no paging. ListChannelsInput has
Type (empty for every type), Page, and Size; a nil Input, or one left
at its zero value, lists every channel from page 1 at size 10000.
found, err := client.GetChannel(ctx, &monitor.GetChannelInput{ChannelID: channelID})
if err != nil {
if vngcloud.IsNotFound(err) {
log.Printf("no channel %s", channelID)
} else {
log.Fatal(err)
}
}There is no get-by-ID call for a channel: GetChannel lists every page and
returns the item whose ID matches, so vngcloud.IsNotFound(err) is true both
for an unknown ID and for an account with no channels at all.
Email, Slack, SMS, and Telegram each need a one-time code before a
create or an Address update; Webhook needs none. SendChannelOTP
messages Address with the code and returns a Ref. Read the code from
the address, then pass both to CreateChannel or UpdateChannel as
OTPRef and OTP.
sent, err := client.SendChannelOTP(ctx, &monitor.SendChannelOTPInput{
Type: monitor.ChannelTypeEmail,
Address: "ops@example.com",
})
if err != nil {
log.Fatal(err)
}
// ... read the code from the address, then: ...
created, err := client.CreateChannel(ctx, &monitor.CreateChannelInput{
Name: "vngcloud-my-email",
Type: monitor.ChannelTypeEmail,
Address: "ops@example.com",
OTPRef: sent.Ref,
OTP: "123456",
})
if errors.Is(err, monitor.ErrOTPRejected) {
log.Fatal("wrong or expired code")
}SendChannelOTP refuses Webhook, and any type outside the five the
console offers, with vngcloud.ErrInvalidInput before any request. It is
a POST that messages the address, so it is never retried after a
failure that may have already reached the server: a retry could send a
second message, and a caller that wants one anyway calls
SendChannelOTP again itself. CreateChannel and UpdateChannel apply
the same no-retry rule to the validate step they run internally when
OTP is set, since a retry there could spend a code the first attempt
already validated.
OTPRef and OTP are meant to be set together, or both left empty; OTP
set with no OTPRef fails with vngcloud.ErrInvalidInput before any
request. A wrong or expired code makes CreateChannel or UpdateChannel
return monitor.ErrOTPRejected and send no create or update. Leaving
OTPRef and OTP both empty sends no otpCode, which is what Webhook
needs and every other type is refused for.
Address, OTPRef, and the OTP itself are secrets, the same as a header
value: none ever appears in an error message, and a server message that
echoes one back comes back with <redacted> in its place instead.
Sending an OTP to, and later notifying, an SMS or Email channel counts
toward that channel's free 20 messages; either one past its free 20 spends
a paid package. Slack and Telegram cost nothing extra.
created, err := client.CreateChannel(ctx, &monitor.CreateChannelInput{
Name: "vngcloud-my-webhook",
Type: monitor.ChannelTypeWebhook,
Address: "https://example.com/hook",
Headers: []monitor.ChannelHeader{{Key: "X-Token", Value: "<secret>"}},
})
if err != nil {
log.Fatal(err)
}
log.Println(created.Channel.ID)
newName := "vngcloud-my-webhook-renamed"
updated, err := client.UpdateChannel(ctx, &monitor.UpdateChannelInput{
ChannelID: created.Channel.ID,
Name: &newName,
})
if err != nil {
log.Fatal(err)
}
log.Println(updated.Channel.Name)
if _, err := client.DeleteChannel(ctx, &monitor.DeleteChannelInput{ChannelID: created.Channel.ID}); err != nil {
log.Fatal(err)
}CreateChannel accepts Type Email, Slack, SMS, Telegram, or
Webhook; any other value fails with vngcloud.ErrInvalidInput before
any request. Name, Type, and Address are required; Headers is
optional and defaults to none, and every type but Webhook also needs
OTPRef and OTP (see Sending and validating an
OTP above) to create.
CreateChannel is a POST and is never retried after a failure that may
already have reached the server, the same as CreateCheck: after any
error that is not a 4xx *vngcloud.APIError or vngcloud.ErrInvalidInput,
the channel may exist, and the caller lists channels by name before
creating it again rather than retrying blind.
UpdateChannel changes Name, Address, Headers, or any combination of
the three; a field left nil keeps the channel's current value, and at
least one must be set. To clear every header on purpose, set Headers to a
non-nil empty slice (&[]monitor.ChannelHeader{}); leaving Headers nil
resends the channel's current headers unchanged. GreenNode's own API takes a
full replacement body and clears any field a request leaves out, so
UpdateChannel reads the channel first with GetChannel and resends every
field the caller did not set itself, rather than trusting the API to leave
them alone. It keeps the channel's Type; there is no way to change a
channel's type, and UpdateChannel never sends one other than the
channel's own current Type. Changing an Email, Slack, SMS, or
Telegram channel's Address needs a fresh OTPRef and OTP (see
Sending and validating an OTP above);
the server enforces that, not the SDK. Its Output.Channel never carries
a fresh UpdatedDate, since the update's own 200 response has no body to
read one from.
The read and the write are two separate requests, with nothing to detect a
change in between: if another caller updates the channel after
UpdateChannel's own GetChannel but before its PUT lands, that change is
silently overwritten by whichever fields this call resends. Serialize
concurrent updates to the same channel elsewhere if that matters.
DeleteChannel removes a channel; GreenNode's own API strips the deleted
channel's ID from every check's Notifications, so alerting through that
channel silently stops on every check that used it, with no warning and no
undo. A second delete of the same ChannelID returns
vngcloud.IsNotFound(err) == true, the same as DeleteCheck, even though
GreenNode's own API answers that specific case with a 400 rather than a
404; a retried DeleteChannel whose first attempt already reached the
server gets this same not-found error on the retry, which the caller
treats the same as a successful delete.
Channel.Address is the email, Slack webhook URL, Telegram chat ID, phone
number, or webhook URL the channel notifies, and Channel.Headers is the
key/value pairs a Webhook channel sends with every notification; both can
hold a secret, such as a token in a header value. The SDK returns them
exactly as the API does, so a caller that will recreate or update a channel
can read them back; they never appear in log output (vngcloud.WithLogger
never logs a body). CreateChannel and UpdateChannel also strip both out
of a server error message before building the *vngcloud.APIError, in
either its raw form or the form the request's own JSON encoding produced
(a header value's literal & sent as \u0026, say): if GreenNode's own
response happens to echo the sent address or a header value back, that
call's error carries <redacted> in its place instead, unless the value is
too short to cut out safely, in which case the whole message is withheld
rather than returned with unrelated text also cut out around it. The
CLI shows Address in full only for Email, SMS, and
Telegram, whose address is personal data rather than a secret; every
other type, known or not, has its Address redacted, keeping only the
scheme and host for an http or https URL and redacting the rest whole
otherwise. Every header value is always redacted, regardless of channel
type. There is no flag to reveal either.
A log project is a vMonitor log quota: ordering one both provisions the log
project and creates its billing quota, sharing one ID. ListLogProjects and
GetLogProject read them; ListLogProjectClasses lists the classes and
retention options a project can be ordered from; QuoteCreateLogProject
prices an order without placing it; and CreateLogProject and
DeleteLogProject order and remove one.
projects, err := client.ListLogProjects(ctx, nil)
if err != nil {
log.Fatal(err)
}
for _, p := range projects.Items {
log.Printf("%s: %s (%s)", p.ID, p.ProjectName, p.Status)
}
classes, err := client.ListLogProjectClasses(ctx, nil)
if err != nil {
log.Fatal(err)
}
for _, c := range classes.Items {
log.Printf("%s: %s, %d retention options", c.Name, c.Status, len(c.Retentions))
}ListLogProjectsInput has Query, BillingStatus, Page, and Size; a
nil Input, or one left at its zero value, lists from page 0 at size 100.
Unlike ListChannels, the underlying API's page is 0-based, so Page: 0
asks for the real first page rather than being promoted to page 1, and its
Size tops out at 100: a larger value, including what ListChannels
itself defaults to, gets a 400 from the server. The output's TotalPage
counts from that same 0-based Page, so the last page is Page == TotalPage-1, not TotalPage.
GetLogProject reads one project by ID; a live project confirms
LogProject's field shape (see its doc comment).
ListLogProjectClassesInput has no fields; a nil Input is valid, and the
API returns every class in one response with no paging.
LogProjectClass.Retentions is empty for a disabled class such as
Enterprise, which has no retention options to order from.
Prices are VND totals that include VAT, with no API VAT breakdown; see Billing and Pricing.
quote, err := client.QuoteCreateLogProject(ctx, &monitor.CreateLogProjectInput{
Name: "vngcloud-my-logs",
Class: monitor.LogProjectClassPro,
RetentionDays: 7,
GBPerDay: 20,
})
if err != nil {
log.Fatal(err)
}
log.Printf("%.0f VND/month", quote.OptimumPrice)CreateLogProjectInput.Class empty prices monitor.LogProjectClassBasic.
RetentionDays 0 picks the class's only retention option; a class with more
than one, such as Pro, needs it named. GBPerDay 0 sends the chosen
option's minimum size. A class or retention the live class list does not
have returns vngcloud.ErrInvalidInput before any pricing request.
QuoteCreateLogProject sends a POST, but it prices an order without
placing one, so it is a read: it is retried after a failure that may have
already reached the server, unlike a create. It always reads the class
list fresh on every call, since the class list and its prices can change
between one request and the next; it does not share that read with
CreateLogProject, which instead reads the class list once and sends that
exact order body to both its own quote and its order (see below). A quote
response missing optimumPrice, or sending it null, fails with a
*vngcloud.APIError rather than pricing the order at a silent 0. The
quote ignores MaxPrice and NoWait: those fields govern only
CreateLogProject's price ceiling and wait.
created, err := client.CreateLogProject(ctx, &monitor.CreateLogProjectInput{
Name: "vngcloud-my-logs",
})
switch {
case errors.Is(err, monitor.ErrPriceAboveMax):
log.Fatal("quoted price exceeds MaxPrice; raise MaxPrice to order it anyway")
case errors.Is(err, dns.ErrNotSettled): // "danny.vn/vngcloud/dns"
log.Printf("log project order %s was accepted; check it later, do not order again", created.OrderID)
case err != nil:
log.Fatal(err)
}
if _, err := client.DeleteLogProject(ctx, &monitor.DeleteLogProjectInput{
LogProjectID: created.LogProject.ID,
Purge: true,
}); err != nil {
log.Fatal(err)
}Before any request, CreateLogProject rejects a NaN, +Inf, -Inf, or
negative MaxPrice with vngcloud.ErrInvalidInput: its price guard cannot
compare any of those safely, and a NaN MaxPrice in particular compares
false against every quote, which would silently disable the guard on a
paid create. It also lists projects by Name first and refuses, also with
vngcloud.ErrInvalidInput, when one already exists with that exact name,
since the post-order wait below would otherwise risk settling on that
existing project instead of the one this call orders.
CreateLogProject then reads the class list once, builds one order body
from Input, and sends that exact body first to price the order and then,
unless it prices above Input.MaxPrice, to place it, so the quote and the
order always price and create the identical resource. It refuses with
monitor.ErrPriceAboveMax, ordering nothing, when that quote's
OptimumPrice exceeds Input.MaxPrice, which defaults to 0:
CreateLogProjectInput{Name: "app"} therefore only ever orders a project
whose class and retention price at 0 VND. Raise MaxPrice to allow a paid
order; the Basic class itself allows only 3 orders or recoveries a month,
and an order past that quota returns the server's own 409 "Exceeded quota"
rather than a distinct SDK error. The order is a POST and is never
retried after a failure that may have already reached the server, the
same as CreateHostedZone: after any error that is not a 4xx
*vngcloud.APIError or vngcloud.ErrInvalidInput, the project may have
been ordered, and the caller lists projects by Name before ordering
again.
The order response is confirmed live to carry only amount, orderId, and
paymentUrl, none of LogProject's own fields: it names no project id,
name, or status. A live free order returned orderId empty or null, so
Output.OrderID is not guaranteed non-empty either way; check
created.LogProject.ID from the post-order wait instead when the project's
own ID is needed. Unless NoWait is set, CreateLogProject therefore
waits up to 120 seconds for a project named Input.Name to appear, by
listing projects, at monitor.LogProjectStatusActive, looking it up by the
name the order itself just sent rather than by anything the order response
might carry; DeleteLogProject waits up to 60 seconds for a read of the
deleted project to either come back not-found or no longer match its
pre-delete state. Either wait running out, or a read or a sleep inside it
failing, such as from a canceled ctx, returns an error wrapping
dns.ErrNotSettled: the design reuses vDNS's own sentinel here rather than
adding a new one, so the same DNS handling applies, and
the write must not be repeated. NoWait returns at once instead:
Output.LogProject stays at its zero value and only Output.OrderID is
set, from the order response's own orderId; DeleteLogProject skips its
pre-delete baseline read too.
DeleteLogProject moves a project to trash, stopping its billing; its logs
are lost. Purge also deletes it from trash, as a second request in the
same call, so a purge is never sent without the delete that precedes it,
and one DeleteLogProject call with Purge: true is the main way to use
it, though that combined call has not itself been run live yet. A separate
purge sent right after an earlier, already-settled delete was seen live to
return a plain 409 Conflict once, for a reason still unconfirmed; the SDK
adds no retry or other tolerance for that status, so a caller that hits it
decides for itself whether to call DeleteLogProject again.
A free project was seen live to leave trash on its own within a few
seconds of a delete, removed from the log-api list, the billing list, and
the trash list together, so DeleteLogProject's own pre-delete baseline
read can already 404 by the time a later Purge call runs. When that
happens with Purge set, DeleteLogProject still sends the delete and the
purge, tolerating a 404 from either, and returns success at once, with no
wait, since there is no baseline left to wait against. When the delete
and the purge both 404, with or without NoWait, nothing confirmed
LogProjectID named a real project, and DeleteLogProject returns the
not-found error instead. Without Purge, the
baseline 404 alone comes back as the SDK's ordinary not-found result, same
as any other delete. A second DeleteLogProject call on a project another
call is still in the middle of removing, rather than one the SDK's own
baseline read already saw as gone, can instead surface a plain 409
Conflict, or a 404 the delete or purge step does not tolerate; the SDK
adds no retry or other special handling for either, since the project is
already headed to the state the call asked for.
ListAlarms and GetAlarm read vMonitor alarms, of either Kind:
monitor.AlarmKindMetric or monitor.AlarmKindLog. CreateLogAlarm,
UpdateLogAlarm, and DeleteLogAlarm write log alarms; see Monitor Log
Alarms.
alarms, err := client.ListAlarms(ctx, &monitor.ListAlarmsInput{Kind: monitor.AlarmKindLog})
if err != nil {
log.Fatal(err)
}
for _, a := range alarms.Items {
log.Printf("%s: %s (%s)", a.ID, a.Name, a.Status)
}
if len(alarms.Items) > 0 {
detail, err := client.GetAlarm(ctx, &monitor.GetAlarmInput{AlarmID: alarms.Items[0].ID})
if err != nil {
log.Fatal(err)
}
log.Println(detail.Alarm.Status)
}ListAlarmsInput.Kind is required; Name, Status, and Severity narrow
the list further, empty for no filter, and Page/Size page the result
starting at page 1. The output's TotalPage counts from that same 1-based
Page, so the last page is Page == TotalPage. Every returned
Alarm.Kind is set to the Kind filter sent, since one call always lists
one kind; ListAlarms also sets each item's Log and MetricMappingID
from that same Kind, clearing whichever field does not belong to it,
rather than trusting which one the response happens to carry.
Alarm holds ID, Name, Description, Kind, Status, and Severity
for both kinds. Kind decodes from the response's own top-level type
field (LOG gives monitor.AlarmKindLog, METRIC gives
monitor.AlarmKindMetric, anything else leaves it empty); ListAlarms
also overwrites Kind, and clears whichever of Log or MetricMappingID
does not belong to it, from its own Kind filter, since one call always
lists one kind, so GetAlarm is the only call whose Alarm.Kind comes
from the response itself. A Metric alarm has Log nil and instead sets
MetricMappingID, naming the channel by the same MetricMappingID a
channel read itself returns, rather than by the channel's ID.
A Log alarm's Log field holds the fields Monitor Log
Alarms defines, decoded from the
response's alarmLog object; a response with no alarmLog at all still
decodes InAlarm and OK from top-level inAlarm and ok keys, leaving
every other LogAlarmDetail field zero.
GetAlarm's response shape has not been checked against a real alarm at
all: the test account has none of either kind, and the design's source for
the alarm calls is the console's own JavaScript, not a live capture. A live
GetAlarm for an ID with no matching alarm returned a 500, not a 404, so
unlike GetCheck and GetChannel, a missing alarm does not resolve to
vngcloud.IsNotFound(err) == true; it comes back as a plain
*vngcloud.APIError.
SDK
- Getting Started
- Authentication
- Configuration
- Errors
- Services
- Backup
- Billing and Pricing
- CDN
- Compute
- Compute Servers
- DNS
- Container Registry
- IAM
- Load Balancer
- Monitor
- Monitor Alerts
- Monitor Log Alarms
- Network
- Network Route Tables
- Network ACLs
- Network DHCP Options
- Network Virtual IPs
- Tagging
- Storage
- Storage Workload Access
- Storage Objects
- Storage Object Copy
- Storage: Projects
- Storage: Bucket Policy
- Storage: Bucket Settings
- Security
- Volume
- VKS
- Limitations
CLI