Changes to RPC object logging and Node config

- Move config from Python to Node
- Also log responses
- Only log request/response object properties in Node, as logging them
in both Node and Python is redundant
- Error-out if Node listen config is missing
- For convenience, make PeerClient copy setting properties from
ClientManager instead of referencing them
This commit is contained in:
Andrew Ferrazzutti 2022-05-15 22:17:28 -04:00
parent 454d1b72cc
commit 27b2c15ad3
9 changed files with 79 additions and 35 deletions

View File

@ -117,7 +117,6 @@ class Config(BaseBridgeConfig):
else: else:
copy("rpc.connection.host") copy("rpc.connection.host")
copy("rpc.connection.port") copy("rpc.connection.port")
copy("rpc.logging_keys")
def _get_permissions(self, key: str) -> tuple[bool, bool, bool, str]: def _get_permissions(self, key: str) -> tuple[bool, bool, bool, str]:
level = self["bridge.permissions"].get(key, "") level = self["bridge.permissions"].get(key, "")

View File

@ -252,11 +252,6 @@ rpc:
# Only for type: tcp # Only for type: tcp
host: localhost host: localhost
port: 29392 port: 29392
# Command arguments to print in logs. Optional.
# TODO Support nested arguments, like channel_props.ktid
logging_keys:
- mxid
#- channel_props
# Python logging configuration. # Python logging configuration.
# #

View File

@ -86,7 +86,6 @@ class RPCClient:
_is_connected: CancelableEvent _is_connected: CancelableEvent
_is_disconnected: CancelableEvent _is_disconnected: CancelableEvent
_connection_lock: asyncio.Lock _connection_lock: asyncio.Lock
_logging_keys: list[str]
def __init__(self, config: Config, register_config_key: str) -> None: def __init__(self, config: Config, register_config_key: str) -> None:
self.config = config self.config = config
@ -106,7 +105,6 @@ class RPCClient:
self._is_disconnected = CancelableEvent(self.loop) self._is_disconnected = CancelableEvent(self.loop)
self._is_disconnected.set() self._is_disconnected.set()
self._connection_lock = asyncio.Lock() self._connection_lock = asyncio.Lock()
self._logging_keys = config["rpc.logging_keys"]
async def connect(self) -> None: async def connect(self) -> None:
async with self._connection_lock: async with self._connection_lock:
@ -149,8 +147,7 @@ class RPCClient:
self._read_task = self.loop.create_task(self._try_read_loop()) self._read_task = self.loop.create_task(self._try_read_loop())
await self._raw_request("register", await self._raw_request("register",
peer_id=self.config["appservice.address"], peer_id=self.config["appservice.address"],
register_config=self.config[self.register_config_key], register_config=self.config[self.register_config_key])
logging_keys=self._logging_keys)
self._is_connected.set() self._is_connected.set()
self._is_disconnected.clear() self._is_disconnected.clear()
@ -250,6 +247,7 @@ class RPCClient:
self.log.debug(f"Nobody waiting for response to {req_id}") self.log.debug(f"Nobody waiting for response to {req_id}")
return return
if command == "response": if command == "response":
self.log.debug("Received response %d", req_id)
waiter.set_result(req.get("response")) waiter.set_result(req.get("response"))
elif command == "error": elif command == "error":
waiter.set_exception(RPCError(req.get("error", line))) waiter.set_exception(RPCError(req.get("error", line)))
@ -305,10 +303,7 @@ class RPCClient:
req_id = self._next_req_id req_id = self._next_req_id
future = self._response_waiters[req_id] = self.loop.create_future() future = self._response_waiters[req_id] = self.loop.create_future()
req = {"id": req_id, "command": command, **data} req = {"id": req_id, "command": command, **data}
self.log.debug("Request %d: %s", req_id, self.log.debug("Request %d: %s", req_id, command)
', '.join(
[command] +
[f"{k}: {data[k]}" for k in self._logging_keys if k in data]))
assert self._writer is not None assert self._writer is not None
self._writer.write(json.dumps(req).encode("utf-8")) self._writer.write(json.dumps(req).encode("utf-8"))
self._writer.write(b"\n") self._writer.write(b"\n")

View File

@ -3,5 +3,8 @@ If `type` is `unix`, `path` is the path where to create the socket, and `force`
If `type` is `tcp`, `port` and `host` are the host/port where to listen. If `type` is `tcp`, `port` and `host` are the host/port where to listen.
### Register timeout ### Register timeout config
`register_timeout` is the amount of time (in milliseconds) that a connecting peer must send a "register" command after initiating a connection. `register_timeout` is the amount of time (in milliseconds) that a connecting peer must send a "register" command after initiating a connection.
### Logging config
`logging_keys` specifies which properties of RPC request & response objects to print in logs. Optional.

View File

@ -3,5 +3,10 @@
"type": "unix", "type": "unix",
"path": "/data/rpc.sock", "path": "/data/rpc.sock",
"force": true "force": true
},
"register_timeout": 3000,
"logging_keys": {
"request": ["mxid"],
"response": ["status"]
} }
} }

View File

@ -4,5 +4,9 @@
"path": "/var/run/matrix-appservice-kakaotalk/rpc.sock", "path": "/var/run/matrix-appservice-kakaotalk/rpc.sock",
"force": false "force": false
}, },
"register_timeout": 3000 "register_timeout": 3000,
"logging_keys": {
"request": ["mxid"],
"response": ["status"]
}
} }

View File

@ -425,6 +425,9 @@ export default class PeerClient {
*/ */
constructor(manager, socket, connID) { constructor(manager, socket, connID) {
this.manager = manager this.manager = manager
this.registerTimeout = manager.registerTimeout
this.loggingKeys = manager.loggingKeys
this.socket = socket this.socket = socket
this.connID = connID this.connID = connID
@ -433,8 +436,6 @@ export default class PeerClient {
this.maxCommandID = 0 this.maxCommandID = 0
this.peerID = "" this.peerID = ""
this.deviceName = "KakaoTalk Bridge" this.deviceName = "KakaoTalk Bridge"
/** @type {[string]} */
this.loggingKeys = []
/** @type {Map<string, UserClient>} */ /** @type {Map<string, UserClient>} */
this.userClients = new Map() this.userClients = new Map()
} }
@ -465,10 +466,10 @@ export default class PeerClient {
setTimeout(() => { setTimeout(() => {
if (!this.peerID && !this.stopped) { if (!this.peerID && !this.stopped) {
this.log(`Didn't receive register request within ${this.manager.registerTimeout/1000} seconds, terminating`) this.log(`Didn't receive register request within ${this.registerTimeout/1000} seconds, terminating`)
this.stop("Register request timeout") this.stop("Register request timeout")
} }
}, this.manager.registerTimeout) }, this.registerTimeout)
} }
async stop(error = null) { async stop(error = null) {
@ -1194,12 +1195,10 @@ export default class PeerClient {
* @param {string} req.peer_id * @param {string} req.peer_id
* @param {Object} req.register_config * @param {Object} req.register_config
* @param {string} req.register_config.device_name * @param {string} req.register_config.device_name
* @param {?[string]} req.logging_keys
*/ */
handleRegister = async (req) => { handleRegister = async (req) => {
this.peerID = req.peer_id this.peerID = req.peer_id
this.deviceName = req.register_config.device_name || this.deviceName this.deviceName = req.register_config.device_name || this.deviceName
this.loggingKeys = req.logging_keys || this.loggingKeys
this.log(`Registered socket ${this.connID} -> ${this.peerID}`) this.log(`Registered socket ${this.connID} -> ${this.peerID}`)
if (this.manager.clients.has(this.peerID)) { if (this.manager.clients.has(this.peerID)) {
const oldClient = this.manager.clients.get(this.peerID) const oldClient = this.manager.clients.get(this.peerID)
@ -1230,12 +1229,7 @@ export default class PeerClient {
this.log("Ignoring old request", req.id) this.log("Ignoring old request", req.id)
return return
} }
this.log( this.log(`Request ${req.id}:`, this.#logObj(req, req.command, this.loggingKeys.request))
`Request ${req.id}:`,
[req.command].concat(
this.loggingKeys.filter(k => k in req).map(k => `${k}: ${JSON.stringify(req[k], this.#writeReplacer)}`))
.join(', ')
)
this.maxCommandID = req.id this.maxCommandID = req.id
let handler let handler
if (!this.peerID) { if (!this.peerID) {
@ -1306,17 +1300,40 @@ export default class PeerClient {
resp.command = "error" resp.command = "error"
if (err instanceof ProtocolError) { if (err instanceof ProtocolError) {
resp.error = err.message resp.error = err.message
this.error(`Response ${resp.id}: ${err.message}`)
} else { } else {
resp.error = err.toString() resp.error = err.toString()
this.log(`Error handling request ${resp.id} ${err.stack}`) this.error(`Response ${resp.id}: ${err.stack}`)
} }
} }
} }
// TODO Check if session is broken. If it is, close the PeerClient if (resp.response) {
const success = resp.response.success !== false
const logger = (success ? this.log : this.error).bind(this)
logger(
`Response ${resp.id}:`,
this.#logObj(
resp.response,
success ? "success" : "failure",
this.loggingKeys.response
)
)
}
await this.write(resp) await this.write(resp)
} }
#writeReplacer = function(key, value) { /**
* @param {object} obj
* @param {string} desc
* @param {[string]} keys
*/
#logObj(obj, desc, keys) {
return [desc].concat(
keys.filter(key => key in obj).map(key => `${key}: ${JSON.stringify(obj[key], this.#writeReplacer)}`)
).join(', ')
}
#writeReplacer(key, value) {
if (value instanceof Long) { if (value instanceof Long) {
return value.toString() return value.toString()
} else { } else {
@ -1324,7 +1341,7 @@ export default class PeerClient {
} }
} }
#readReviver = function(key, value) { #readReviver(key, value) {
if (value instanceof Object) { if (value instanceof Object) {
// TODO Use a type map if there will be many possible types // TODO Use a type map if there will be many possible types
if (value.__type__ == "Long") { if (value.__type__ == "Long") {

View File

@ -20,11 +20,33 @@ import path from "path"
import PeerClient from "./client.js" import PeerClient from "./client.js"
import { promisify } from "./util.js" import { promisify } from "./util.js"
/**
* @typedef {object} ListenConfig
* @property {string} type
* @property {string} path
* @property {boolean} force
*/
/**
* @typedef {object} LoggingKeys
* @property {[string]} request
* @property {[string]} response
*/
export default class ClientManager { export default class ClientManager {
constructor(listenConfig, registerTimeout) { /**
* @param {ListenConfig} listenConfig
* @param {number} registerTimeout
* @param {?LoggingKeys} loggingKeys
*/
constructor(listenConfig, registerTimeout, loggingKeys) {
if (!listenConfig) {
throw new Error("Listen config missing")
}
this.listenConfig = listenConfig this.listenConfig = listenConfig
this.registerTimeout = registerTimeout || 3000 this.registerTimeout = registerTimeout || 3000
this.loggingKeys = loggingKeys || {request: [], response: []}
this.server = net.createServer(this.acceptConnection) this.server = net.createServer(this.acceptConnection)
this.connections = [] this.connections = []
this.clients = new Map() this.clients = new Map()
@ -43,7 +65,7 @@ export default class ClientManager {
} else { } else {
const connID = this.connIDSequence++ const connID = this.connIDSequence++
this.connections[connID] = sock this.connections[connID] = sock
new PeerClient(this, sock, connID).start() new PeerClient(this, sock, connID, this.loggingKeys).start()
} }
} }

View File

@ -32,7 +32,11 @@ const configPath = args["--config"] || "config.json"
console.log("[Main] Reading config from", configPath) console.log("[Main] Reading config from", configPath)
const config = JSON.parse(fs.readFileSync(configPath).toString()) const config = JSON.parse(fs.readFileSync(configPath).toString())
const manager = new ClientManager(config.listen, config.register_timeout) const manager = new ClientManager(
config.listen,
config.register_timeout,
config.logging_keys
)
function stop() { function stop() {
manager.stop().then(() => { manager.stop().then(() => {