mail based rss feed aggregator
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

swap from glight to woof and substantially improve logging

ollie 707e8c50 a6d204d2

+156 -139
+1 -1
gleam.toml
··· 27 27 group_registry = ">= 1.0.0 and < 2.0.0" 28 28 gleam_httpc = ">= 5.0.0 and < 6.0.0" 29 29 gleam_http = ">= 4.3.0 and < 5.0.0" 30 - glight = ">= 2.3.0 and < 3.0.0" 30 + woof = ">= 1.2.0 and < 2.0.0" 31 31 32 32 [dev_dependencies] 33 33 gleeunit = ">= 1.0.0 and < 2.0.0"
+7 -3
justfile
··· 17 17 insert_test_data: 18 18 gleam run -m insert_test_data 19 19 20 - # delete and re-make the database; including test data 20 + # migrate the database and insert test data 21 21 prepare-db: 22 - rm db/data -r 23 - mkdir db/data -p 24 22 dbmate up 25 23 @just insert_test_data 24 + 25 + # delete the existing database and recreate it 26 + reset-db: 27 + rm db/data -r 28 + mkdir db/data -p 29 + @just prepare-db 26 30 27 31 # re-generate the database and run parrot 28 32 update-sql: prepare-db
+3 -4
manifest.toml
··· 19 19 { name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" }, 20 20 { name = "glearray", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "5E272F7CB278CC05A929C58DEB58F5D5AC6DB5B879A681E71138658D0061C38A" }, 21 21 { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 22 - { name = "glight", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "jsx"], otp_app = "glight", source = "hex", outer_checksum = "EF128D5D1D98F282C910CCAC056597E51333CD2452FCA17D4416156D229BC07F" }, 23 22 { name = "group_registry", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "group_registry", source = "hex", outer_checksum = "BC798A53D6F2406DB94E27CB45C57052CB56B32ACF7CC16EA20F6BAEC7E36B90" }, 24 23 { name = "hackney", version = "1.25.0", build_tools = ["rebar3"], requirements = ["certifi", "idna", "metrics", "mimerl", "parse_trans", "ssl_verify_fun", "unicode_util_compat"], otp_app = "hackney", source = "hex", outer_checksum = "7209BFD75FD1F42467211FF8F59EA74D6F2A9E81CBCEE95A56711EE79FD6B1D4" }, 25 24 { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 26 25 { name = "idna", version = "6.1.1", build_tools = ["rebar3"], requirements = ["unicode_util_compat"], otp_app = "idna", source = "hex", outer_checksum = "92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA" }, 27 - { name = "jsx", version = "3.1.0", build_tools = ["rebar3"], requirements = [], otp_app = "jsx", source = "hex", outer_checksum = "0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3" }, 28 - { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 26 + { name = "logging", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "BC5F18CE5DD9686100229FE5409BDC3DD5C46D5A7DF2F804AD2D8F0DD6C5060E" }, 29 27 { name = "lustre", version = "5.6.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "EE558CD4DB9F09FCC16417ADF0183A3C2DAC3E4B21ED3AC0CAE859792AB810CA" }, 30 28 { name = "metrics", version = "1.0.1", build_tools = ["rebar3"], requirements = [], otp_app = "metrics", source = "hex", outer_checksum = "69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16" }, 31 29 { name = "mimerl", version = "1.5.0", build_tools = ["rebar3"], requirements = [], otp_app = "mimerl", source = "hex", outer_checksum = "DB648CE065BAE14EA84CA8B5DD123F42F49417CEF693541110BF6F9E9BE9ECC4" }, ··· 39 37 { name = "ssl_verify_fun", version = "1.1.7", build_tools = ["mix", "rebar3", "make"], requirements = [], otp_app = "ssl_verify_fun", source = "hex", outer_checksum = "FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8" }, 40 38 { name = "tom", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "234A842F3D087D35737483F5DFB6DE9839E3366EF0CAF8726D2D094210227670" }, 41 39 { name = "unicode_util_compat", version = "0.7.1", build_tools = ["rebar3"], requirements = [], otp_app = "unicode_util_compat", source = "hex", outer_checksum = "B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642" }, 40 + { name = "woof", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "woof", source = "hex", outer_checksum = "A5DC2FCB04F23B0F440978885A167A91450B88F7760B969127187C57E05D489C" }, 42 41 { name = "youid", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_stdlib", "gleam_time"], otp_app = "youid", source = "hex", outer_checksum = "7A3ABA44B1B38BC2BDCB5474C5317AA372BE58DFBC649815EE08B03526DDA18D" }, 43 42 ] 44 43 ··· 51 50 gleam_otp = { version = ">= 1.2.0 and < 2.0.0" } 52 51 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 53 52 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 54 - glight = { version = ">= 2.3.0 and < 3.0.0" } 55 53 group_registry = { version = ">= 1.0.0 and < 2.0.0" } 56 54 logging = { version = ">= 1.3.0 and < 2.0.0" } 57 55 lustre = { version = ">= 5.6.0 and < 6.0.0" } 58 56 parrot = { version = ">= 2.2.1 and < 3.0.0" } 59 57 parsed_it = { version = ">= 0.1.1 and < 1.0.0" } 60 58 sqlight = { version = ">= 1.0.3 and < 2.0.0" } 59 + woof = { version = ">= 1.2.0 and < 2.0.0" } 61 60 youid = { version = ">= 1.6.0 and < 2.0.0" }
+32 -21
src/eater.gleam
··· 1 1 import eater/fetcher 2 - import eater/logging_helper 3 2 import eater/sender 4 3 import eater/smtp 5 4 import envoy 6 5 import gleam/erlang/process 7 6 import gleam/otp/static_supervisor as supervisor 8 7 import gleam/result 9 - import glight 10 8 import group_registry 11 9 import logging 12 10 import sqlight 13 - 14 - /// log with module related structured data 15 - /// 16 - fn log(level: logging.LogLevel, message: String) -> Nil { 17 - glight.logger() 18 - |> glight.with("module", "eater") 19 - |> logging_helper.log_logger(level, message) 20 - } 11 + import woof 21 12 22 13 pub fn main() -> Nil { 23 - glight.configure([glight.Console]) 24 - glight.set_log_level(glight.Info) 14 + configure_logging() 15 + 16 + let logger = woof.new("WARMUP") 25 17 26 - log(logging.Info, "Starting") 18 + woof.log(logger, woof.Info, "Starting", []) 27 19 28 20 let assert Ok(smtp_environment) = smtp.environment() 21 + as "Failed to get smtp environment" 29 22 30 - let assert Ok(database) = database_config() 23 + let assert Ok(database) = database_config() as "Failed to get database config" 31 24 use database <- sqlight.with_connection(database) 32 25 33 26 let registry = process.new_name("registry") ··· 41 34 |> supervisor.add(sender.factory(sender_factory)) 42 35 |> supervisor.start() 43 36 44 - log(logging.Info, "Finished starting supervisor") 37 + woof.log(logger, woof.Info, "Finished starting supervisor", []) 38 + 39 + woof.log(logger, woof.Info, "Starting senders", []) 45 40 46 41 let assert Ok(_) = 47 42 sender.start_all_existing( ··· 52 47 ) 53 48 as "Failed to start existing senders" 54 49 55 - log(logging.Info, "Finished starting senders") 50 + woof.log(logger, woof.Info, "Finished starting senders", []) 51 + 52 + woof.log(logger, woof.Info, "Starting fetchers", []) 56 53 57 - let assert Ok(fetchers) = 54 + let assert Ok(_) = 58 55 fetcher.start_all_existing(fetcher_factory, registry, database) 59 56 as "Failed to start existing fetchers" 60 57 61 - log(logging.Info, "Finished starting fetchers") 58 + woof.log(logger, woof.Info, "Finished starting fetchers", []) 62 59 63 - let assert [fetcher, ..] = fetchers 60 + process.sleep_forever() 61 + } 64 62 65 - process.send_after(fetcher.data, 5000, fetcher.CheckFeeds) 63 + fn configure_logging() { 64 + woof.configure(woof.Config( 65 + level: woof.Debug, 66 + format: woof.Json, 67 + colors: woof.Auto, 68 + )) 66 69 67 - process.sleep_forever() 70 + woof.set_sink(fn(entry, formatted) { 71 + woof.beam_logger_sink(entry, formatted) 72 + woof.default_sink(entry, formatted) 73 + }) 74 + 75 + woof.set_global_context([ 76 + woof.field("app", "eater"), 77 + woof.field("version", "0.0.1"), 78 + ]) 68 79 } 69 80 70 81 // startup stuff ----------------------------------------------------------------
+30 -29
src/eater/fetcher.gleam
··· 1 1 import eater/database 2 2 import eater/feed/rss 3 - import eater/logging_helper 4 3 import eater/pubsub 5 4 import gleam/erlang/process 6 5 import gleam/http 7 6 import gleam/http/request 8 7 import gleam/http/response 9 8 import gleam/httpc 10 - import gleam/int 11 9 import gleam/list 12 10 import gleam/otp/actor 13 11 import gleam/otp/factory_supervisor as factory 14 12 import gleam/otp/supervision 15 13 import gleam/result 16 14 import gleam/string 17 - import glight 18 15 import group_registry 19 - import logging 20 16 import parsed_it/xml 21 17 import sqlight 18 + import woof 19 + 20 + const interval_in_ms: Int = 3_600_000 22 21 23 22 /// log with module related structured data 24 23 /// 25 - fn log(level: logging.LogLevel, message: String) -> Nil { 26 - glight.logger() 27 - |> glight.with("module", "fetcher") 28 - |> logging_helper.log_logger(level, message) 24 + fn log( 25 + level: woof.Level, 26 + message: String, 27 + fields: List(#(String, String)), 28 + ) -> Nil { 29 + woof.new("FETCHER") 30 + |> woof.log(level, message, fields) 29 31 } 30 - 31 - const interval_in_ms: Int = 3_600_000 32 32 33 33 /// the factory for the fetcher actors 34 34 /// ··· 46 46 factory: process.Name(factory.Message(Start, _)), 47 47 registry: process.Name(group_registry.Message(pubsub.Message)), 48 48 database: sqlight.Connection, 49 - ) { 49 + ) -> Result(List(actor.Started(a)), actor.StartError) { 50 50 use feeds <- result.try( 51 51 database.all_feeds(database) 52 52 |> result.map_error(fn(error) { ··· 107 107 fn start(args: Start) -> Result(actor.Started(_), actor.StartError) { 108 108 let Start(feed:, registry:, database:) = args 109 109 110 - log(logging.Info, "Starting Fetcher for " <> feed.link) 110 + log(woof.Info, "Starting", [woof.field("feed", feed.link)]) 111 111 112 112 actor.new_with_initialiser(100, fn(subject) { 113 113 // TODO: persist the next timestamp to check at to the database ··· 138 138 case handled { 139 139 Ok(_) -> Nil 140 140 Error(error) -> 141 - log( 142 - logging.Error, 143 - "handle_feed failed for " 144 - <> state.feed.link 145 - <> " with error: " 146 - <> string.inspect(error), 147 - ) 141 + log(woof.Error, "Checking feed failed", [ 142 + woof.field("feed", state.feed.link), 143 + woof.field("details", string.inspect(error)), 144 + ]) 148 145 } 149 146 150 147 // cancel old timer in case we received another CheckFeeds message that wasnt sent by the timer ··· 168 165 /// 169 166 /// handles the failure cases and incrementsthe feed's cooldown accordingly 170 167 /// 171 - fn handle_feed(location: rss.Location, state: State) { 168 + fn handle_feed( 169 + location: rss.Location, 170 + state: State, 171 + ) -> Result(Nil, HandleFeedError) { 172 172 let feed = fetch_feed(location) 173 173 174 174 case feed { ··· 200 200 fn fetch_feed(location: rss.Location) -> Result(rss.Feed, FetchError) { 201 201 case location { 202 202 rss.Location(skip_n_times: 0, ..) -> { 203 - log(logging.Info, "Fetching " <> location.link) 203 + log(woof.Info, "Fetching", [woof.field("feed", location.link)]) 204 + 204 205 use req <- result.try( 205 206 request.to(location.link) 206 207 |> result.replace_error(FailedToParseUrl(location.link)), ··· 219 220 xml.parse(body, rss.decoder()) 220 221 |> result.map_error(FailedToParseRssFeed) 221 222 response -> { 222 - log( 223 - logging.Warning, 224 - "Failed to fetch " 225 - <> location.link 226 - <> " with code " 227 - <> int.to_string(response.status), 228 - ) 223 + log(woof.Warning, "Fetching failed", [ 224 + woof.field("feed", location.link), 225 + woof.int_field("status", response.status), 226 + ]) 229 227 Error(Non200Response(response)) 230 228 } 231 229 } 232 230 } 233 231 _ -> { 234 - log(logging.Info, "Skipping(cooldown) " <> location.link) 232 + log(woof.Info, "Skipping", [ 233 + woof.field("feed", location.link), 234 + woof.field("reason", "cooldown"), 235 + ]) 235 236 Error(OnCooldown) 236 237 } 237 238 }
-16
src/eater/logging_helper.gleam
··· 1 - import glight 2 - import logging 3 - 4 - pub fn log_logger(logger, level: logging.LogLevel, message: String) -> Nil { 5 - let _ = case level { 6 - logging.Emergency -> glight.emergency(logger, message) 7 - logging.Alert -> glight.alert(logger, message) 8 - logging.Critical -> glight.critical(logger, message) 9 - logging.Error -> glight.error(logger, message) 10 - logging.Warning -> glight.warning(logger, message) 11 - logging.Notice -> glight.notice(logger, message) 12 - logging.Info -> glight.info(logger, message) 13 - logging.Debug -> glight.debug(logger, message) 14 - } 15 - Nil 16 - }
+16 -8
src/eater/pubsub.gleam
··· 1 1 import eater/feed/rss 2 - import eater/logging_helper 3 2 import eater/user 4 3 import gleam/erlang/process 5 4 import gleam/list 6 5 import gleam/otp/supervision 7 - import glight 8 6 import group_registry 9 - import logging 7 + import woof 10 8 import youid/uuid 11 9 12 10 /// log with module related structured data 13 11 /// 14 - fn log(level: logging.LogLevel, message: String) -> Nil { 15 - glight.logger() 16 - |> glight.with("module", "pubsub") 17 - |> logging_helper.log_logger(level, message) 12 + fn log( 13 + level: woof.Level, 14 + message: String, 15 + fields: List(#(String, String)), 16 + ) -> Nil { 17 + woof.new("PUBSUB") 18 + |> woof.log(level, message, fields) 18 19 } 19 20 20 21 pub fn supervised( ··· 52 53 53 54 let members = group_registry.members(registry, user_channel(user)) 54 55 56 + log(woof.Info, "Published subscription change", [ 57 + woof.field("user", user.email), 58 + ]) 59 + 55 60 list.map(members, process.send(_, SubscriptionChanged)) 56 61 Nil 57 62 } ··· 68 73 69 74 let members = group_registry.members(registry, channel) 70 75 71 - log(logging.Info, feed.link <> " published " <> update.new.link) 76 + log(woof.Info, "Feed published update", [ 77 + woof.field("feed", feed.link), 78 + woof.field("update", update.new.link), 79 + ]) 72 80 73 81 list.map(members, process.send(_, FeedUpdate(update:))) 74 82
+67 -57
src/eater/sender.gleam
··· 1 1 import eater/database 2 2 import eater/feed/rss 3 - import eater/logging_helper 4 3 import eater/pubsub 5 4 import eater/smtp 6 5 import eater/user 7 6 import gcourier/smtp as gsmtp 8 7 import gleam/bool 9 8 import gleam/erlang/process 10 - import gleam/int 11 9 import gleam/list 12 10 import gleam/otp/actor 13 11 import gleam/otp/factory_supervisor as factory 12 + import gleam/otp/supervision 14 13 import gleam/result 15 14 import gleam/string 16 - import glight 17 15 import group_registry 18 - import logging 19 16 import sqlight 17 + import woof 20 18 21 19 /// log with module related structured data 22 20 /// 23 - fn log(level: logging.LogLevel, message: String) -> Nil { 24 - glight.logger() 25 - |> glight.with("module", "sender") 26 - |> logging_helper.log_logger(level, message) 21 + fn log( 22 + level: woof.Level, 23 + message: String, 24 + fields: List(#(String, String)), 25 + ) -> Nil { 26 + woof.new("SENDER") 27 + |> woof.log(level, message, fields) 27 28 } 28 29 29 30 // factory ---------------------------------------------------------------------- 30 31 31 - pub fn factory(name: process.Name(_)) { 32 + pub fn factory( 33 + name: process.Name(_), 34 + ) -> supervision.ChildSpecification(factory.Supervisor(Start, Nil)) { 32 35 factory.worker_child(sender) 33 36 |> factory.named(name) 34 37 |> factory.supervised() ··· 43 46 registry: process.Name(group_registry.Message(pubsub.Message)), 44 47 database: sqlight.Connection, 45 48 smtp_environment: smtp.SmtpEnvironment, 46 - ) { 49 + ) -> Result(List(actor.Started(Nil)), actor.StartError) { 47 50 use users <- result.try( 48 51 database.all_users(database) 49 52 |> result.map_error(fn(error) { ··· 75 78 factory_name: process.Name(_), 76 79 with: Start, 77 80 ) -> Result(actor.Started(Nil), actor.StartError) { 78 - log(logging.Info, "Starting sender for " <> with.user.email) 81 + log(woof.Info, "Starting", [woof.field("user", with.user.email)]) 79 82 let factory = factory.get_by_name(factory_name) 80 83 factory.start_child(factory, with) 81 84 } ··· 95 98 96 99 // actor ------------------------------------------------------------------------ 97 100 98 - fn sender(args: Start) { 101 + fn sender(args: Start) -> Result(actor.Started(Nil), actor.StartError) { 99 102 let Start(database:, registry:, user:, smtp_environment:) = args 100 103 101 104 actor.new_with_initialiser(100, fn(subject) { ··· 145 148 let handled = handle_feed_update(state, update) 146 149 case handled { 147 150 Ok(_) -> { 148 - log( 149 - logging.Info, 150 - state.user.email 151 - <> " has already received '" 152 - <> update.new.link 153 - <> "'. Skipping.", 154 - ) 155 - 156 151 actor.continue(State(..state, sending_failed_n_times: 0)) 152 + } 153 + Error(AlreadySent) -> { 154 + log(woof.Info, "Skipping", [ 155 + woof.field("update", update.new.link), 156 + woof.field("user", state.user.email), 157 + woof.field("reason", "Already sent"), 158 + ]) 159 + actor.continue(state) 157 160 } 158 161 Error(error) -> { 159 - log( 160 - logging.Warning, 161 - "Handling feed update " 162 - <> update.new.link 163 - <> " failed with error: " 164 - <> string.inspect(error), 165 - ) 162 + log(woof.Warning, "Feed update failed", [ 163 + woof.field("update", update.new.link), 164 + woof.field("user", state.user.email), 165 + woof.field("details", string.inspect(error)), 166 + ]) 166 167 167 168 // retry again in a bit 168 169 process.send_after( ··· 189 190 let handled = handle_feed_update(state, update) 190 191 case handled { 191 192 Ok(_) -> actor.continue(State(..state, sending_failed_n_times: 0)) 193 + Error(AlreadySent) -> actor.continue(state) 192 194 Error(error) -> { 193 - log( 194 - logging.Warning, 195 - "Attempt " 196 - <> int.to_string(attempt + 1) 197 - <> " of sending the feed update " 198 - <> update.new.link 199 - <> " failed with error: " 200 - <> string.inspect(error), 201 - ) 195 + log(woof.Warning, "Sending failed again", [ 196 + woof.field("user", state.user.email), 197 + woof.field("update", update.new.link), 198 + woof.int_field("attempt", attempt), 199 + woof.field("details", string.inspect(error)), 200 + ]) 202 201 203 202 // retry again in a bit 204 203 process.send_after( ··· 215 214 } 216 215 } 217 216 } 218 - Retry(message: pubsub.FeedUpdate(update:), ..) -> { 219 - log( 220 - logging.Warning, 221 - "Sending update " 222 - <> update.new.link 223 - <> " failed too many times. It is being dropped.", 224 - ) 217 + Retry(message: pubsub.FeedUpdate(update:), attempt:) -> { 218 + log(woof.Warning, "Dropping feed update", [ 219 + woof.field("user", state.user.email), 220 + woof.field("update", update.new.link), 221 + woof.int_field("attempt", attempt), 222 + woof.field("reason", "Failed too many times"), 223 + ]) 225 224 actor.continue(state) 226 225 } 227 226 Retry(message: pubsub.SubscriptionChanged, ..) -> ··· 235 234 FailedToCheckSentStatus(sqlight.Error) 236 235 FailedToSendEmail(gsmtp.Error) 237 236 FailedToPersistSending(sqlight.Error) 237 + AlreadySent 238 238 } 239 239 240 - fn handle_feed_update(state: State, update: rss.FeedUpdate) { 240 + fn handle_feed_update( 241 + state: State, 242 + update: rss.FeedUpdate, 243 + ) -> Result(Nil, FeedUpdateError) { 241 244 use user_has_previously_received_item <- result.try( 242 245 database.was_update_sent_to_user( 243 246 user: state.user, ··· 247 250 |> result.map_error(FailedToCheckSentStatus), 248 251 ) 249 252 250 - use <- bool.guard(when: user_has_previously_received_item, return: Ok(Nil)) 253 + use <- bool.guard( 254 + when: user_has_previously_received_item, 255 + return: Error(AlreadySent), 256 + ) 251 257 252 258 use _ <- result.try( 253 259 send_email(state, update) |> result.map_error(FailedToSendEmail), 254 260 ) 255 261 256 - log(logging.Info, "Sent " <> update.new.link <> " to " <> state.user.email) 262 + log(woof.Info, "Sent update to user", [ 263 + woof.field("update", update.new.link), 264 + woof.field("user", state.user.email), 265 + ]) 257 266 258 267 use _ <- result.try( 259 268 database.update_was_sent_to_user(update, state.user, state.database) 260 269 |> result.map_error(FailedToPersistSending), 261 270 ) 262 271 263 - log( 264 - logging.Info, 265 - "Persisted sending of " 266 - <> update.new.link 267 - <> " to " 268 - <> state.user.email 269 - <> " to database", 270 - ) 272 + log(woof.Info, "Sent status persisted", [ 273 + woof.field("user", state.user.email), 274 + woof.field("update", update.new.link), 275 + ]) 271 276 272 277 Ok(Nil) 273 278 } ··· 279 284 280 285 // subscriptions changed -------------------------------------------------------- 281 286 282 - fn update_feeds(state state: State) { 283 - log(logging.Info, "Updating feeds for " <> state.user.email) 287 + fn update_feeds(state state: State) -> Result(process.Selector(Message), String) { 288 + log(woof.Info, "Updating feeds for user", [ 289 + woof.field("user", state.user.email), 290 + ]) 284 291 285 292 let self = process.self() 286 293 ··· 304 311 ) 305 312 306 313 list.fold(feeds, selector, fn(selector, feed) { 307 - log(logging.Info, state.user.email <> " subscribed to " <> feed.link) 314 + log(woof.Info, "User subscribed to feed", [ 315 + woof.field("user", state.user.email), 316 + woof.field("feed", feed.link), 317 + ]) 308 318 309 319 pubsub.feed_selector( 310 320 feed:,