Developer forum

Forum » Dynamicweb 10 » Email alerting

Email alerting

Jelle Deridder
Reply

Hi all,

In Dynamicweb we run a scheduled task (Run SQL add-in) that writes job status to custom logging tables:

dbo.dw_ProductNumberJobLog (one row per run, Status = Started/Succeeded/Failed, plus LogId

We want to set up an email only when the latest run in dbo.dw_ProductNumberJobLog is Failed (ideally including the LogId in the email).

However:

  • Products → Queries → Shared queries only filters product data and cannot query these logging tables.
  • Insights → Monitoring → Notifications in our setup seems global (category/level + send email), but we can’t target a specific scheduled task or a custom SQL/log-table condition.
  • The dashboard SQL Query Counter widget appears to only support very limited SELECT COUNT ... queries.

What is the recommended DW10-native approach to send a notification/email based on a custom SQL log table (latest status = Failed), or to bind notifications to a specific scheduled task failure?

Thanks!


Replies

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

Hi Jelle

There is not simple way - but you can do a custom schedule task add in.

This BaseScheduledTaskAddIn runs as a Dynamicweb scheduled task, reads the latest row from your dbo.dw_ProductNumberJobLog, and if that row’s Status is Failed it sends a templated email (uses {JobName}, {LogId}, {Status}, {RunDate}, {Message} placeholders). It avoids duplicate alerts by storing the last notified LogId in a small helper table (dbo.dw_ScheduledTaskNotifications). Sending uses Dynamicweb’s EmailHandler (falls back to system notification emails / EmailHandler.SystemMailFrom() if ToEmail/FromEmail aren’t provided). All behaviour (From, To, Template, optional DB/SMTP overrides) is configurable through the scheduled-task parameters, and any email send error causes the task to surface a failure so you can see it in DW.

This is made using AI and completely untestet. Use it as a starting point.

 

using Dynamicweb.Data;
using Dynamicweb.Extensibility.AddIns;
using Dynamicweb.Extensibility.Editors;
using Dynamicweb.Logging;
using Dynamicweb.Configuration;
using Dynamicweb.Mailing;
using System;
using System.Data.SqlClient;
using System.Linq;
using System.Net.Mail;
using System.Text;

namespace Dynamicweb.Scheduling.ScheduledTaskAddIns
{
    [AddInName(".ProductNumberJobNotificationAddIn"), AddInLabel("ProductNumberJob Notification"), AddInDescription("Sends an e-mail when the latest row in dbo.dw_ProductNumberJobLog is Failed.")]
    public class ProductNumberJobNotificationAddIn : BaseScheduledTaskAddIn
    {
        // Minimal settings (required)
        [AddInParameter("From email")]
        public string? FromEmail { get; set; }

        [AddInParameter("To email (comma separated)"), AddInParameterEditor(typeof(TextParameterEditor), "inputClass=NewUIinput;TextArea=False;")]
        public string? ToEmail { get; set; }

        [AddInParameter("Template (use placeholders: {JobName}, {LogId}, {Status}, {RunDate}, {Message})"), AddInParameterEditor(typeof(TextParameterEditor), "inputClass=NewUIinput;TextArea=True;style=height:120px;")]
        public string? Template { get; set; }

        // Optional: explicit connection string (otherwise use site default)
        [AddInParameter("ConnectionString (optional)")]
        public string? ConnectionString { get; set; }

        // Optional SMTP override (kept for backwards compatibility, but not used when EmailHandler.Send is available)
        [AddInParameter("SmtpHost (optional)")]
        public string? SmtpHost { get; set; }

        [AddInParameter("SmtpPort (optional)")]
        public string? SmtpPort { get; set; }

        [AddInParameter("SmtpUseSsl (optional)")]
        public string? SmtpUseSsl { get; set; }

        [AddInParameter("SmtpUser (optional)")]
        public string? SmtpUser { get; set; }

        [AddInParameter("SmtpPass (optional)")]
        public string? SmtpPass { get; set; }

        public override bool Run()
        {
            ILogger logger = LogManager.Current.GetLogger("ScheduledTasks", Task?.LogFileName ?? "");
            try
            {
                logger.Log($"Task {ScheduledTaskName} started.");

                // Validate required parameters (ToEmail optional — fallback to system notification emails)
                if (string.IsNullOrWhiteSpace(FromEmail))
                {
                    // It's OK if FromEmail is empty; we'll fall back to EmailHandler.SystemMailFrom()
                }

                var connString = GetEffectiveConnectionString();
                if (string.IsNullOrWhiteSpace(connString))
                {
                    logger.Error("No database connection string could be found. Set the 'ConnectionString' scheduled task parameter or configure site connection string.");
                    return false;
                }

                using (var conn = new SqlConnection(connString))
                {
                    conn.Open();

                    // Get latest job log
                    var latestCmd = new SqlCommand(@"
                        SELECT TOP 1 LogId, Status, RunDate, Message
                        FROM dbo.dw_ProductNumberJobLog
                        ORDER BY RunDate DESC, LogId DESC", conn);

                    int? latestLogId = null;
                    string latestStatus = null;
                    DateTime? latestRunDate = null;
                    string latestMessage = null;

                    using (var r = latestCmd.ExecuteReader())
                    {
                        if (r.Read())
                        {
                            if (!r.IsDBNull(0)) latestLogId = r.GetInt32(0);
                            if (!r.IsDBNull(1)) latestStatus = r.GetString(1);
                            if (!r.IsDBNull(2)) latestRunDate = r.GetDateTime(2);
                            if (r.FieldCount >= 4 && !r.IsDBNull(3)) latestMessage = r.GetString(3);
                        }
                        else
                        {
                            logger.Log("No rows found in dbo.dw_ProductNumberJobLog; nothing to do.");
                            return true;
                        }
                    }

                    if (latestStatus == null)
                    {
                        logger.Log("Latest row has no Status; nothing to do.");
                        return true;
                    }

                    if (!string.Equals(latestStatus, "Failed", StringComparison.OrdinalIgnoreCase))
                    {
                        logger.Log($"Latest status is '{latestStatus}' — only 'Failed' triggers a notification. Exiting.");
                        return true;
                    }

                    if (!latestLogId.HasValue)
                    {
                        logger.Log("Latest failed row has no LogId — cannot track notifications without LogId. Exiting.");
                        return true;
                    }

                    // Check last notified
                    var jobName = GetParameterValue("JobName") ?? ScheduledTaskName ?? "ProductNumberJob";
                    var selectLastNotified = new SqlCommand("SELECT LastNotifiedLogId FROM dbo.dw_ScheduledTaskNotifications WHERE JobName = @JobName", conn);
                    selectLastNotified.Parameters.AddWithValue("@JobName", jobName);

                    int? lastNotified = null;
                    var obj = selectLastNotified.ExecuteScalar();
                    if (obj != null && obj != DBNull.Value)
                    {
                        if (int.TryParse(obj.ToString(), out var tmp)) lastNotified = tmp;
                    }

                    if (lastNotified.HasValue && lastNotified.Value == latestLogId.Value)
                    {
                        logger.Log($"Already notified for LogId {latestLogId.Value}. Exiting.");
                        return true;
                    }

                    // Render template
                    var template = Template ?? "Scheduled task {JobName} failed. LogId: {LogId}, Status: {Status}, RunDate: {RunDate}";
                    var body = RenderTemplate(template, jobName, latestLogId.Value, latestStatus, latestRunDate, latestMessage);
                    var subject = $"Scheduled task '{jobName}' failed (LogId {latestLogId.Value})";

                    // Send mail using Dynamicweb EmailHandler (preferred)
                    SendMail(FromEmail, ToEmail, subject, body, logger);

                    // Upsert last notified
                    var existsCmd = new SqlCommand("SELECT COUNT(1) FROM dbo.dw_ScheduledTaskNotifications WHERE JobName = @JobName", conn);
                    existsCmd.Parameters.AddWithValue("@JobName", jobName);
                    var exists = (int)existsCmd.ExecuteScalar();

                    if (exists > 0)
                    {
                        var upd = new SqlCommand("UPDATE dbo.dw_ScheduledTaskNotifications SET LastNotifiedLogId = @LogId, NotifiedAt = GETUTCDATE() WHERE JobName = @JobName", conn);
                        upd.Parameters.AddWithValue("@LogId", latestLogId.Value);
                        upd.Parameters.AddWithValue("@JobName", jobName);
                        upd.ExecuteNonQuery();
                    }
                    else
                    {
                        var ins = new SqlCommand("INSERT INTO dbo.dw_ScheduledTaskNotifications (JobName, LastNotifiedLogId, NotifiedAt) VALUES (@JobName, @LogId, GETUTCDATE())", conn);
                        ins.Parameters.AddWithValue("@JobName", jobName);
                        ins.Parameters.AddWithValue("@LogId", latestLogId.Value);
                        ins.ExecuteNonQuery();
                    }

                    logger.Log($"Notification processed for LogId {latestLogId.Value}.");
                }

                logger.Log($"Task {ScheduledTaskName} finished.");
                return true;
            }
            catch (Exception ex)
            {
                LogManager.System.GetLogger(LogCategory.Provider, GetType().FullName ?? "").Error("ProductNumberJobNotificationAddIn error", ex);
                return false;
            }
        }

        private string RenderTemplate(string template, string jobName, int logId, string status, DateTime? runDate, string? message)
        {
            var runDateText = runDate.HasValue ? runDate.Value.ToString("u") : "";
            return template
                .Replace("{JobName}", jobName)
                .Replace("{LogId}", logId.ToString())
                .Replace("{Status}", status ?? "")
                .Replace("{RunDate}", runDateText)
                .Replace("{Message}", message ?? "");
        }

        private void SendMail(string fromEmail, string? toEmails, string subject, string body, ILogger logger)
        {
            try
            {
                using var mail = new MailMessage();

                // Determine recipients: use ToEmail param if set, otherwise fall back to system notification emails
                if (string.IsNullOrWhiteSpace(toEmails))
                {
                    var systemEmails = SystemConfiguration.Instance.GetNotificationEmails();
                    if (systemEmails == null || systemEmails.Length == 0)
                    {
                        logger.Error("No recipient configured (ToEmail empty and system notification emails are empty). Aborting send.");
                        throw new InvalidOperationException("No recipient configured for ProductNumberJobNotificationAddIn.");
                    }

                    foreach (var em in systemEmails)
                    {
                        if (!string.IsNullOrWhiteSpace(em))
                            mail.To.Add(em);
                    }
                }
                else
                {
                    var tos = toEmails.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries).Select(t => t.Trim());
                    foreach (var to in tos)
                    {
                        mail.To.Add(new MailAddress(to));
                    }
                }

                // Determine sender: prefer explicit FromEmail, otherwise system mail
                if (!string.IsNullOrWhiteSpace(fromEmail))
                {
                    var fromAddr = new MailAddress(fromEmail);
                    mail.Sender = fromAddr;
                    mail.From = fromAddr;
                }
                else
                {
                    var systemFrom = EmailHandler.SystemMailFrom();
                    if (systemFrom != null)
                    {
                        mail.Sender = systemFrom;
                        mail.From = systemFrom;
                    }
                    else
                    {
                        logger.Error("No FromEmail provided and EmailHandler.SystemMailFrom() returned null.");
                        throw new InvalidOperationException("No sender address configured.");
                    }
                }

                mail.Subject = subject;
                mail.Body = body;
                mail.IsBodyHtml = true;
                mail.BodyEncoding = Encoding.UTF8;

                // Use EmailHandler.Send (your example pattern)
                if (!EmailHandler.Send(mail, true, false))
                {
                    logger.Debug("Failed to send ProductNumberJob notification using EmailHandler.Send.");
                    throw new InvalidOperationException("EmailHandler.Send returned false.");
                }

                logger.Log($"Email sent (subject: {subject}) to {string.Join(", ", mail.To.Cast<MailAddress>().Select(a => a.Address))}.");
            }
            catch (Exception ex)
            {
                logger.Error("Failed to send notification email.", ex);
                // Re-throw so DW scheduler marks the task as failed (adjust if you prefer to swallow send errors)
                throw;
            }
        }

        private string? GetParameterValue(string key)
        {
            try
            {
                var prop = GetType().GetProperty(key);
                if (prop != null)
                {
                    var val = prop.GetValue(this) as string;
                    if (!string.IsNullOrWhiteSpace(val)) return val;
                }

                if (Task?.Parameters is System.Collections.IDictionary dict && dict.Contains(key))
                {
                    return dict[key]?.ToString();
                }

                if (Task?.Parameters is System.Collections.Generic.IDictionary<string, string> dict2 && dict2.TryGetValue(key, out var v))
                {
                    return v;
                }
            }
            catch { /* swallow */ }

            return null;
        }

        private string GetEffectiveConnectionString()
        {
            if (!string.IsNullOrWhiteSpace(ConnectionString))
                return ConnectionString;

            var param = GetParameterValue("ConnectionString");
            if (!string.IsNullOrWhiteSpace(param))
                return param;

            try
            {
                var cs = Database.ConnectionString;
                if (!string.IsNullOrEmpty(cs)) return cs;
            }
            catch { /* ignore */ }

            try
            {
                var csFromConfig = System.Configuration.ConfigurationManager.ConnectionStrings["Dynamicweb"]?.ConnectionString;
                if (!string.IsNullOrEmpty(csFromConfig)) return csFromConfig;
            }
            catch { /* ignore */ }

            var env = Environment.GetEnvironmentVariable("DW_CONNECTIONSTRING");
            if (!string.IsNullOrEmpty(env)) return env;

            throw new InvalidOperationException("No connection string found.");
        }
    }
}

 

 

And the table needed to hold history:
CREATE TABLE dbo.dw_ScheduledTaskNotifications
(
    JobName      NVARCHAR(200) NOT NULL PRIMARY KEY,
    LastNotifiedLogId INT NULL,
    NotifiedAt   DATETIME2 NULL
);

 

 

You must be logged in to post in the forum